From 035fc26063431611a14487a6fae5bf5bbc3250a2 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 16 Jun 2025 13:08:38 +0500 Subject: [PATCH 01/26] redesign profile dropdown --- .../src/pages/common/profileDropdown.tsx | 433 +++++++++++------- 1 file changed, 265 insertions(+), 168 deletions(-) diff --git a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx index 4f083cc18..ab12d9eaf 100644 --- a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx +++ b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx @@ -1,5 +1,6 @@ import { default as Dropdown } from "antd/es/dropdown"; import { default as Menu, MenuItemProps } from "antd/es/menu"; +import { Input } from "antd"; import { Org, OrgRoleInfo } from "constants/orgConstants"; import { ORGANIZATION_SETTING } from "constants/routesURL"; import { User } from "constants/userConstants"; @@ -13,9 +14,10 @@ import { DropDownSubMenu, EditIcon, PackUpIcon, + SearchIcon, } from "lowcoder-design"; import ProfileSettingModal from "pages/setting/profile"; -import React, { useMemo } from "react"; +import React, { useMemo, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { createOrgAction, switchOrg } from "redux/reduxActions/orgActions"; import styled from "styled-components"; @@ -31,98 +33,182 @@ import type { ItemType } from "antd/es/menu/interface"; const { Item } = Menu; -const ProfileWrapper = styled.div` +const ProfileDropdownContainer = styled.div` + width: 280px; + background: white; + border-radius: 8px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12); + border: 1px solid #e1e3eb; + overflow: hidden; +`; + +const ProfileSection = styled.div` display: flex; align-items: center; - flex-direction: column; - gap: 10px; - padding: 4px 0 12px 0; - - p { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - word-break: keep-all; - } - - svg { - visibility: hidden; + padding: 16px; + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: #f8f9fa; } +`; - :hover svg { - visibility: visible; +const ProfileInfo = styled.div` + margin-left: 12px; + flex: 1; + min-width: 0; +`; - g g { - fill: #3377ff; - } - } +const ProfileName = styled.div` + font-weight: 500; + font-size: 14px; + color: #222222; + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; `; -const StyledDropdown = styled(Dropdown)` - display: flex; - flex-direction: column; - min-width: 0; - align-items: end; +const ProfileOrg = styled.div` + font-size: 12px; + color: #8b8fa3; + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; `; -const StyledPackUpIcon = styled(PackUpIcon)` - width: 20px; - height: 20px; - transform: rotate(90deg); +const ProfileRole = styled.div` + font-size: 11px; + color: #4965f2; + background: #f0f5ff; + border: 1px solid #d6e4ff; + border-radius: 4px; + padding: 2px 6px; + display: inline-block; + max-width: fit-content; `; -const SelectDropMenuItem = styled((props: MenuItemProps) => )` - .ant-dropdown-menu-item-icon { - position: absolute; - right: 0; - width: 16px; - height: 16px; - margin-right: 8px; - } +const WorkspaceSection = styled.div` + padding: 8px 0; +`; - .ant-dropdown-menu-title-content { - color: #4965f2; - padding-right: 22px; - } +const SectionHeader = styled.div` + padding: 8px 16px; + font-size: 12px; + font-weight: 500; + color: #8b8fa3; + text-transform: uppercase; + letter-spacing: 0.5px; `; -const StyledDropdownSubMenu = styled(DropDownSubMenu)` - min-width: 192px; +const SearchContainer = styled.div` + padding: 8px 12px; + border-bottom: 1px solid #f0f0f0; +`; - .ant-dropdown-menu-item { - height: 29px; +const StyledSearchInput = styled(Input)` + .ant-input { + border: 1px solid #e1e3eb; + border-radius: 6px; + font-size: 13px; + + &:focus { + border-color: #4965f2; + box-shadow: 0 0 0 2px rgba(73, 101, 242, 0.1); + } } +`; - .ant-dropdown-menu-item-divider, - .ant-dropdown-menu-submenu-title-divider { - background-color: #e1e3eb; +const WorkspaceList = styled.div` + max-height: 200px; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 4px; + } + + &::-webkit-scrollbar-track { + background: #f1f1f1; + } + + &::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 2px; + } + + &::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; } `; -const StyledNameLabel = styled.div` - width: 160px; - text-align: center; - position: relative; - margin-top: -3px; +const WorkspaceItem = styled.div<{ isActive?: boolean }>` display: flex; - justify-content: center; - - p { - font-weight: 500; - font-size: 14px; - line-height: 16px; - color: #222222; - padding-left: 16px; + align-items: center; + padding: 10px 16px; + cursor: pointer; + transition: background-color 0.2s; + background-color: ${props => props.isActive ? '#f0f5ff' : 'transparent'}; + + &:hover { + background-color: ${props => props.isActive ? '#f0f5ff' : '#f8f9fa'}; } `; -const OrgRoleLabel = styled.div` - font-size: 12px; +const WorkspaceName = styled.div` + flex: 1; + font-size: 13px; + color: #222222; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const ActiveIcon = styled(CheckoutIcon)` + width: 16px; + height: 16px; color: #4965f2; - line-height: 14px; - border: 1px solid #d6e4ff; - border-radius: 8px; - padding: 1px 5px; + margin-left: 8px; +`; + +const ActionsSection = styled.div` + border-top: 1px solid #f0f0f0; +`; + +const ActionItem = styled.div` + display: flex; + align-items: center; + padding: 12px 16px; + cursor: pointer; + transition: background-color 0.2s; + font-size: 13px; + color: #222222; + + &:hover { + background-color: #f8f9fa; + } + + svg { + width: 16px; + height: 16px; + margin-right: 10px; + } +`; + +const EmptyState = styled.div` + padding: 20px 16px; + text-align: center; + color: #8b8fa3; + font-size: 13px; +`; + +const StyledDropdown = styled(Dropdown)` + display: flex; + flex-direction: column; + min-width: 0; + align-items: end; `; type DropDownProps = { @@ -131,6 +217,7 @@ type DropDownProps = { profileSide: number; fontSize?: number; }; + export default function ProfileDropdown(props: DropDownProps) { const { avatarUrl, username, orgs, currentOrgId } = props.user; const currentOrgRoleId = props.user.orgRoleMap.get(currentOrgId); @@ -141,120 +228,130 @@ export default function ProfileDropdown(props: DropDownProps) { const settingModalVisible = useSelector(isProfileSettingModalVisible); const sysConfig = useSelector(selectSystemConfig); const dispatch = useDispatch(); - const handleClick = (e: any) => { - if (e.key === "profile") { - // click the profile, while not close the dropdown - if (checkIsMobile(window.innerWidth)) { - return; - } - dispatch(profileSettingModalVisible(true)); - } else if (e.key === "logout") { - // logout - const organizationId = localStorage.getItem('lowcoder_login_orgId'); - if (organizationId) { - localStorage.removeItem('lowcoder_login_orgId'); - } - dispatch(logoutAction({ - organizationId: organizationId || undefined, - })); - } else if (e.keyPath.includes("switchOrg")) { - if (e.key === "newOrganization") { - // create new organization - dispatch(createOrgAction(orgs)); - history.push(ORGANIZATION_SETTING); - } else if (currentOrgId !== e.key) { - // switch org - dispatch(switchOrg(e.key)); - } + const [searchTerm, setSearchTerm] = useState(""); + const [dropdownVisible, setDropdownVisible] = useState(false); + + const filteredOrgs = useMemo(() => { + if (!searchTerm.trim()) return orgs; + return orgs.filter(org => + org.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }, [orgs, searchTerm]); + + const handleProfileClick = () => { + if (checkIsMobile(window.innerWidth)) { + setDropdownVisible(false); + return; + } + dispatch(profileSettingModalVisible(true)); + setDropdownVisible(false); + }; + + const handleLogout = () => { + const organizationId = localStorage.getItem('lowcoder_login_orgId'); + if (organizationId) { + localStorage.removeItem('lowcoder_login_orgId'); + } + dispatch(logoutAction({ + organizationId: organizationId || undefined, + })); + setDropdownVisible(false); + }; + + const handleOrgSwitch = (orgId: string) => { + if (currentOrgId !== orgId) { + dispatch(switchOrg(orgId)); } + setDropdownVisible(false); + }; + + const handleCreateOrg = () => { + dispatch(createOrgAction(orgs)); + history.push(ORGANIZATION_SETTING); + setDropdownVisible(false); + }; + + const handleSearchChange = (e: React.ChangeEvent) => { + setSearchTerm(e.target.value); }; - let profileDropdownMenuItems:ItemType[] = [ - { - key: 'profile', - label: ( - - - - {username} - {!checkIsMobile(window.innerWidth) && } - + const dropdownContent = ( + e.stopPropagation()}> + {/* Profile Section */} + + + + {username} {currentOrg && ( - - {currentOrg.name} - + {currentOrg.name} )} {currentOrgRoleId && OrgRoleInfo[currentOrgRoleId] && ( - {OrgRoleInfo[currentOrgRoleId].name} + {OrgRoleInfo[currentOrgRoleId].name} )} - - ), - }, - { - key: 'logout', - label: trans("profile.logout"), - } - ] - - if(orgs && orgs.length > 0 && showSwitchOrg(props.user, sysConfig)) { - const switchOrgSubMenu = orgs.map((org: Org) => ({ - key: org.id, - icon: currentOrgId === org.id && , - label: org.name - })) - - let addWorkSpace:ItemType[] = []; - if(!checkIsMobile(window.innerWidth)) { - addWorkSpace = [ - { type: 'divider'}, - { - key: 'newOrganization', - icon: , - label: trans("profile.createOrg") - } - ] - } + + {!checkIsMobile(window.innerWidth) && } + - const switchOrgMenu = { - key: 'switchOrg', - label: trans("profile.switchOrg"), - popupOffset: checkIsMobile(window.innerWidth) ? [-200, 36] : [4, -12], - children: [ - { - key: 'joinedOrg', - label: ( - - {trans("profile.joinedOrg")} - - ), - disabled: true, - }, - ...switchOrgSubMenu, - ...addWorkSpace, - ] - } - profileDropdownMenuItems.splice(1, 0, switchOrgMenu); - } + {/* Workspaces Section */} + {orgs && orgs.length > 0 && showSwitchOrg(props.user, sysConfig) && ( + + {trans("profile.switchOrg")} + + {orgs.length > 3 && ( + + } + size="small" + /> + + )} + + + {filteredOrgs.length > 0 ? ( + filteredOrgs.map((org: Org) => ( + handleOrgSwitch(org.id)} + > + {org.name} + {currentOrgId === org.id && } + + )) + ) : ( + No workspaces found + )} + - const menu = ( - } - items={profileDropdownMenuItems} - /> + {!checkIsMobile(window.innerWidth) && ( + + + {trans("profile.createOrg")} + + )} + + )} + + {/* Actions Section */} + + + {trans("profile.logout")} + + + ); + return ( <> menu} + open={dropdownVisible} + onOpenChange={setDropdownVisible} + dropdownRender={() => dropdownContent} trigger={["click"]} + placement="bottomRight" >
Date: Mon, 16 Jun 2025 14:30:12 +0500 Subject: [PATCH 02/26] testing workspaces endpoint --- client/packages/lowcoder/src/api/userApi.ts | 26 ++++++- .../src/constants/reduxActionConstants.ts | 8 +++ .../src/pages/common/profileDropdown.tsx | 32 +++++---- .../redux/reducers/uiReducers/usersReducer.ts | 69 +++++++++++++++++++ .../src/redux/reduxActions/orgActions.ts | 12 +++- .../lowcoder/src/redux/sagas/orgSagas.ts | 42 +++++++++++ .../src/redux/selectors/orgSelectors.ts | 11 +++ 7 files changed, 185 insertions(+), 15 deletions(-) diff --git a/client/packages/lowcoder/src/api/userApi.ts b/client/packages/lowcoder/src/api/userApi.ts index c80d4b19d..6cd38cf2e 100644 --- a/client/packages/lowcoder/src/api/userApi.ts +++ b/client/packages/lowcoder/src/api/userApi.ts @@ -1,6 +1,6 @@ import Api from "api/api"; import { AxiosPromise } from "axios"; -import { OrgAndRole } from "constants/orgConstants"; +import { Org, OrgAndRole } from "constants/orgConstants"; import { BaseUserInfo, CurrentUser } from "constants/userConstants"; import { MarkUserStatusPayload, UpdateUserPayload } from "redux/reduxActions/userActions"; import { ApiResponse, GenericApiResponse } from "./apiResponses"; @@ -60,10 +60,21 @@ export interface FetchApiKeysResponse extends ApiResponse { export type GetCurrentUserResponse = GenericApiResponse; +export interface GetMyOrgsResponse extends ApiResponse { + data: { + items: Org[]; + totalCount: number; + currentPage: number; + pageSize: number; + hasMore: boolean; + }; +} + class UserApi extends Api { static thirdPartyLoginURL = "/auth/tp/login"; static thirdPartyBindURL = "/auth/tp/bind"; static usersURL = "/users"; + static myOrgsURL = "/users/myorg"; static sendVerifyCodeURL = "/auth/otp/send"; static logoutURL = "/auth/logout"; static userURL = "/users/me"; @@ -127,6 +138,19 @@ class UserApi extends Api { static getCurrentUser(): AxiosPromise { return Api.get(UserApi.currentUserURL); } + static getMyOrgs( + page: number = 1, + pageSize: number = 20, + search?: string + ): AxiosPromise { + const params = new URLSearchParams({ + page: page.toString(), + pageSize: pageSize.toString(), + ...(search && { search }) + }); + + return Api.get(`${UserApi.myOrgsURL}?${params}`); + } static getRawCurrentUser(): AxiosPromise { return Api.get(UserApi.rawCurrentUserURL); diff --git a/client/packages/lowcoder/src/constants/reduxActionConstants.ts b/client/packages/lowcoder/src/constants/reduxActionConstants.ts index aea840a5c..1694c450f 100644 --- a/client/packages/lowcoder/src/constants/reduxActionConstants.ts +++ b/client/packages/lowcoder/src/constants/reduxActionConstants.ts @@ -11,6 +11,14 @@ export const ReduxActionTypes = { FETCH_API_KEYS_SUCCESS: "FETCH_API_KEYS_SUCCESS", MOVE_TO_FOLDER2_SUCCESS: "MOVE_TO_FOLDER2_SUCCESS", + /* workspace RELATED */ + FETCH_WORKSPACES_INIT: "FETCH_WORKSPACES_INIT", + FETCH_WORKSPACES_SUCCESS: "FETCH_WORKSPACES_SUCCESS", + FETCH_WORKSPACES_ERROR: "FETCH_WORKSPACES_ERROR", + LOAD_MORE_WORKSPACES_SUCCESS: "LOAD_MORE_WORKSPACES_SUCCESS", + SEARCH_WORKSPACES_INIT: "SEARCH_WORKSPACES_INIT", + + /* plugin RELATED */ FETCH_DATA_SOURCE_TYPES: "FETCH_DATA_SOURCE_TYPES", FETCH_DATA_SOURCE_TYPES_SUCCESS: "FETCH_DATA_SOURCE_TYPES_SUCCESS", diff --git a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx index ab12d9eaf..f243b639e 100644 --- a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx +++ b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx @@ -17,9 +17,9 @@ import { SearchIcon, } from "lowcoder-design"; import ProfileSettingModal from "pages/setting/profile"; -import React, { useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { createOrgAction, switchOrg } from "redux/reduxActions/orgActions"; +import { createOrgAction, fetchWorkspacesAction, switchOrg } from "redux/reduxActions/orgActions"; import styled from "styled-components"; import history from "util/history"; import ProfileImage from "pages/common/profileImage"; @@ -30,6 +30,7 @@ import { showSwitchOrg } from "@lowcoder-ee/pages/common/customerService"; import { checkIsMobile } from "util/commonUtils"; import { selectSystemConfig } from "redux/selectors/configSelectors"; import type { ItemType } from "antd/es/menu/interface"; +import { getCurrentOrg, getWorkspaces } from "@lowcoder-ee/redux/selectors/orgSelectors"; const { Item } = Menu; @@ -219,24 +220,29 @@ type DropDownProps = { }; export default function ProfileDropdown(props: DropDownProps) { - const { avatarUrl, username, orgs, currentOrgId } = props.user; + const { avatarUrl, username, currentOrgId } = props.user; const currentOrgRoleId = props.user.orgRoleMap.get(currentOrgId); - const currentOrg = useMemo( - () => props.user.orgs.find((o) => o.id === currentOrgId), - [props.user, currentOrgId] - ); + const currentOrg = useSelector(getCurrentOrg); + const workspaces = useSelector(getWorkspaces); const settingModalVisible = useSelector(isProfileSettingModalVisible); const sysConfig = useSelector(selectSystemConfig); const dispatch = useDispatch(); const [searchTerm, setSearchTerm] = useState(""); const [dropdownVisible, setDropdownVisible] = useState(false); + // Load workspaces when dropdown opens + useEffect(() => { + if (dropdownVisible && workspaces.items.length === 0) { + dispatch(fetchWorkspacesAction(1)); + } + }, [dropdownVisible]); + // Use workspaces.items instead of props.user.orgs const filteredOrgs = useMemo(() => { - if (!searchTerm.trim()) return orgs; - return orgs.filter(org => + if (!searchTerm.trim()) return workspaces.items; + return workspaces.items.filter(org => org.name.toLowerCase().includes(searchTerm.toLowerCase()) ); - }, [orgs, searchTerm]); + }, [workspaces.items, searchTerm]); const handleProfileClick = () => { if (checkIsMobile(window.innerWidth)) { @@ -266,7 +272,7 @@ export default function ProfileDropdown(props: DropDownProps) { }; const handleCreateOrg = () => { - dispatch(createOrgAction(orgs)); + dispatch(createOrgAction(workspaces.items)); history.push(ORGANIZATION_SETTING); setDropdownVisible(false); }; @@ -293,11 +299,11 @@ export default function ProfileDropdown(props: DropDownProps) { {/* Workspaces Section */} - {orgs && orgs.length > 0 && showSwitchOrg(props.user, sysConfig) && ( + {workspaces.items.length > 0 && showSwitchOrg(props.user, sysConfig) && ( {trans("profile.switchOrg")} - {orgs.length > 3 && ( + {workspaces.items.length > 3 && ( ({ + ...state, + workspaces: { + ...state.workspaces, + loading: true + } + }), + + [ReduxActionTypes.FETCH_WORKSPACES_SUCCESS]: ( + state: UsersReduxState, + action: ReduxAction + ) => ({ + ...state, + workspaces: { + items: action.payload.items, + currentPage: action.payload.currentPage, + pageSize: action.payload.pageSize, + totalCount: action.payload.totalCount, + hasMore: action.payload.hasMore, + loading: false, + searchQuery: action.payload.searchQuery + } + }), + + [ReduxActionTypes.LOAD_MORE_WORKSPACES_SUCCESS]: ( + state: UsersReduxState, + action: ReduxAction + ) => ({ + ...state, + workspaces: { + ...state.workspaces, + items: [...state.workspaces.items, ...action.payload.items], // Append new items + currentPage: action.payload.currentPage, + hasMore: action.payload.hasMore, + loading: false + } + }), + + [ReduxActionTypes.FETCH_WORKSPACES_ERROR]: (state: UsersReduxState) => ({ + ...state, + workspaces: { + ...state.workspaces, + loading: false + } + }), }); export interface UsersReduxState { @@ -205,6 +262,18 @@ export interface UsersReduxState { error: string; profileSettingModalVisible: boolean; apiKeys: Array; + + // NEW state for workspaces + // NEW: Separate workspace state + workspaces: { + items: Org[]; // Current page of workspaces + currentPage: number; // Which page we're on + pageSize: number; // Items per page (e.g., 20) + totalCount: number; // Total workspaces available + hasMore: boolean; // Are there more pages? + loading: boolean; // Loading state + searchQuery: string; // Current search term + }; } export default usersReducer; diff --git a/client/packages/lowcoder/src/redux/reduxActions/orgActions.ts b/client/packages/lowcoder/src/redux/reduxActions/orgActions.ts index 196d7c155..e14b50efd 100644 --- a/client/packages/lowcoder/src/redux/reduxActions/orgActions.ts +++ b/client/packages/lowcoder/src/redux/reduxActions/orgActions.ts @@ -191,4 +191,14 @@ export const fetchLastMonthAPIUsageActionSuccess = (payload: OrgLastMonthAPIUsag type: ReduxActionTypes.FETCH_ORG_LAST_MONTH_API_USAGE_SUCCESS, payload: payload, }; -}; \ No newline at end of file +}; + +export const fetchWorkspacesAction = (page: number = 1, search?: string) => ({ + type: ReduxActionTypes.FETCH_WORKSPACES_INIT, + payload: { page, search } +}); + +export const loadMoreWorkspacesAction = (page: number, search?: string) => ({ + type: ReduxActionTypes.FETCH_WORKSPACES_INIT, + payload: { page, search, isLoadMore: true } +}); \ No newline at end of file diff --git a/client/packages/lowcoder/src/redux/sagas/orgSagas.ts b/client/packages/lowcoder/src/redux/sagas/orgSagas.ts index f4b2d3d3f..ba11b9482 100644 --- a/client/packages/lowcoder/src/redux/sagas/orgSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/orgSagas.ts @@ -30,6 +30,8 @@ import { getUser } from "redux/selectors/usersSelectors"; import { validateResponse } from "api/apiUtils"; import { User } from "constants/userConstants"; import { getUserSaga } from "redux/sagas/userSagas"; +import { GetMyOrgsResponse } from "@lowcoder-ee/api/userApi"; +import UserApi from "@lowcoder-ee/api/userApi"; export function* updateGroupSaga(action: ReduxAction) { try { @@ -324,6 +326,43 @@ export function* fetchLastMonthAPIUsageSaga(action: ReduxAction<{ } } +// fetch my orgs +// In userSagas.ts +export function* fetchWorkspacesSaga(action: ReduxAction<{page: number, search?: string, isLoadMore?: boolean}>) { + try { + const { page, search, isLoadMore } = action.payload; + + const response: AxiosResponse = yield call( + UserApi.getMyOrgs, + page, + 20, // pageSize + search + ); + + if (validateResponse(response)) { + const actionType = isLoadMore + ? ReduxActionTypes.LOAD_MORE_WORKSPACES_SUCCESS + : ReduxActionTypes.FETCH_WORKSPACES_SUCCESS; + + yield put({ + type: actionType, + payload: { + items: response.data.data.items, + totalCount: response.data.data.totalCount, + currentPage: response.data.data.currentPage, + pageSize: response.data.data.pageSize, + hasMore: response.data.data.hasMore, + searchQuery: search || "" + } + }); + } + } catch (error: any) { + yield put({ + type: ReduxActionTypes.FETCH_WORKSPACES_ERROR, + }); + } +} + export default function* orgSagas() { yield all([ takeLatest(ReduxActionTypes.UPDATE_GROUP_INFO, updateGroupSaga), @@ -343,5 +382,8 @@ export default function* orgSagas() { takeLatest(ReduxActionTypes.UPDATE_ORG, updateOrgSaga), takeLatest(ReduxActionTypes.FETCH_ORG_API_USAGE, fetchAPIUsageSaga), takeLatest(ReduxActionTypes.FETCH_ORG_LAST_MONTH_API_USAGE, fetchLastMonthAPIUsageSaga), + takeLatest(ReduxActionTypes.FETCH_WORKSPACES_INIT, fetchWorkspacesSaga), + + ]); } diff --git a/client/packages/lowcoder/src/redux/selectors/orgSelectors.ts b/client/packages/lowcoder/src/redux/selectors/orgSelectors.ts index 2115f1499..d60cbbad9 100644 --- a/client/packages/lowcoder/src/redux/selectors/orgSelectors.ts +++ b/client/packages/lowcoder/src/redux/selectors/orgSelectors.ts @@ -1,3 +1,5 @@ +import { Org } from "@lowcoder-ee/constants/orgConstants"; +import { getUser } from "./usersSelectors"; import { AppState } from "redux/reducers"; export const getOrgUsers = (state: AppState) => { @@ -27,3 +29,12 @@ export const getOrgApiUsage = (state: AppState) => { export const getOrgLastMonthApiUsage = (state: AppState) => { return state.ui.org.lastMonthApiUsage; } + +// Add to usersSelectors.ts +export const getWorkspaces = (state: AppState) => state.ui.users.workspaces; + +export const getCurrentOrg = (state: AppState): Org | undefined => { + const user = getUser(state); + const workspaces = getWorkspaces(state); + return workspaces.items.find(org => org.id === user.currentOrgId); +}; \ No newline at end of file From 8b4067286d5e7bed65e9609658d16c1fbd7d53ad Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 16 Jun 2025 22:03:26 +0500 Subject: [PATCH 03/26] fix profile dropdown --- .../src/pages/common/profileDropdown.tsx | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx index f243b639e..ab12d9eaf 100644 --- a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx +++ b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx @@ -17,9 +17,9 @@ import { SearchIcon, } from "lowcoder-design"; import ProfileSettingModal from "pages/setting/profile"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useMemo, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { createOrgAction, fetchWorkspacesAction, switchOrg } from "redux/reduxActions/orgActions"; +import { createOrgAction, switchOrg } from "redux/reduxActions/orgActions"; import styled from "styled-components"; import history from "util/history"; import ProfileImage from "pages/common/profileImage"; @@ -30,7 +30,6 @@ import { showSwitchOrg } from "@lowcoder-ee/pages/common/customerService"; import { checkIsMobile } from "util/commonUtils"; import { selectSystemConfig } from "redux/selectors/configSelectors"; import type { ItemType } from "antd/es/menu/interface"; -import { getCurrentOrg, getWorkspaces } from "@lowcoder-ee/redux/selectors/orgSelectors"; const { Item } = Menu; @@ -220,29 +219,24 @@ type DropDownProps = { }; export default function ProfileDropdown(props: DropDownProps) { - const { avatarUrl, username, currentOrgId } = props.user; + const { avatarUrl, username, orgs, currentOrgId } = props.user; const currentOrgRoleId = props.user.orgRoleMap.get(currentOrgId); - const currentOrg = useSelector(getCurrentOrg); - const workspaces = useSelector(getWorkspaces); + const currentOrg = useMemo( + () => props.user.orgs.find((o) => o.id === currentOrgId), + [props.user, currentOrgId] + ); const settingModalVisible = useSelector(isProfileSettingModalVisible); const sysConfig = useSelector(selectSystemConfig); const dispatch = useDispatch(); const [searchTerm, setSearchTerm] = useState(""); const [dropdownVisible, setDropdownVisible] = useState(false); - // Load workspaces when dropdown opens - useEffect(() => { - if (dropdownVisible && workspaces.items.length === 0) { - dispatch(fetchWorkspacesAction(1)); - } - }, [dropdownVisible]); - // Use workspaces.items instead of props.user.orgs const filteredOrgs = useMemo(() => { - if (!searchTerm.trim()) return workspaces.items; - return workspaces.items.filter(org => + if (!searchTerm.trim()) return orgs; + return orgs.filter(org => org.name.toLowerCase().includes(searchTerm.toLowerCase()) ); - }, [workspaces.items, searchTerm]); + }, [orgs, searchTerm]); const handleProfileClick = () => { if (checkIsMobile(window.innerWidth)) { @@ -272,7 +266,7 @@ export default function ProfileDropdown(props: DropDownProps) { }; const handleCreateOrg = () => { - dispatch(createOrgAction(workspaces.items)); + dispatch(createOrgAction(orgs)); history.push(ORGANIZATION_SETTING); setDropdownVisible(false); }; @@ -299,11 +293,11 @@ export default function ProfileDropdown(props: DropDownProps) { {/* Workspaces Section */} - {workspaces.items.length > 0 && showSwitchOrg(props.user, sysConfig) && ( + {orgs && orgs.length > 0 && showSwitchOrg(props.user, sysConfig) && ( {trans("profile.switchOrg")} - {workspaces.items.length > 3 && ( + {orgs.length > 3 && ( Date: Mon, 16 Jun 2025 23:23:58 +0500 Subject: [PATCH 04/26] setup redux, sagas for the new myorg endpoint --- client/packages/lowcoder/src/api/userApi.ts | 10 +++++---- .../src/pages/common/profileDropdown.tsx | 15 +++++++++++-- .../lowcoder/src/redux/sagas/orgSagas.ts | 21 ++++++++++++++----- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/client/packages/lowcoder/src/api/userApi.ts b/client/packages/lowcoder/src/api/userApi.ts index 6cd38cf2e..5ac5e088f 100644 --- a/client/packages/lowcoder/src/api/userApi.ts +++ b/client/packages/lowcoder/src/api/userApi.ts @@ -62,11 +62,13 @@ export type GetCurrentUserResponse = GenericApiResponse; export interface GetMyOrgsResponse extends ApiResponse { data: { - items: Org[]; - totalCount: number; - currentPage: number; + data: Array<{ + orgId: string; + orgName: string; + }>; + pageNum: number; pageSize: number; - hasMore: boolean; + total: number; }; } diff --git a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx index ab12d9eaf..31f2bfbb2 100644 --- a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx +++ b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx @@ -4,6 +4,7 @@ import { Input } from "antd"; import { Org, OrgRoleInfo } from "constants/orgConstants"; import { ORGANIZATION_SETTING } from "constants/routesURL"; import { User } from "constants/userConstants"; +import { getWorkspaces } from "redux/selectors/orgSelectors"; import { AddIcon, CheckoutIcon, @@ -17,9 +18,9 @@ import { SearchIcon, } from "lowcoder-design"; import ProfileSettingModal from "pages/setting/profile"; -import React, { useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { createOrgAction, switchOrg } from "redux/reduxActions/orgActions"; +import { createOrgAction, fetchWorkspacesAction, switchOrg } from "redux/reduxActions/orgActions"; import styled from "styled-components"; import history from "util/history"; import ProfileImage from "pages/common/profileImage"; @@ -221,6 +222,8 @@ type DropDownProps = { export default function ProfileDropdown(props: DropDownProps) { const { avatarUrl, username, orgs, currentOrgId } = props.user; const currentOrgRoleId = props.user.orgRoleMap.get(currentOrgId); + const workspaces = useSelector(getWorkspaces); + console.log("workspaces", workspaces); const currentOrg = useMemo( () => props.user.orgs.find((o) => o.id === currentOrgId), [props.user, currentOrgId] @@ -231,6 +234,14 @@ export default function ProfileDropdown(props: DropDownProps) { const [searchTerm, setSearchTerm] = useState(""); const [dropdownVisible, setDropdownVisible] = useState(false); + + // Load workspaces when dropdown opens for the first time + useEffect(() => { + if (dropdownVisible && workspaces.items.length === 0) { + dispatch(fetchWorkspacesAction(1)); + } + }, [dropdownVisible, workspaces.items.length, dispatch]); + const filteredOrgs = useMemo(() => { if (!searchTerm.trim()) return orgs; return orgs.filter(org => diff --git a/client/packages/lowcoder/src/redux/sagas/orgSagas.ts b/client/packages/lowcoder/src/redux/sagas/orgSagas.ts index ba11b9482..b529c953a 100644 --- a/client/packages/lowcoder/src/redux/sagas/orgSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/orgSagas.ts @@ -340,6 +340,17 @@ export function* fetchWorkspacesSaga(action: ReduxAction<{page: number, search?: ); if (validateResponse(response)) { + const apiData = response.data.data; + console.log("apiData", apiData); + const hasMore = (apiData.pageNum * apiData.pageSize) < apiData.total; + + // Transform orgId/orgName to match Org interface + const transformedItems = apiData.data.map(item => ({ + id: item.orgId, + name: item.orgName, + // Add other Org properties if needed (logoUrl, etc.) + })); + const actionType = isLoadMore ? ReduxActionTypes.LOAD_MORE_WORKSPACES_SUCCESS : ReduxActionTypes.FETCH_WORKSPACES_SUCCESS; @@ -347,11 +358,11 @@ export function* fetchWorkspacesSaga(action: ReduxAction<{page: number, search?: yield put({ type: actionType, payload: { - items: response.data.data.items, - totalCount: response.data.data.totalCount, - currentPage: response.data.data.currentPage, - pageSize: response.data.data.pageSize, - hasMore: response.data.data.hasMore, + items: transformedItems, + totalCount: apiData.total, + currentPage: apiData.pageNum, + pageSize: apiData.pageSize, + hasMore: hasMore, searchQuery: search || "" } }); From 35b7c68fdfd9cad0034420e47c1ee12f33aedb1b Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 16 Jun 2025 23:26:34 +0500 Subject: [PATCH 05/26] fix profile dropdown create workspace issue --- .../src/pages/common/profileDropdown.tsx | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx index 31f2bfbb2..a22cbfb44 100644 --- a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx +++ b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx @@ -198,6 +198,25 @@ const ActionItem = styled.div` } `; +const CreateWorkspaceItem = styled(ActionItem)` + color: #4965f2; + font-weight: 500; + + + &:hover { + background-color: #f0f5ff; + color: #3651d4; + } + + svg { + color: #4965f2; + } + + &:hover svg { + color: #3651d4; + } +`; + const EmptyState = styled.div` padding: 20px 16px; text-align: center; @@ -336,13 +355,10 @@ export default function ProfileDropdown(props: DropDownProps) { No workspaces found )} - - {!checkIsMobile(window.innerWidth) && ( - + {trans("profile.createOrg")} - - )} + )} From a5d372a4fb516f5d790d50f5f793bc21e831b38f Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 17 Jun 2025 12:07:37 +0500 Subject: [PATCH 06/26] test --- .../src/constants/reduxActionConstants.ts | 4 +- .../redux/reducers/uiReducers/usersReducer.ts | 52 ++----------------- .../src/redux/reduxActions/orgActions.ts | 4 +- .../lowcoder/src/redux/sagas/orgSagas.ts | 19 ++----- 4 files changed, 12 insertions(+), 67 deletions(-) diff --git a/client/packages/lowcoder/src/constants/reduxActionConstants.ts b/client/packages/lowcoder/src/constants/reduxActionConstants.ts index 1694c450f..f14f40c73 100644 --- a/client/packages/lowcoder/src/constants/reduxActionConstants.ts +++ b/client/packages/lowcoder/src/constants/reduxActionConstants.ts @@ -14,9 +14,7 @@ export const ReduxActionTypes = { /* workspace RELATED */ FETCH_WORKSPACES_INIT: "FETCH_WORKSPACES_INIT", FETCH_WORKSPACES_SUCCESS: "FETCH_WORKSPACES_SUCCESS", - FETCH_WORKSPACES_ERROR: "FETCH_WORKSPACES_ERROR", - LOAD_MORE_WORKSPACES_SUCCESS: "LOAD_MORE_WORKSPACES_SUCCESS", - SEARCH_WORKSPACES_INIT: "SEARCH_WORKSPACES_INIT", + /* plugin RELATED */ diff --git a/client/packages/lowcoder/src/redux/reducers/uiReducers/usersReducer.ts b/client/packages/lowcoder/src/redux/reducers/uiReducers/usersReducer.ts index 9d8fa393b..4146dfd62 100644 --- a/client/packages/lowcoder/src/redux/reducers/uiReducers/usersReducer.ts +++ b/client/packages/lowcoder/src/redux/reducers/uiReducers/usersReducer.ts @@ -24,12 +24,7 @@ const initialState: UsersReduxState = { apiKeys: [], workspaces: { items: [], - currentPage: 1, - pageSize: 20, totalCount: 0, - hasMore: false, - loading: false, - searchQuery: "" } }; @@ -202,51 +197,19 @@ const usersReducer = createReducer(initialState, { }), - [ReduxActionTypes.FETCH_WORKSPACES_INIT]: (state: UsersReduxState) => ({ - ...state, - workspaces: { - ...state.workspaces, - loading: true - } - }), - [ReduxActionTypes.FETCH_WORKSPACES_SUCCESS]: ( state: UsersReduxState, - action: ReduxAction - ) => ({ - ...state, - workspaces: { - items: action.payload.items, - currentPage: action.payload.currentPage, - pageSize: action.payload.pageSize, - totalCount: action.payload.totalCount, - hasMore: action.payload.hasMore, - loading: false, - searchQuery: action.payload.searchQuery - } - }), - - [ReduxActionTypes.LOAD_MORE_WORKSPACES_SUCCESS]: ( - state: UsersReduxState, - action: ReduxAction + action: ReduxAction<{ items: Org[], totalCount: number, isLoadMore?: boolean }> ) => ({ ...state, workspaces: { - ...state.workspaces, - items: [...state.workspaces.items, ...action.payload.items], // Append new items - currentPage: action.payload.currentPage, - hasMore: action.payload.hasMore, - loading: false + items: action.payload.isLoadMore + ? [...state.workspaces.items, ...action.payload.items] // Append for load more + : action.payload.items, // Replace for new search/initial load + totalCount: action.payload.totalCount } }), - [ReduxActionTypes.FETCH_WORKSPACES_ERROR]: (state: UsersReduxState) => ({ - ...state, - workspaces: { - ...state.workspaces, - loading: false - } - }), }); export interface UsersReduxState { @@ -267,12 +230,7 @@ export interface UsersReduxState { // NEW: Separate workspace state workspaces: { items: Org[]; // Current page of workspaces - currentPage: number; // Which page we're on - pageSize: number; // Items per page (e.g., 20) totalCount: number; // Total workspaces available - hasMore: boolean; // Are there more pages? - loading: boolean; // Loading state - searchQuery: string; // Current search term }; } diff --git a/client/packages/lowcoder/src/redux/reduxActions/orgActions.ts b/client/packages/lowcoder/src/redux/reduxActions/orgActions.ts index e14b50efd..f29877068 100644 --- a/client/packages/lowcoder/src/redux/reduxActions/orgActions.ts +++ b/client/packages/lowcoder/src/redux/reduxActions/orgActions.ts @@ -193,9 +193,9 @@ export const fetchLastMonthAPIUsageActionSuccess = (payload: OrgLastMonthAPIUsag }; }; -export const fetchWorkspacesAction = (page: number = 1, search?: string) => ({ +export const fetchWorkspacesAction = (page: number = 1, search?: string, isLoadMore?: boolean) => ({ type: ReduxActionTypes.FETCH_WORKSPACES_INIT, - payload: { page, search } + payload: { page, search, isLoadMore } }); export const loadMoreWorkspacesAction = (page: number, search?: string) => ({ diff --git a/client/packages/lowcoder/src/redux/sagas/orgSagas.ts b/client/packages/lowcoder/src/redux/sagas/orgSagas.ts index b529c953a..ca8123f64 100644 --- a/client/packages/lowcoder/src/redux/sagas/orgSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/orgSagas.ts @@ -341,36 +341,25 @@ export function* fetchWorkspacesSaga(action: ReduxAction<{page: number, search?: if (validateResponse(response)) { const apiData = response.data.data; - console.log("apiData", apiData); - const hasMore = (apiData.pageNum * apiData.pageSize) < apiData.total; // Transform orgId/orgName to match Org interface const transformedItems = apiData.data.map(item => ({ id: item.orgId, name: item.orgName, - // Add other Org properties if needed (logoUrl, etc.) })); - const actionType = isLoadMore - ? ReduxActionTypes.LOAD_MORE_WORKSPACES_SUCCESS - : ReduxActionTypes.FETCH_WORKSPACES_SUCCESS; - yield put({ - type: actionType, + type: ReduxActionTypes.FETCH_WORKSPACES_SUCCESS, payload: { items: transformedItems, totalCount: apiData.total, - currentPage: apiData.pageNum, - pageSize: apiData.pageSize, - hasMore: hasMore, - searchQuery: search || "" + isLoadMore: isLoadMore || false } }); } } catch (error: any) { - yield put({ - type: ReduxActionTypes.FETCH_WORKSPACES_ERROR, - }); + // Handle error in component instead of Redux + console.error('Error fetching workspaces:', error); } } From 1b63471dd63716afceacdcc40f0c3c764e104bde Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 17 Jun 2025 12:48:13 +0500 Subject: [PATCH 07/26] fix params --- client/packages/lowcoder/src/api/userApi.ts | 8 ++++---- client/packages/lowcoder/src/redux/sagas/orgSagas.ts | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/packages/lowcoder/src/api/userApi.ts b/client/packages/lowcoder/src/api/userApi.ts index 5ac5e088f..cd06186ca 100644 --- a/client/packages/lowcoder/src/api/userApi.ts +++ b/client/packages/lowcoder/src/api/userApi.ts @@ -141,14 +141,14 @@ class UserApi extends Api { return Api.get(UserApi.currentUserURL); } static getMyOrgs( - page: number = 1, + pageNum: number = 1, pageSize: number = 20, - search?: string + orgName?: string ): AxiosPromise { const params = new URLSearchParams({ - page: page.toString(), + pageNum: pageNum.toString(), pageSize: pageSize.toString(), - ...(search && { search }) + ...(orgName && { orgName }) }); return Api.get(`${UserApi.myOrgsURL}?${params}`); diff --git a/client/packages/lowcoder/src/redux/sagas/orgSagas.ts b/client/packages/lowcoder/src/redux/sagas/orgSagas.ts index ca8123f64..971739427 100644 --- a/client/packages/lowcoder/src/redux/sagas/orgSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/orgSagas.ts @@ -334,20 +334,21 @@ export function* fetchWorkspacesSaga(action: ReduxAction<{page: number, search?: const response: AxiosResponse = yield call( UserApi.getMyOrgs, - page, - 20, // pageSize - search + page, // pageNum + 5, // pageSize (changed to 5 for testing) + search // orgName ); if (validateResponse(response)) { const apiData = response.data.data; + console.log("apiData", apiData); // Transform orgId/orgName to match Org interface const transformedItems = apiData.data.map(item => ({ id: item.orgId, name: item.orgName, })); - + yield put({ type: ReduxActionTypes.FETCH_WORKSPACES_SUCCESS, payload: { @@ -358,7 +359,6 @@ export function* fetchWorkspacesSaga(action: ReduxAction<{page: number, search?: }); } } catch (error: any) { - // Handle error in component instead of Redux console.error('Error fetching workspaces:', error); } } From 049d3721fc05e3f26f7ab451520eeee03398b4f4 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 17 Jun 2025 13:46:37 +0500 Subject: [PATCH 08/26] make currentOrg selector --- .../lowcoder/src/pages/common/profileDropdown.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx index a22cbfb44..d6820be93 100644 --- a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx +++ b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx @@ -4,7 +4,7 @@ import { Input } from "antd"; import { Org, OrgRoleInfo } from "constants/orgConstants"; import { ORGANIZATION_SETTING } from "constants/routesURL"; import { User } from "constants/userConstants"; -import { getWorkspaces } from "redux/selectors/orgSelectors"; +import { getWorkspaces, getCurrentOrg } from "redux/selectors/orgSelectors"; import { AddIcon, CheckoutIcon, @@ -242,11 +242,7 @@ export default function ProfileDropdown(props: DropDownProps) { const { avatarUrl, username, orgs, currentOrgId } = props.user; const currentOrgRoleId = props.user.orgRoleMap.get(currentOrgId); const workspaces = useSelector(getWorkspaces); - console.log("workspaces", workspaces); - const currentOrg = useMemo( - () => props.user.orgs.find((o) => o.id === currentOrgId), - [props.user, currentOrgId] - ); + const currentOrg = useSelector(getCurrentOrg); const settingModalVisible = useSelector(isProfileSettingModalVisible); const sysConfig = useSelector(selectSystemConfig); const dispatch = useDispatch(); From a9025323b25484e93f1a889344cd2c4a80f1edae Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 17 Jun 2025 16:16:32 +0500 Subject: [PATCH 09/26] add page size param --- .../packages/lowcoder/src/pages/common/profileDropdown.tsx | 3 ++- .../packages/lowcoder/src/redux/reduxActions/orgActions.ts | 4 ++-- client/packages/lowcoder/src/redux/sagas/orgSagas.ts | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx index d6820be93..50c393f02 100644 --- a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx +++ b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx @@ -253,7 +253,8 @@ export default function ProfileDropdown(props: DropDownProps) { // Load workspaces when dropdown opens for the first time useEffect(() => { if (dropdownVisible && workspaces.items.length === 0) { - dispatch(fetchWorkspacesAction(1)); + // fetch all workspaces for the dropdown + dispatch(fetchWorkspacesAction(1, 1000)); } }, [dropdownVisible, workspaces.items.length, dispatch]); diff --git a/client/packages/lowcoder/src/redux/reduxActions/orgActions.ts b/client/packages/lowcoder/src/redux/reduxActions/orgActions.ts index f29877068..7b94ee84d 100644 --- a/client/packages/lowcoder/src/redux/reduxActions/orgActions.ts +++ b/client/packages/lowcoder/src/redux/reduxActions/orgActions.ts @@ -193,9 +193,9 @@ export const fetchLastMonthAPIUsageActionSuccess = (payload: OrgLastMonthAPIUsag }; }; -export const fetchWorkspacesAction = (page: number = 1, search?: string, isLoadMore?: boolean) => ({ +export const fetchWorkspacesAction = (page: number = 1,pageSize: number = 20, search?: string, isLoadMore?: boolean) => ({ type: ReduxActionTypes.FETCH_WORKSPACES_INIT, - payload: { page, search, isLoadMore } + payload: { page, pageSize, search, isLoadMore } }); export const loadMoreWorkspacesAction = (page: number, search?: string) => ({ diff --git a/client/packages/lowcoder/src/redux/sagas/orgSagas.ts b/client/packages/lowcoder/src/redux/sagas/orgSagas.ts index 971739427..31abe2384 100644 --- a/client/packages/lowcoder/src/redux/sagas/orgSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/orgSagas.ts @@ -328,14 +328,14 @@ export function* fetchLastMonthAPIUsageSaga(action: ReduxAction<{ // fetch my orgs // In userSagas.ts -export function* fetchWorkspacesSaga(action: ReduxAction<{page: number, search?: string, isLoadMore?: boolean}>) { +export function* fetchWorkspacesSaga(action: ReduxAction<{page: number, pageSize: number, search?: string, isLoadMore?: boolean}>) { try { - const { page, search, isLoadMore } = action.payload; + const { page, pageSize, search, isLoadMore } = action.payload; const response: AxiosResponse = yield call( UserApi.getMyOrgs, page, // pageNum - 5, // pageSize (changed to 5 for testing) + pageSize, // pageSize (changed to 5 for testing) search // orgName ); From 7709d58dd19715d97a4eefb8457ed2d2919f4682 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 17 Jun 2025 16:31:49 +0500 Subject: [PATCH 10/26] replace orgs data with myorg for dropdown --- .../lowcoder/src/pages/common/profileDropdown.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx index 50c393f02..b2201f919 100644 --- a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx +++ b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx @@ -259,11 +259,11 @@ export default function ProfileDropdown(props: DropDownProps) { }, [dropdownVisible, workspaces.items.length, dispatch]); const filteredOrgs = useMemo(() => { - if (!searchTerm.trim()) return orgs; - return orgs.filter(org => + if (!searchTerm.trim()) return workspaces.items; + return workspaces.items.filter(org => org.name.toLowerCase().includes(searchTerm.toLowerCase()) ); - }, [orgs, searchTerm]); + }, [workspaces.items, searchTerm]); const handleProfileClick = () => { if (checkIsMobile(window.innerWidth)) { @@ -320,11 +320,11 @@ export default function ProfileDropdown(props: DropDownProps) { {/* Workspaces Section */} - {orgs && orgs.length > 0 && showSwitchOrg(props.user, sysConfig) && ( + {workspaces.items && workspaces.items.length > 0 && showSwitchOrg(props.user, sysConfig) && ( {trans("profile.switchOrg")} - {orgs.length > 3 && ( + {workspaces.items.length > 3 && ( Date: Tue, 17 Jun 2025 18:46:03 +0500 Subject: [PATCH 11/26] remove dispatch from the profile dropdown --- .../lowcoder/src/pages/common/profileDropdown.tsx | 8 +------- client/packages/lowcoder/src/redux/sagas/userSagas.ts | 4 ++++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx index b2201f919..d18ac17ee 100644 --- a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx +++ b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx @@ -250,13 +250,7 @@ export default function ProfileDropdown(props: DropDownProps) { const [dropdownVisible, setDropdownVisible] = useState(false); - // Load workspaces when dropdown opens for the first time - useEffect(() => { - if (dropdownVisible && workspaces.items.length === 0) { - // fetch all workspaces for the dropdown - dispatch(fetchWorkspacesAction(1, 1000)); - } - }, [dropdownVisible, workspaces.items.length, dispatch]); + const filteredOrgs = useMemo(() => { if (!searchTerm.trim()) return workspaces.items; diff --git a/client/packages/lowcoder/src/redux/sagas/userSagas.ts b/client/packages/lowcoder/src/redux/sagas/userSagas.ts index 5b980953f..74b2f6990 100644 --- a/client/packages/lowcoder/src/redux/sagas/userSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/userSagas.ts @@ -25,6 +25,7 @@ import { messageInstance } from "lowcoder-design/src/components/GlobalInstances" import { AuthSearchParams } from "constants/authConstants"; import { saveAuthSearchParams } from "pages/userAuth/authUtils"; import { initTranslator } from "i18n"; +import { fetchWorkspacesAction } from "../reduxActions/orgActions"; function validResponseData(response: AxiosResponse) { return response && response.data && response.data.data; @@ -71,10 +72,13 @@ export function* getUserSaga() { orgs: orgs, orgRoleMap: orgRoleMap, }; + yield put({ type: ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS, payload: user, }); + // fetch all workspaces and store in redux + yield put(fetchWorkspacesAction(1, 1000)); } } catch (error: any) { yield put({ From f8513531c57705ea15b46bb0336aff9437ce2391 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 18 Jun 2025 13:57:50 +0500 Subject: [PATCH 12/26] fetch 10 workspaces initially --- client/packages/lowcoder/src/redux/sagas/userSagas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/redux/sagas/userSagas.ts b/client/packages/lowcoder/src/redux/sagas/userSagas.ts index 74b2f6990..d0dfdba06 100644 --- a/client/packages/lowcoder/src/redux/sagas/userSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/userSagas.ts @@ -78,7 +78,7 @@ export function* getUserSaga() { payload: user, }); // fetch all workspaces and store in redux - yield put(fetchWorkspacesAction(1, 1000)); + yield put(fetchWorkspacesAction(1, 10)); } } catch (error: any) { yield put({ From 9ea107bcd249acd81d9d67a57a4f0af594fb620c Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 18 Jun 2025 16:37:21 +0500 Subject: [PATCH 13/26] add pagination and filtering for the dropdown --- .../src/pages/common/profileDropdown.tsx | 261 +++++++++++++++--- 1 file changed, 221 insertions(+), 40 deletions(-) diff --git a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx index d18ac17ee..0dbbb9f2c 100644 --- a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx +++ b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx @@ -31,7 +31,9 @@ import { showSwitchOrg } from "@lowcoder-ee/pages/common/customerService"; import { checkIsMobile } from "util/commonUtils"; import { selectSystemConfig } from "redux/selectors/configSelectors"; import type { ItemType } from "antd/es/menu/interface"; - +import { Pagination } from "antd"; +import { debounce } from "lodash"; +import UserApi from "api/userApi"; const { Item } = Menu; const ProfileDropdownContainer = styled.div` @@ -231,6 +233,46 @@ const StyledDropdown = styled(Dropdown)` align-items: end; `; + +const PaginationContainer = styled.div` + padding: 12px 16px; + border-top: 1px solid #f0f0f0; + display: flex; + justify-content: center; + + .ant-pagination { + margin: 0; + + .ant-pagination-item { + min-width: 24px; + height: 24px; + line-height: 22px; + font-size: 12px; + margin-right: 4px; + } + + .ant-pagination-prev, + .ant-pagination-next { + min-width: 24px; + height: 24px; + line-height: 22px; + margin-right: 4px; + } + + .ant-pagination-item-link { + font-size: 11px; + } + } +`; +const LoadingSpinner = styled.div` + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + color: #8b8fa3; + font-size: 13px; +`; + type DropDownProps = { onClick?: (text: string) => void; user: User; @@ -246,9 +288,107 @@ export default function ProfileDropdown(props: DropDownProps) { const settingModalVisible = useSelector(isProfileSettingModalVisible); const sysConfig = useSelector(selectSystemConfig); const dispatch = useDispatch(); - const [searchTerm, setSearchTerm] = useState(""); - const [dropdownVisible, setDropdownVisible] = useState(false); + // Local state for pagination and search + const [searchTerm, setSearchTerm] = useState(""); + const [dropdownVisible, setDropdownVisible] = useState(false); + const [currentPageWorkspaces, setCurrentPageWorkspaces] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const [totalCount, setTotalCount] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [isSearching, setIsSearching] = useState(false); + + const pageSize = 10; + + // Determine which workspaces to show + const displayWorkspaces = useMemo(() => { + if (searchTerm.trim()) { + return currentPageWorkspaces; // Search results + } + if (currentPage === 1) { + return workspaces.items; // First page from Redux + } + return currentPageWorkspaces; // Other pages from API + }, [searchTerm, currentPage, workspaces.items, currentPageWorkspaces]); + + // Update total count based on context + useEffect(() => { + if (searchTerm.trim()) { + // Keep search result count + return; + } + if (currentPage === 1) { + setTotalCount(workspaces.totalCount); + } + }, [searchTerm, currentPage, workspaces.totalCount]); + + // Fetch workspaces for specific page + const fetchWorkspacesPage = async (page: number, search?: string) => { + setIsLoading(true); + try { + const response = await UserApi.getMyOrgs(page, pageSize, search); + if (response.data.success) { + const apiData = response.data.data; + const transformedItems = apiData.data.map(item => ({ + id: item.orgId, + name: item.orgName, + })); + + setCurrentPageWorkspaces(transformedItems as Org[]); + setTotalCount(apiData.total); + } + } catch (error) { + console.error('Error fetching workspaces:', error); + setCurrentPageWorkspaces([]); + } finally { + setIsLoading(false); + } +}; + + // Handle page change + const handlePageChange = (page: number) => { + setCurrentPage(page); + if (page === 1 && !searchTerm.trim()) { + // Use Redux data for first page when not searching + setCurrentPageWorkspaces([]); + } else { + // Fetch from API for other pages or when searching + fetchWorkspacesPage(page, searchTerm.trim() || undefined); + } + }; + + + // Debounced search function + const debouncedSearch = useMemo( + () => debounce(async (term: string) => { + if (!term.trim()) { + setCurrentPage(1); + setCurrentPageWorkspaces([]); + setTotalCount(workspaces.totalCount); + setIsSearching(false); + return; + } + + setIsSearching(true); + setCurrentPage(1); + await fetchWorkspacesPage(1, term); + setIsSearching(false); + }, 300), + [workspaces.totalCount] + ); + + + + // Reset state when dropdown closes + useEffect(() => { + if (!dropdownVisible) { + setCurrentPageWorkspaces([]); + setCurrentPage(1); + setSearchTerm(""); + setTotalCount(workspaces.totalCount); + setIsSearching(false); + } + }, [dropdownVisible, workspaces.totalCount]); @@ -292,12 +432,15 @@ export default function ProfileDropdown(props: DropDownProps) { setDropdownVisible(false); }; - const handleSearchChange = (e: React.ChangeEvent) => { - setSearchTerm(e.target.value); - }; + // Handle search input change + const handleSearchChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setSearchTerm(value); + debouncedSearch(value); +}; const dropdownContent = ( - e.stopPropagation()}> + e.stopPropagation()}> {/* Profile Section */} @@ -310,48 +453,86 @@ export default function ProfileDropdown(props: DropDownProps) { {OrgRoleInfo[currentOrgRoleId].name} )} - {!checkIsMobile(window.innerWidth) && } + {!checkIsMobile(window.innerWidth) && ( + + )} {/* Workspaces Section */} - {workspaces.items && workspaces.items.length > 0 && showSwitchOrg(props.user, sysConfig) && ( - - {trans("profile.switchOrg")} - - {workspaces.items.length > 3 && ( - - } - size="small" - /> - - )} + {workspaces.items && + workspaces.items.length > 0 && + showSwitchOrg(props.user, sysConfig) && ( + + {trans("profile.switchOrg")} + + {workspaces.items.length > 3 && ( + + } + size="small" + /> + + )} - - {filteredOrgs.length > 0 ? ( - filteredOrgs.map((org: Org) => ( - handleOrgSwitch(org.id)} - > - {org.name} - {currentOrgId === org.id && } - - )) - ) : ( - No workspaces found + {/* Workspaces List */} + + {isSearching || isLoading ? ( + + + {isSearching ? "Searching..." : "Loading..."} + + ) : displayWorkspaces.length > 0 ? ( + displayWorkspaces.map((org: Org) => ( + handleOrgSwitch(org.id)} + > + {org.name} + {currentOrgId === org.id && } + + )) + ) : ( + + {searchTerm.trim() + ? "No workspaces found" + : "No workspaces available"} + + )} + + + {/* Pagination */} + {totalCount > pageSize && !isSearching && !isLoading && ( + + + `${range[0]}-${range[1]} of ${total}` + } + onChange={handlePageChange} + simple={totalCount > 100} + /> + )} - {trans("profile.createOrg")} - - )} + + )} {/* Actions Section */} From 28a2101b3172890fbbb66a2d338eaad1051096dd Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 18 Jun 2025 17:51:25 +0500 Subject: [PATCH 14/26] refactor profile dropdown --- .../src/pages/common/WorkspaceSection.tsx | 298 +++++++++++++ .../src/pages/common/profileDropdown.tsx | 394 +----------------- .../lowcoder/src/util/useWorkspaceManager.ts | 183 ++++++++ 3 files changed, 500 insertions(+), 375 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx create mode 100644 client/packages/lowcoder/src/util/useWorkspaceManager.ts diff --git a/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx b/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx new file mode 100644 index 000000000..8a57715f5 --- /dev/null +++ b/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx @@ -0,0 +1,298 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; +import { Input, Pagination } from 'antd'; +import { User } from 'constants/userConstants'; +import { switchOrg, createOrgAction } from 'redux/reduxActions/orgActions'; +import { selectSystemConfig } from 'redux/selectors/configSelectors'; +import { showSwitchOrg } from '@lowcoder-ee/pages/common/customerService'; +import { useWorkspaceManager } from 'util/useWorkspaceManager'; +import { trans } from 'i18n'; +import { + AddIcon, + CheckoutIcon, + PackUpIcon, + SearchIcon, +} from 'lowcoder-design'; +import { ORGANIZATION_SETTING } from 'constants/routesURL'; +import history from 'util/history'; +import { Org } from 'constants/orgConstants'; + +// Styled Components +const WorkspaceSection = styled.div` + padding: 8px 0; +`; + +const SectionHeader = styled.div` + padding: 8px 16px; + font-size: 12px; + font-weight: 500; + color: #8b8fa3; + text-transform: uppercase; + letter-spacing: 0.5px; +`; + +const SearchContainer = styled.div` + padding: 8px 12px; + border-bottom: 1px solid #f0f0f0; +`; + +const StyledSearchInput = styled(Input)` + .ant-input { + border: 1px solid #e1e3eb; + border-radius: 6px; + font-size: 13px; + + &:focus { + border-color: #4965f2; + box-shadow: 0 0 0 2px rgba(73, 101, 242, 0.1); + } + } +`; + +const WorkspaceList = styled.div` + max-height: 200px; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 4px; + } + + &::-webkit-scrollbar-track { + background: #f1f1f1; + } + + &::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 2px; + } + + &::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; + } +`; + +const WorkspaceItem = styled.div<{ isActive?: boolean }>` + display: flex; + align-items: center; + padding: 10px 16px; + cursor: pointer; + transition: background-color 0.2s; + background-color: ${props => props.isActive ? '#f0f5ff' : 'transparent'}; + + &:hover { + background-color: ${props => props.isActive ? '#f0f5ff' : '#f8f9fa'}; + } +`; + +const WorkspaceName = styled.div` + flex: 1; + font-size: 13px; + color: #222222; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const ActiveIcon = styled(CheckoutIcon)` + width: 16px; + height: 16px; + color: #4965f2; + margin-left: 8px; +`; + +const CreateWorkspaceItem = styled.div` + display: flex; + align-items: center; + padding: 12px 16px; + cursor: pointer; + transition: background-color 0.2s; + font-size: 13px; + color: #4965f2; + font-weight: 500; + + &:hover { + background-color: #f0f5ff; + color: #3651d4; + } + + svg { + width: 16px; + height: 16px; + margin-right: 10px; + color: #4965f2; + } + + &:hover svg { + color: #3651d4; + } +`; + +const EmptyState = styled.div` + padding: 20px 16px; + text-align: center; + color: #8b8fa3; + font-size: 13px; +`; + +const PaginationContainer = styled.div` + padding: 12px 16px; + border-top: 1px solid #f0f0f0; + display: flex; + justify-content: center; + + .ant-pagination { + margin: 0; + + .ant-pagination-item { + min-width: 24px; + height: 24px; + line-height: 22px; + font-size: 12px; + margin-right: 4px; + } + + .ant-pagination-prev, + .ant-pagination-next { + min-width: 24px; + height: 24px; + line-height: 22px; + margin-right: 4px; + } + + .ant-pagination-item-link { + font-size: 11px; + } + } +`; + +const LoadingSpinner = styled.div` + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + color: #8b8fa3; + font-size: 13px; +`; + +// Component Props +interface WorkspaceSectionProps { + user: User; + isDropdownOpen: boolean; + onClose: () => void; +} + +// Main Component +export default function WorkspaceSectionComponent({ + user, + isDropdownOpen, + onClose +}: WorkspaceSectionProps) { + const dispatch = useDispatch(); + const sysConfig = useSelector(selectSystemConfig); + + // Use our custom hook + const { + searchTerm, + currentPage, + totalCount, + isLoading, + displayWorkspaces, + handleSearchChange, + handlePageChange, + pageSize, + } = useWorkspaceManager({ isDropdownOpen }); + + // Early returns for better performance + if (!showSwitchOrg(user, sysConfig)) return null; + if (!displayWorkspaces?.length && !searchTerm.trim()) return null; + + // Event handlers + const handleOrgSwitch = (orgId: string) => { + if (user.currentOrgId !== orgId) { + dispatch(switchOrg(orgId)); + } + onClose(); + }; + + const handleCreateOrg = () => { + dispatch(createOrgAction(user.orgs)); + history.push(ORGANIZATION_SETTING); + onClose(); + }; + + return ( + + {trans("profile.switchOrg")} + + {/* Search Input - Only show if more than 3 workspaces */} + + handleSearchChange(e.target.value)} + prefix={} + size="small" + /> + + + {/* Workspace List */} + + {isLoading ? ( + + + Loading... + + ) : displayWorkspaces.length > 0 ? ( + displayWorkspaces.map((org: Org) => ( + handleOrgSwitch(org.id)} + > + {org.name} + {user.currentOrgId === org.id && } + + )) + ) : ( + + {searchTerm.trim() + ? "No workspaces found" + : "No workspaces available" + } + + )} + + + {/* Pagination - Only show when needed */} + {totalCount > pageSize && !isLoading && ( + + + `${range[0]}-${range[1]} of ${total}` + } + onChange={handlePageChange} + simple={totalCount > 100} // Simple mode for large datasets + /> + + )} + + {/* Create Workspace Button */} + + + {trans("profile.createOrg")} + + + ); +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx index 0dbbb9f2c..ac86f42fe 100644 --- a/client/packages/lowcoder/src/pages/common/profileDropdown.tsx +++ b/client/packages/lowcoder/src/pages/common/profileDropdown.tsx @@ -1,41 +1,23 @@ import { default as Dropdown } from "antd/es/dropdown"; -import { default as Menu, MenuItemProps } from "antd/es/menu"; -import { Input } from "antd"; import { Org, OrgRoleInfo } from "constants/orgConstants"; -import { ORGANIZATION_SETTING } from "constants/routesURL"; import { User } from "constants/userConstants"; -import { getWorkspaces, getCurrentOrg } from "redux/selectors/orgSelectors"; +import { getCurrentOrg } from "redux/selectors/orgSelectors"; import { - AddIcon, - CheckoutIcon, - CommonGrayLabel, CommonTextLabel, - CommonTextLabel2, - DropdownMenu, - DropDownSubMenu, EditIcon, - PackUpIcon, - SearchIcon, } from "lowcoder-design"; import ProfileSettingModal from "pages/setting/profile"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { createOrgAction, fetchWorkspacesAction, switchOrg } from "redux/reduxActions/orgActions"; import styled from "styled-components"; -import history from "util/history"; import ProfileImage from "pages/common/profileImage"; import { isProfileSettingModalVisible } from "redux/selectors/usersSelectors"; import { logoutAction, profileSettingModalVisible } from "redux/reduxActions/userActions"; import { trans } from "i18n"; -import { showSwitchOrg } from "@lowcoder-ee/pages/common/customerService"; import { checkIsMobile } from "util/commonUtils"; -import { selectSystemConfig } from "redux/selectors/configSelectors"; -import type { ItemType } from "antd/es/menu/interface"; -import { Pagination } from "antd"; -import { debounce } from "lodash"; -import UserApi from "api/userApi"; -const { Item } = Menu; +import WorkspaceSectionComponent from "./WorkspaceSection"; +// Keep existing styled components for profile and actions const ProfileDropdownContainer = styled.div` width: 280px; background: white; @@ -94,88 +76,6 @@ const ProfileRole = styled.div` max-width: fit-content; `; -const WorkspaceSection = styled.div` - padding: 8px 0; -`; - -const SectionHeader = styled.div` - padding: 8px 16px; - font-size: 12px; - font-weight: 500; - color: #8b8fa3; - text-transform: uppercase; - letter-spacing: 0.5px; -`; - -const SearchContainer = styled.div` - padding: 8px 12px; - border-bottom: 1px solid #f0f0f0; -`; - -const StyledSearchInput = styled(Input)` - .ant-input { - border: 1px solid #e1e3eb; - border-radius: 6px; - font-size: 13px; - - &:focus { - border-color: #4965f2; - box-shadow: 0 0 0 2px rgba(73, 101, 242, 0.1); - } - } -`; - -const WorkspaceList = styled.div` - max-height: 200px; - overflow-y: auto; - - &::-webkit-scrollbar { - width: 4px; - } - - &::-webkit-scrollbar-track { - background: #f1f1f1; - } - - &::-webkit-scrollbar-thumb { - background: #c1c1c1; - border-radius: 2px; - } - - &::-webkit-scrollbar-thumb:hover { - background: #a8a8a8; - } -`; - -const WorkspaceItem = styled.div<{ isActive?: boolean }>` - display: flex; - align-items: center; - padding: 10px 16px; - cursor: pointer; - transition: background-color 0.2s; - background-color: ${props => props.isActive ? '#f0f5ff' : 'transparent'}; - - &:hover { - background-color: ${props => props.isActive ? '#f0f5ff' : '#f8f9fa'}; - } -`; - -const WorkspaceName = styled.div` - flex: 1; - font-size: 13px; - color: #222222; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -`; - -const ActiveIcon = styled(CheckoutIcon)` - width: 16px; - height: 16px; - color: #4965f2; - margin-left: 8px; -`; - const ActionsSection = styled.div` border-top: 1px solid #f0f0f0; `; @@ -200,32 +100,6 @@ const ActionItem = styled.div` } `; -const CreateWorkspaceItem = styled(ActionItem)` - color: #4965f2; - font-weight: 500; - - - &:hover { - background-color: #f0f5ff; - color: #3651d4; - } - - svg { - color: #4965f2; - } - - &:hover svg { - color: #3651d4; - } -`; - -const EmptyState = styled.div` - padding: 20px 16px; - text-align: center; - color: #8b8fa3; - font-size: 13px; -`; - const StyledDropdown = styled(Dropdown)` display: flex; flex-direction: column; @@ -233,46 +107,7 @@ const StyledDropdown = styled(Dropdown)` align-items: end; `; - -const PaginationContainer = styled.div` - padding: 12px 16px; - border-top: 1px solid #f0f0f0; - display: flex; - justify-content: center; - - .ant-pagination { - margin: 0; - - .ant-pagination-item { - min-width: 24px; - height: 24px; - line-height: 22px; - font-size: 12px; - margin-right: 4px; - } - - .ant-pagination-prev, - .ant-pagination-next { - min-width: 24px; - height: 24px; - line-height: 22px; - margin-right: 4px; - } - - .ant-pagination-item-link { - font-size: 11px; - } - } -`; -const LoadingSpinner = styled.div` - display: flex; - align-items: center; - justify-content: center; - padding: 16px; - color: #8b8fa3; - font-size: 13px; -`; - +// Component Props type DropDownProps = { onClick?: (text: string) => void; user: User; @@ -280,125 +115,18 @@ type DropDownProps = { fontSize?: number; }; +// Simplified Main Component export default function ProfileDropdown(props: DropDownProps) { - const { avatarUrl, username, orgs, currentOrgId } = props.user; + const { avatarUrl, username, currentOrgId } = props.user; const currentOrgRoleId = props.user.orgRoleMap.get(currentOrgId); - const workspaces = useSelector(getWorkspaces); const currentOrg = useSelector(getCurrentOrg); const settingModalVisible = useSelector(isProfileSettingModalVisible); - const sysConfig = useSelector(selectSystemConfig); const dispatch = useDispatch(); - // Local state for pagination and search - const [searchTerm, setSearchTerm] = useState(""); - const [dropdownVisible, setDropdownVisible] = useState(false); - const [currentPageWorkspaces, setCurrentPageWorkspaces] = useState([]); - const [currentPage, setCurrentPage] = useState(1); - const [totalCount, setTotalCount] = useState(0); - const [isLoading, setIsLoading] = useState(false); - const [isSearching, setIsSearching] = useState(false); - - const pageSize = 10; - - // Determine which workspaces to show - const displayWorkspaces = useMemo(() => { - if (searchTerm.trim()) { - return currentPageWorkspaces; // Search results - } - if (currentPage === 1) { - return workspaces.items; // First page from Redux - } - return currentPageWorkspaces; // Other pages from API - }, [searchTerm, currentPage, workspaces.items, currentPageWorkspaces]); - - // Update total count based on context - useEffect(() => { - if (searchTerm.trim()) { - // Keep search result count - return; - } - if (currentPage === 1) { - setTotalCount(workspaces.totalCount); - } - }, [searchTerm, currentPage, workspaces.totalCount]); - - // Fetch workspaces for specific page - const fetchWorkspacesPage = async (page: number, search?: string) => { - setIsLoading(true); - try { - const response = await UserApi.getMyOrgs(page, pageSize, search); - if (response.data.success) { - const apiData = response.data.data; - const transformedItems = apiData.data.map(item => ({ - id: item.orgId, - name: item.orgName, - })); - - setCurrentPageWorkspaces(transformedItems as Org[]); - setTotalCount(apiData.total); - } - } catch (error) { - console.error('Error fetching workspaces:', error); - setCurrentPageWorkspaces([]); - } finally { - setIsLoading(false); - } -}; - - // Handle page change - const handlePageChange = (page: number) => { - setCurrentPage(page); - if (page === 1 && !searchTerm.trim()) { - // Use Redux data for first page when not searching - setCurrentPageWorkspaces([]); - } else { - // Fetch from API for other pages or when searching - fetchWorkspacesPage(page, searchTerm.trim() || undefined); - } - }; - - - // Debounced search function - const debouncedSearch = useMemo( - () => debounce(async (term: string) => { - if (!term.trim()) { - setCurrentPage(1); - setCurrentPageWorkspaces([]); - setTotalCount(workspaces.totalCount); - setIsSearching(false); - return; - } - - setIsSearching(true); - setCurrentPage(1); - await fetchWorkspacesPage(1, term); - setIsSearching(false); - }, 300), - [workspaces.totalCount] - ); - - - - // Reset state when dropdown closes - useEffect(() => { - if (!dropdownVisible) { - setCurrentPageWorkspaces([]); - setCurrentPage(1); - setSearchTerm(""); - setTotalCount(workspaces.totalCount); - setIsSearching(false); - } - }, [dropdownVisible, workspaces.totalCount]); - - - - const filteredOrgs = useMemo(() => { - if (!searchTerm.trim()) return workspaces.items; - return workspaces.items.filter(org => - org.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); - }, [workspaces.items, searchTerm]); + // Simple state - only what we need + const [dropdownVisible, setDropdownVisible] = useState(false); + // Event handlers const handleProfileClick = () => { if (checkIsMobile(window.innerWidth)) { setDropdownVisible(false); @@ -419,26 +147,11 @@ export default function ProfileDropdown(props: DropDownProps) { setDropdownVisible(false); }; - const handleOrgSwitch = (orgId: string) => { - if (currentOrgId !== orgId) { - dispatch(switchOrg(orgId)); - } - setDropdownVisible(false); - }; - - const handleCreateOrg = () => { - dispatch(createOrgAction(orgs)); - history.push(ORGANIZATION_SETTING); + const handleDropdownClose = () => { setDropdownVisible(false); }; - // Handle search input change - const handleSearchChange = (e: React.ChangeEvent) => { - const value = e.target.value; - setSearchTerm(value); - debouncedSearch(value); -}; - + // Dropdown content const dropdownContent = ( e.stopPropagation()}> {/* Profile Section */} @@ -458,81 +171,12 @@ export default function ProfileDropdown(props: DropDownProps) { )} - {/* Workspaces Section */} - {workspaces.items && - workspaces.items.length > 0 && - showSwitchOrg(props.user, sysConfig) && ( - - {trans("profile.switchOrg")} - - {workspaces.items.length > 3 && ( - - } - size="small" - /> - - )} - - {/* Workspaces List */} - - {isSearching || isLoading ? ( - - - {isSearching ? "Searching..." : "Loading..."} - - ) : displayWorkspaces.length > 0 ? ( - displayWorkspaces.map((org: Org) => ( - handleOrgSwitch(org.id)} - > - {org.name} - {currentOrgId === org.id && } - - )) - ) : ( - - {searchTerm.trim() - ? "No workspaces found" - : "No workspaces available"} - - )} - - - {/* Pagination */} - {totalCount > pageSize && !isSearching && !isLoading && ( - - - `${range[0]}-${range[1]} of ${total}` - } - onChange={handlePageChange} - simple={totalCount > 100} - /> - - )} - - - {trans("profile.createOrg")} - - - )} + {/* Workspaces Section - Now extracted and clean! */} + {/* Actions Section */} @@ -565,4 +209,4 @@ export default function ProfileDropdown(props: DropDownProps) { {settingModalVisible && } ); -} +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/util/useWorkspaceManager.ts b/client/packages/lowcoder/src/util/useWorkspaceManager.ts new file mode 100644 index 000000000..c0aa2d03a --- /dev/null +++ b/client/packages/lowcoder/src/util/useWorkspaceManager.ts @@ -0,0 +1,183 @@ +import { useReducer, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { debounce } from 'lodash'; +import { Org } from 'constants/orgConstants'; +import { getWorkspaces } from 'redux/selectors/orgSelectors'; +import UserApi from 'api/userApi'; + +// State interface for the workspace manager +interface WorkspaceState { + searchTerm: string; + currentPage: number; + currentPageWorkspaces: Org[]; + totalCount: number; + isLoading: boolean; +} + +// Action types for the reducer +type WorkspaceAction = + | { type: 'SET_SEARCH_TERM'; payload: string } + | { type: 'SET_PAGE'; payload: number } + | { type: 'SET_LOADING'; payload: boolean } + | { type: 'SET_WORKSPACES'; payload: { workspaces: Org[]; total: number } } + | { type: 'RESET'; payload: { totalCount: number } }; + +// Initial state +const initialState: WorkspaceState = { + searchTerm: '', + currentPage: 1, + currentPageWorkspaces: [], + totalCount: 0, + isLoading: false, +}; + +// Reducer function - handles state transitions +function workspaceReducer(state: WorkspaceState, action: WorkspaceAction): WorkspaceState { + switch (action.type) { + case 'SET_SEARCH_TERM': + return { + ...state, + searchTerm: action.payload, + currentPage: 1 // Reset to page 1 when searching + }; + case 'SET_PAGE': + return { ...state, currentPage: action.payload }; + case 'SET_LOADING': + return { ...state, isLoading: action.payload }; + case 'SET_WORKSPACES': + return { + ...state, + currentPageWorkspaces: action.payload.workspaces, + totalCount: action.payload.total, + isLoading: false, + }; + case 'RESET': + return { + ...initialState, + totalCount: action.payload.totalCount, + }; + default: + return state; + } +} + +// Hook interface +interface UseWorkspaceManagerOptions { + isDropdownOpen: boolean; + pageSize?: number; +} + +// Main hook +export function useWorkspaceManager({ + isDropdownOpen, + pageSize = 10 +}: UseWorkspaceManagerOptions) { + // Get workspaces from Redux + const workspaces = useSelector(getWorkspaces); + + // Initialize reducer with Redux total count + const [state, dispatch] = useReducer(workspaceReducer, { + ...initialState, + totalCount: workspaces.totalCount, + }); + + // Reset state when dropdown closes + useEffect(() => { + if (!isDropdownOpen) { + dispatch({ type: 'RESET', payload: { totalCount: workspaces.totalCount } }); + } + }, [isDropdownOpen, workspaces.totalCount]); + + // API call to fetch workspaces + const fetchWorkspacesPage = async (page: number, search?: string) => { + dispatch({ type: 'SET_LOADING', payload: true }); + + try { + const response = await UserApi.getMyOrgs(page, pageSize, search); + if (response.data.success) { + const apiData = response.data.data; + const transformedItems = apiData.data.map(item => ({ + id: item.orgId, + name: item.orgName, + })); + + dispatch({ + type: 'SET_WORKSPACES', + payload: { + workspaces: transformedItems as Org[], + total: apiData.total, + }, + }); + } + } catch (error) { + console.error('Error fetching workspaces:', error); + dispatch({ type: 'SET_WORKSPACES', payload: { workspaces: [], total: 0 } }); + } + }; + + // Debounced search function + const debouncedSearch = debounce(async (term: string) => { + if (!term.trim()) { + // Clear search - reset to Redux data + dispatch({ + type: 'SET_WORKSPACES', + payload: { workspaces: [], total: workspaces.totalCount } + }); + return; + } + + // Perform search + await fetchWorkspacesPage(1, term); + }, 300); + + // Handle search input change + const handleSearchChange = (value: string) => { + dispatch({ type: 'SET_SEARCH_TERM', payload: value }); + debouncedSearch(value); + }; + + // Handle page change + const handlePageChange = (page: number) => { + dispatch({ type: 'SET_PAGE', payload: page }); + + if (page === 1 && !state.searchTerm.trim()) { + // Page 1 + no search = use Redux data + dispatch({ + type: 'SET_WORKSPACES', + payload: { workspaces: [], total: workspaces.totalCount } + }); + } else { + // Other pages or search = fetch from API + fetchWorkspacesPage(page, state.searchTerm.trim() || undefined); + } + }; + + // Determine which workspaces to display + const displayWorkspaces = (() => { + if (state.searchTerm.trim() || state.currentPage > 1) { + return state.currentPageWorkspaces; // API results + } + return workspaces.items; // Redux data for page 1 + })(); + + // Determine current total count + const currentTotalCount = state.searchTerm.trim() + ? state.totalCount + : workspaces.totalCount; + + return { + // State + searchTerm: state.searchTerm, + currentPage: state.currentPage, + isLoading: state.isLoading, + displayWorkspaces, + totalCount: currentTotalCount, + + // Actions + handleSearchChange, + handlePageChange, + + // Config + pageSize, + }; +} \ No newline at end of file From 0d596100a90c96b0d1b5a18e5ce2b39b058615bf Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 18 Jun 2025 19:09:43 +0500 Subject: [PATCH 15/26] fix debouncing --- .../lowcoder/src/util/useWorkspaceManager.ts | 84 +++++++++++-------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/client/packages/lowcoder/src/util/useWorkspaceManager.ts b/client/packages/lowcoder/src/util/useWorkspaceManager.ts index c0aa2d03a..eb685f944 100644 --- a/client/packages/lowcoder/src/util/useWorkspaceManager.ts +++ b/client/packages/lowcoder/src/util/useWorkspaceManager.ts @@ -1,4 +1,4 @@ -import { useReducer, useEffect } from 'react'; +import { useReducer, useEffect, useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { debounce } from 'lodash'; import { Org } from 'constants/orgConstants'; @@ -88,47 +88,59 @@ export function useWorkspaceManager({ } }, [isDropdownOpen, workspaces.totalCount]); - // API call to fetch workspaces - const fetchWorkspacesPage = async (page: number, search?: string) => { - dispatch({ type: 'SET_LOADING', payload: true }); - - try { - const response = await UserApi.getMyOrgs(page, pageSize, search); - if (response.data.success) { - const apiData = response.data.data; - const transformedItems = apiData.data.map(item => ({ - id: item.orgId, - name: item.orgName, - })); - + // API call to fetch workspaces (memoized for stable reference) + const fetchWorkspacesPage = useCallback( + async (page: number, search?: string) => { + dispatch({ type: 'SET_LOADING', payload: true }); + + try { + const response = await UserApi.getMyOrgs(page, pageSize, search); + if (response.data.success) { + const apiData = response.data.data; + const transformedItems = apiData.data.map(item => ({ + id: item.orgId, + name: item.orgName, + })); + + dispatch({ + type: 'SET_WORKSPACES', + payload: { + workspaces: transformedItems as Org[], + total: apiData.total, + }, + }); + } + } catch (error) { + console.error('Error fetching workspaces:', error); + dispatch({ type: 'SET_WORKSPACES', payload: { workspaces: [], total: 0 } }); + } + }, + [dispatch, pageSize] + ); + + // Debounced search function (memoized to keep a single instance across renders) + const debouncedSearch = useMemo(() => + debounce(async (term: string) => { + if (!term.trim()) { + // Clear search - reset to Redux data dispatch({ type: 'SET_WORKSPACES', - payload: { - workspaces: transformedItems as Org[], - total: apiData.total, - }, + payload: { workspaces: [], total: workspaces.totalCount }, }); + return; } - } catch (error) { - console.error('Error fetching workspaces:', error); - dispatch({ type: 'SET_WORKSPACES', payload: { workspaces: [], total: 0 } }); - } - }; - // Debounced search function - const debouncedSearch = debounce(async (term: string) => { - if (!term.trim()) { - // Clear search - reset to Redux data - dispatch({ - type: 'SET_WORKSPACES', - payload: { workspaces: [], total: workspaces.totalCount } - }); - return; - } + // Perform search + await fetchWorkspacesPage(1, term); + }, 300) + , [dispatch, fetchWorkspacesPage, workspaces.totalCount]); - // Perform search - await fetchWorkspacesPage(1, term); - }, 300); + // Cleanup debounce on unmount + useEffect(() => { + return () => { + debouncedSearch.cancel(); + }; + }, [debouncedSearch]); // Handle search input change const handleSearchChange = (value: string) => { From c7edbd1902fa7862406403ff6f3c79ade43e4122 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 18 Jun 2025 20:14:15 +0500 Subject: [PATCH 16/26] fix loading when search --- client/packages/lowcoder/src/util/useWorkspaceManager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/util/useWorkspaceManager.ts b/client/packages/lowcoder/src/util/useWorkspaceManager.ts index eb685f944..d8f227669 100644 --- a/client/packages/lowcoder/src/util/useWorkspaceManager.ts +++ b/client/packages/lowcoder/src/util/useWorkspaceManager.ts @@ -38,7 +38,8 @@ function workspaceReducer(state: WorkspaceState, action: WorkspaceAction): Works return { ...state, searchTerm: action.payload, - currentPage: 1 // Reset to page 1 when searching + currentPage: 1 , + isLoading: Boolean(action.payload.trim()) }; case 'SET_PAGE': return { ...state, currentPage: action.payload }; From 238698da5a64e91ad5b9a23a07ef3420b3dafa6a Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 18 Jun 2025 21:21:07 +0500 Subject: [PATCH 17/26] fix shrinking issues when page 1 to page 2 --- client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx b/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx index 8a57715f5..78e32941a 100644 --- a/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx +++ b/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx @@ -205,7 +205,6 @@ export default function WorkspaceSectionComponent({ // Early returns for better performance if (!showSwitchOrg(user, sysConfig)) return null; - if (!displayWorkspaces?.length && !searchTerm.trim()) return null; // Event handlers const handleOrgSwitch = (orgId: string) => { From 061191095dd92d400ee3b86ebc137fd33f4e64bb Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 18 Jun 2025 22:09:18 +0500 Subject: [PATCH 18/26] add antD loader in UI --- .../src/pages/common/WorkspaceSection.tsx | 21 ++++++------------- .../lowcoder/src/util/useWorkspaceManager.ts | 2 +- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx b/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx index 78e32941a..e5a2a0636 100644 --- a/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx +++ b/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; -import { Input, Pagination } from 'antd'; +import { Input, Pagination, Spin } from 'antd'; import { User } from 'constants/userConstants'; import { switchOrg, createOrgAction } from 'redux/reduxActions/orgActions'; import { selectSystemConfig } from 'redux/selectors/configSelectors'; @@ -11,7 +11,6 @@ import { trans } from 'i18n'; import { AddIcon, CheckoutIcon, - PackUpIcon, SearchIcon, } from 'lowcoder-design'; import { ORGANIZATION_SETTING } from 'constants/routesURL'; @@ -166,13 +165,11 @@ const PaginationContainer = styled.div` } `; -const LoadingSpinner = styled.div` +const LoadingContainer = styled.div` display: flex; align-items: center; justify-content: center; - padding: 16px; - color: #8b8fa3; - font-size: 13px; + padding: 24px 16px; `; // Component Props @@ -238,15 +235,9 @@ export default function WorkspaceSectionComponent({ {/* Workspace List */} {isLoading ? ( - - - Loading... - + + + ) : displayWorkspaces.length > 0 ? ( displayWorkspaces.map((org: Org) => ( Date: Thu, 19 Jun 2025 11:28:07 +0500 Subject: [PATCH 19/26] fix delete sync --- client/packages/lowcoder/src/redux/sagas/orgSagas.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/packages/lowcoder/src/redux/sagas/orgSagas.ts b/client/packages/lowcoder/src/redux/sagas/orgSagas.ts index 31abe2384..2aca5ba3d 100644 --- a/client/packages/lowcoder/src/redux/sagas/orgSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/orgSagas.ts @@ -25,6 +25,7 @@ import { fetchLastMonthAPIUsageActionSuccess, UpdateUserGroupRolePayload, UpdateUserOrgRolePayload, + fetchWorkspacesAction, } from "redux/reduxActions/orgActions"; import { getUser } from "redux/selectors/usersSelectors"; import { validateResponse } from "api/apiUtils"; @@ -270,6 +271,8 @@ export function* deleteOrgSaga(action: ReduxAction<{ orgId: string }>) { orgId: action.payload.orgId, }, }); + // Refetch workspaces to update the profile dropdown + yield put(fetchWorkspacesAction(1, 10)); } } catch (error: any) { messageInstance.error(error.message); From 96acd6abf72b439ddfa5e69d0961f7efe47fc08f Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 19 Jun 2025 11:36:31 +0500 Subject: [PATCH 20/26] fix sync after edit --- client/packages/lowcoder/src/redux/sagas/orgSagas.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/packages/lowcoder/src/redux/sagas/orgSagas.ts b/client/packages/lowcoder/src/redux/sagas/orgSagas.ts index 2aca5ba3d..54e4eee1e 100644 --- a/client/packages/lowcoder/src/redux/sagas/orgSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/orgSagas.ts @@ -286,6 +286,8 @@ export function* updateOrgSaga(action: ReduxAction) { const isValidResponse: boolean = validateResponse(response); if (isValidResponse) { yield put(updateOrgSuccess(action.payload)); + // Refetch workspaces to update the profile dropdown + yield put(fetchWorkspacesAction(1, 10)); } } catch (error: any) { messageInstance.error(error.message); From db11f0ef9ced98cb7a0422de00705457e777a985 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 19 Jun 2025 20:59:40 +0500 Subject: [PATCH 21/26] add pagination/filtering for the Workspaces page --- .../src/pages/common/WorkspaceSection.tsx | 2 +- .../pages/setting/organization/orgList.tsx | 332 +++++++++++------- .../lowcoder/src/util/useWorkspaceManager.ts | 8 - 3 files changed, 205 insertions(+), 137 deletions(-) diff --git a/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx b/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx index e5a2a0636..8358ef461 100644 --- a/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx +++ b/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx @@ -198,7 +198,7 @@ export default function WorkspaceSectionComponent({ handleSearchChange, handlePageChange, pageSize, - } = useWorkspaceManager({ isDropdownOpen }); + } = useWorkspaceManager({}); // Early returns for better performance if (!showSwitchOrg(user, sysConfig)) return null; diff --git a/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx b/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx index ba99bc7df..df2a4804b 100644 --- a/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx +++ b/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx @@ -1,5 +1,5 @@ import { ADMIN_ROLE, SUPER_ADMIN_ROLE } from "constants/orgConstants"; -import { AddIcon, CustomModal, DangerIcon, EditPopover } from "lowcoder-design"; +import { AddIcon, CustomModal, DangerIcon, EditPopover, SearchIcon } from "lowcoder-design"; import { useDispatch, useSelector } from "react-redux"; import { createOrgAction, deleteOrgAction } from "redux/reduxActions/orgActions"; import styled from "styled-components"; @@ -15,13 +15,16 @@ import { Table } from "components/Table"; import history from "util/history"; import { StyledOrgLogo } from "./styledComponents"; import { Level1SettingPageContentWithList, Level1SettingPageTitleWithBtn } from "../styled"; -import { timestampToHumanReadable } from "util/dateTimeUtils"; import { isSaasMode } from "util/envUtils"; import { selectSystemConfig } from "redux/selectors/configSelectors"; import { default as Form } from "antd/es/form"; import { default as Input } from "antd/es/input"; +import { Pagination, Spin } from "antd"; import { getUser } from "redux/selectors/usersSelectors"; import { getOrgCreateStatus } from "redux/selectors/orgSelectors"; +import { useWorkspaceManager } from "util/useWorkspaceManager"; +import { Org } from "constants/orgConstants"; +import { useState } from "react"; const OrgName = styled.div` display: flex; @@ -53,6 +56,36 @@ const TableStyled = styled(Table)` } `; +const SearchContainer = styled.div` + margin-bottom: 16px; + max-width: 320px; +`; + +const StyledSearchInput = styled(Input)` + .ant-input { + border: 1px solid #e1e3eb; + border-radius: 6px; + font-size: 13px; + + &:focus { + border-color: #4965f2; + box-shadow: 0 0 0 2px rgba(73, 101, 242, 0.1); + } + } +`; + +const PaginationContainer = styled.div` + margin-top: 16px; + display: flex; + justify-content: flex-end; +`; + +const LoadingContainer = styled.div` + display: flex; + justify-content: center; + padding: 40px 0; +`; + const Content = styled.div` &, .ant-form-item-label, @@ -120,29 +153,43 @@ const Tip = styled.div` type DataItemInfo = { id: string; del: boolean; - createTime: string; orgName: string; logoUrl: string; }; function OrganizationSetting() { const user = useSelector(getUser); - const orgs = user.orgs; - const adminOrgs = orgs.filter((org) => { - const role = user.orgRoleMap.get(org.id); - return role === ADMIN_ROLE || role === SUPER_ADMIN_ROLE; - }); const orgCreateStatus = useSelector(getOrgCreateStatus); const dispatch = useDispatch(); const sysConfig = useSelector(selectSystemConfig); const [form] = Form.useForm(); - const dataSource = adminOrgs.map((org) => ({ + // Use the workspace manager hook for search and pagination + const { + searchTerm, + currentPage, + totalCount, + isLoading, + displayWorkspaces, + handleSearchChange, + handlePageChange, + pageSize, + } = useWorkspaceManager({ + pageSize: 10 + }); + + + // Filter to only show orgs where user has admin permissions + const adminOrgs = displayWorkspaces.filter((org: Org) => { + const role = user.orgRoleMap.get(org.id); + return role === ADMIN_ROLE || role === SUPER_ADMIN_ROLE; + }); + + const dataSource = adminOrgs.map((org: Org) => ({ id: org.id, del: adminOrgs.length > 1, - createTime: org.createTime, orgName: org.name, - logoUrl: org.logoUrl, + logoUrl: org.logoUrl || "", })); return ( @@ -154,131 +201,160 @@ function OrganizationSetting() { loading={orgCreateStatus === "requesting"} buttonType={"primary"} icon={} - onClick={() => dispatch(createOrgAction(orgs))} + onClick={() => dispatch(createOrgAction(user.orgs))} > {trans("orgSettings.createOrg")} )} + + {/* Search Input */} + + handleSearchChange(e.target.value)} + prefix={} + size="middle" + /> + +
- ({ - onClick: () => history.push(buildOrgId((record as DataItemInfo).id)), - })} - columns={[ - { - title: trans("orgSettings.orgName"), - dataIndex: "orgName", - ellipsis: true, - render: (_, record: any) => { - return ( - - - {record.orgName} - - ); - }, - }, - { - title: trans("memberSettings.createTime"), - dataIndex: "createTime", - ellipsis: true, - render: (value) => ( - {timestampToHumanReadable(value)} - ), - }, - { title: " ", dataIndex: "operation", width: "208px" }, - ]} - dataSource={dataSource.map((item, i) => ({ - ...item, - key: i, - operation: ( - - history.push(buildOrgId(item.id))} - > - {trans("edit")} - - {item.del && ( - { - CustomModal.confirm({ - width: "384px", - title: trans("orgSettings.deleteModalTitle"), - bodyStyle: { marginTop: 0 }, - content: ( - - - - - {transToNode("orgSettings.deleteModalContent", { - permanentlyDelete: ( - {trans("orgSettings.permanentlyDelete")} - ), - notRestored: {trans("orgSettings.notRestored")}, - })} - - -
- - {item.orgName} - - ), - })} - rules={[ - { - required: true, - message: trans("orgSettings.deleteModalTip"), - }, - ]} - > - - -
-
- ), - onConfirm: () => { - form.submit(); - return form.validateFields().then(() => { - const name = form.getFieldValue("name"); - if (name === item.orgName) { - dispatch(deleteOrgAction(item.id)); + {isLoading ? ( + + + + ) : ( + <> + ({ + onClick: () => history.push(buildOrgId((record as DataItemInfo).id)), + })} + columns={[ + { + title: trans("orgSettings.orgName"), + dataIndex: "orgName", + ellipsis: true, + render: (_, record: any) => { + return ( + + + {record.orgName} + + ); + }, + }, + { title: " ", dataIndex: "operation", width: "208px" }, + ]} + dataSource={dataSource.map((item, i) => ({ + ...item, + key: i, + operation: ( + + history.push(buildOrgId(item.id))} + > + {trans("edit")} + + {item.del && ( + { + CustomModal.confirm({ + width: "384px", + title: trans("orgSettings.deleteModalTitle"), + bodyStyle: { marginTop: 0 }, + content: ( + + + + + {transToNode("orgSettings.deleteModalContent", { + permanentlyDelete: ( + {trans("orgSettings.permanentlyDelete")} + ), + notRestored: {trans("orgSettings.notRestored")}, + })} + + +
+ + {item.orgName} + + ), + })} + rules={[ + { + required: true, + message: trans("orgSettings.deleteModalTip"), + }, + ]} + > + + +
+
+ ), + onConfirm: () => { + form.submit(); + return form.validateFields().then(() => { + const name = form.getFieldValue("name"); + if (name === item.orgName) { + dispatch(deleteOrgAction(item.id)); + form.resetFields(); + } else { + form.setFields([ + { + name: "name", + errors: [trans("orgSettings.deleteModalErr")], + }, + ]); + throw new Error(); + } + }); + }, + onCancel: () => { form.resetFields(); - } else { - form.setFields([ - { - name: "name", - errors: [trans("orgSettings.deleteModalErr")], - }, - ]); - throw new Error(); - } + }, + confirmBtnType: "delete", + okText: trans("orgSettings.deleteModalBtn"), }); - }, - onCancel: () => { - form.resetFields(); - }, - confirmBtnType: "delete", - okText: trans("orgSettings.deleteModalBtn"), - }); - }} - > - -
- )} -
- ), - }))} - /> + }} + > + +
+ )} +
+ ), + }))} + /> + + {/* Pagination */} + {totalCount > pageSize && ( + + + `${range[0]}-${range[1]} of ${total} organizations` + } + onChange={handlePageChange} + /> + + )} + + )}
); diff --git a/client/packages/lowcoder/src/util/useWorkspaceManager.ts b/client/packages/lowcoder/src/util/useWorkspaceManager.ts index 2086544ef..501fe7758 100644 --- a/client/packages/lowcoder/src/util/useWorkspaceManager.ts +++ b/client/packages/lowcoder/src/util/useWorkspaceManager.ts @@ -64,13 +64,11 @@ function workspaceReducer(state: WorkspaceState, action: WorkspaceAction): Works // Hook interface interface UseWorkspaceManagerOptions { - isDropdownOpen: boolean; pageSize?: number; } // Main hook export function useWorkspaceManager({ - isDropdownOpen, pageSize = 10 }: UseWorkspaceManagerOptions) { // Get workspaces from Redux @@ -82,12 +80,6 @@ export function useWorkspaceManager({ totalCount: workspaces.totalCount, }); - // Reset state when dropdown closes - useEffect(() => { - if (!isDropdownOpen) { - dispatch({ type: 'RESET', payload: { totalCount: workspaces.totalCount } }); - } - }, [isDropdownOpen, workspaces.totalCount]); // API call to fetch workspaces (memoized for stable reference) const fetchWorkspacesPage = useCallback( From 2f291f7345a3b468a06fff2fbb8740b2d5a205c3 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 19 Jun 2025 21:44:54 +0500 Subject: [PATCH 22/26] add selector for the current org --- .../lowcoder/src/redux/selectors/orgSelectors.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/client/packages/lowcoder/src/redux/selectors/orgSelectors.ts b/client/packages/lowcoder/src/redux/selectors/orgSelectors.ts index d60cbbad9..322f414f7 100644 --- a/client/packages/lowcoder/src/redux/selectors/orgSelectors.ts +++ b/client/packages/lowcoder/src/redux/selectors/orgSelectors.ts @@ -1,6 +1,7 @@ import { Org } from "@lowcoder-ee/constants/orgConstants"; import { getUser } from "./usersSelectors"; import { AppState } from "redux/reducers"; +import { getHomeOrg } from "./applicationSelector"; export const getOrgUsers = (state: AppState) => { return state.ui.org.orgUsers; @@ -33,8 +34,14 @@ export const getOrgLastMonthApiUsage = (state: AppState) => { // Add to usersSelectors.ts export const getWorkspaces = (state: AppState) => state.ui.users.workspaces; -export const getCurrentOrg = (state: AppState): Org | undefined => { - const user = getUser(state); - const workspaces = getWorkspaces(state); - return workspaces.items.find(org => org.id === user.currentOrgId); +export const getCurrentOrg = (state: AppState): Pick | undefined => { + const homeOrg = getHomeOrg(state); + if (!homeOrg) { + return undefined; + } + + return { + id: homeOrg.id, + name: homeOrg.name, + }; }; \ No newline at end of file From b3abc6139f995734e92db50ac7f51945da443d3e Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 19 Jun 2025 22:09:31 +0500 Subject: [PATCH 23/26] add active indicator in the workspaces page --- .../src/pages/setting/organization/orgList.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx b/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx index df2a4804b..53b0d4ddd 100644 --- a/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx +++ b/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx @@ -1,5 +1,5 @@ import { ADMIN_ROLE, SUPER_ADMIN_ROLE } from "constants/orgConstants"; -import { AddIcon, CustomModal, DangerIcon, EditPopover, SearchIcon } from "lowcoder-design"; +import { AddIcon, CustomModal, DangerIcon, EditPopover, SearchIcon, CheckoutIcon } from "lowcoder-design"; import { useDispatch, useSelector } from "react-redux"; import { createOrgAction, deleteOrgAction } from "redux/reduxActions/orgActions"; import styled from "styled-components"; @@ -50,6 +50,14 @@ const OrgName = styled.div` } `; +// Icon to indicate the currently active organization +const ActiveOrgIcon = styled(CheckoutIcon)` + width: 16px; + height: 16px; + color: #4965f2; + margin-left: 6px; +`; + const TableStyled = styled(Table)` .ant-table-tbody > tr > td { padding: 11px 12px; @@ -239,10 +247,12 @@ function OrganizationSetting() { dataIndex: "orgName", ellipsis: true, render: (_, record: any) => { + const isActiveOrg = record.id === user.currentOrgId; return ( {record.orgName} + {isActiveOrg && } ); }, From 23fcbf94a76bdc53e8d7f67ed79620d82008a0fc Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 19 Jun 2025 22:26:24 +0500 Subject: [PATCH 24/26] add the ability to switch workspaces from workspaces page --- .../src/pages/setting/organization/orgList.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx b/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx index 53b0d4ddd..cd6b1dc3a 100644 --- a/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx +++ b/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx @@ -1,7 +1,7 @@ import { ADMIN_ROLE, SUPER_ADMIN_ROLE } from "constants/orgConstants"; import { AddIcon, CustomModal, DangerIcon, EditPopover, SearchIcon, CheckoutIcon } from "lowcoder-design"; import { useDispatch, useSelector } from "react-redux"; -import { createOrgAction, deleteOrgAction } from "redux/reduxActions/orgActions"; +import { createOrgAction, deleteOrgAction, switchOrg } from "redux/reduxActions/orgActions"; import styled from "styled-components"; import { trans, transToNode } from "i18n"; import { buildOrgId } from "constants/routesURL"; @@ -58,6 +58,12 @@ const ActiveOrgIcon = styled(CheckoutIcon)` margin-left: 6px; `; +// Button to switch to this organization +const SwitchBtn = styled(EditBtn)` + min-width: 64px; + margin-right: 8px; +`; + const TableStyled = styled(Table)` .ant-table-tbody > tr > td { padding: 11px 12px; @@ -264,6 +270,15 @@ function OrganizationSetting() { key: i, operation: ( + {item.id !== user.currentOrgId && ( + dispatch(switchOrg(item.id))} + > + {trans("profile.switchOrg")} + + )} Date: Thu, 19 Jun 2025 22:30:28 +0500 Subject: [PATCH 25/26] disable row click on switch --- .../lowcoder/src/pages/setting/organization/orgList.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx b/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx index cd6b1dc3a..5a71b0675 100644 --- a/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx +++ b/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx @@ -274,7 +274,10 @@ function OrganizationSetting() { dispatch(switchOrg(item.id))} + onClick={(e) => { + e.stopPropagation(); + dispatch(switchOrg(item.id)); + }} > {trans("profile.switchOrg")} From 73a64b405d6dcf0bcff185732dacdc00783bcdfc Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 19 Jun 2025 22:58:34 +0500 Subject: [PATCH 26/26] fix switch org button --- client/packages/lowcoder/src/i18n/locales/en.ts | 1 + .../lowcoder/src/pages/setting/organization/orgList.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index de24d5b64..58c642da3 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -3639,6 +3639,7 @@ export const en = { "profile": { "orgSettings": "Workspace Settings", "switchOrg": "Switch Workspace", + "switchWorkspace": "Switch", "joinedOrg": "My Workspaces", "createOrg": "Create Workspace", "logout": "Log Out", diff --git a/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx b/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx index 5a71b0675..2f4dc160e 100644 --- a/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx +++ b/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx @@ -25,6 +25,7 @@ import { getOrgCreateStatus } from "redux/selectors/orgSelectors"; import { useWorkspaceManager } from "util/useWorkspaceManager"; import { Org } from "constants/orgConstants"; import { useState } from "react"; +import { SwapOutlined } from "@ant-design/icons"; const OrgName = styled.div` display: flex; @@ -60,7 +61,7 @@ const ActiveOrgIcon = styled(CheckoutIcon)` // Button to switch to this organization const SwitchBtn = styled(EditBtn)` - min-width: 64px; + min-width: auto; margin-right: 8px; `; @@ -274,12 +275,13 @@ function OrganizationSetting() { } onClick={(e) => { e.stopPropagation(); dispatch(switchOrg(item.id)); }} > - {trans("profile.switchOrg")} + {trans("profile.switchWorkspace")} )}