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

Skip to content

Commit 524bc75

Browse files
Merge pull request #1787 from iamfaran/feat/myorg-endpoint
[Feat]: Improve Profile Dropdown, and Workspaces Page using "myorg" endpoint
2 parents faffd94 + 24e04c0 commit 524bc75

File tree

12 files changed

+990
-329
lines changed

12 files changed

+990
-329
lines changed

‎client/packages/lowcoder/src/api/userApi.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Api from "api/api";
22
import { AxiosPromise } from "axios";
3-
import { OrgAndRole } from "constants/orgConstants";
3+
import { Org, OrgAndRole } from "constants/orgConstants";
44
import { BaseUserInfo, CurrentUser } from "constants/userConstants";
55
import { MarkUserStatusPayload, UpdateUserPayload } from "redux/reduxActions/userActions";
66
import { ApiResponse, GenericApiResponse } from "./apiResponses";
@@ -60,10 +60,23 @@ export interface FetchApiKeysResponse extends ApiResponse {
6060

6161
export type GetCurrentUserResponse = GenericApiResponse<CurrentUser>;
6262

63+
export interface GetMyOrgsResponse extends ApiResponse {
64+
data: {
65+
data: Array<{
66+
orgId: string;
67+
orgName: string;
68+
}>;
69+
pageNum: number;
70+
pageSize: number;
71+
total: number;
72+
};
73+
}
74+
6375
class UserApi extends Api {
6476
static thirdPartyLoginURL = "/auth/tp/login";
6577
static thirdPartyBindURL = "/auth/tp/bind";
6678
static usersURL = "/users";
79+
static myOrgsURL = "/users/myorg";
6780
static sendVerifyCodeURL = "/auth/otp/send";
6881
static logoutURL = "/auth/logout";
6982
static userURL = "/users/me";
@@ -127,6 +140,19 @@ class UserApi extends Api {
127140
static getCurrentUser(): AxiosPromise<GetCurrentUserResponse> {
128141
return Api.get(UserApi.currentUserURL);
129142
}
143+
static getMyOrgs(
144+
pageNum: number = 1,
145+
pageSize: number = 20,
146+
orgName?: string
147+
): AxiosPromise<GetMyOrgsResponse> {
148+
const params = new URLSearchParams({
149+
pageNum: pageNum.toString(),
150+
pageSize: pageSize.toString(),
151+
...(orgName && { orgName })
152+
});
153+
154+
return Api.get(`${UserApi.myOrgsURL}?${params}`);
155+
}
130156

131157
static getRawCurrentUser(): AxiosPromise<GetCurrentUserResponse> {
132158
return Api.get(UserApi.rawCurrentUserURL);

‎client/packages/lowcoder/src/constants/reduxActionConstants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ export const ReduxActionTypes = {
1111
FETCH_API_KEYS_SUCCESS: "FETCH_API_KEYS_SUCCESS",
1212
MOVE_TO_FOLDER2_SUCCESS: "MOVE_TO_FOLDER2_SUCCESS",
1313

14+
/* workspace RELATED */
15+
FETCH_WORKSPACES_INIT: "FETCH_WORKSPACES_INIT",
16+
FETCH_WORKSPACES_SUCCESS: "FETCH_WORKSPACES_SUCCESS",
17+
18+
19+
1420
/* plugin RELATED */
1521
FETCH_DATA_SOURCE_TYPES: "FETCH_DATA_SOURCE_TYPES",
1622
FETCH_DATA_SOURCE_TYPES_SUCCESS: "FETCH_DATA_SOURCE_TYPES_SUCCESS",

‎client/packages/lowcoder/src/i18n/locales/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3639,6 +3639,7 @@ export const en = {
36393639
"profile": {
36403640
"orgSettings": "Workspace Settings",
36413641
"switchOrg": "Switch Workspace",
3642+
"switchWorkspace": "Switch",
36423643
"joinedOrg": "My Workspaces",
36433644
"createOrg": "Create Workspace",
36443645
"logout": "Log Out",
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import React from 'react';
2+
import { useDispatch, useSelector } from 'react-redux';
3+
import styled from 'styled-components';
4+
import { Input, Pagination, Spin } from 'antd';
5+
import { User } from 'constants/userConstants';
6+
import { switchOrg, createOrgAction } from 'redux/reduxActions/orgActions';
7+
import { selectSystemConfig } from 'redux/selectors/configSelectors';
8+
import { showSwitchOrg } from '@lowcoder-ee/pages/common/customerService';
9+
import { useWorkspaceManager } from 'util/useWorkspaceManager';
10+
import { trans } from 'i18n';
11+
import {
12+
AddIcon,
13+
CheckoutIcon,
14+
SearchIcon,
15+
} from 'lowcoder-design';
16+
import { ORGANIZATION_SETTING } from 'constants/routesURL';
17+
import history from 'util/history';
18+
import { Org } from 'constants/orgConstants';
19+
20+
// Styled Components
21+
const WorkspaceSection = styled.div`
22+
padding: 8px 0;
23+
`;
24+
25+
const SectionHeader = styled.div`
26+
padding: 8px 16px;
27+
font-size: 12px;
28+
font-weight: 500;
29+
color: #8b8fa3;
30+
text-transform: uppercase;
31+
letter-spacing: 0.5px;
32+
`;
33+
34+
const SearchContainer = styled.div`
35+
padding: 8px 12px;
36+
border-bottom: 1px solid #f0f0f0;
37+
`;
38+
39+
const StyledSearchInput = styled(Input)`
40+
.ant-input {
41+
border: 1px solid #e1e3eb;
42+
border-radius: 6px;
43+
font-size: 13px;
44+
45+
&:focus {
46+
border-color: #4965f2;
47+
box-shadow: 0 0 0 2px rgba(73, 101, 242, 0.1);
48+
}
49+
}
50+
`;
51+
52+
const WorkspaceList = styled.div`
53+
max-height: 200px;
54+
overflow-y: auto;
55+
56+
&::-webkit-scrollbar {
57+
width: 4px;
58+
}
59+
60+
&::-webkit-scrollbar-track {
61+
background: #f1f1f1;
62+
}
63+
64+
&::-webkit-scrollbar-thumb {
65+
background: #c1c1c1;
66+
border-radius: 2px;
67+
}
68+
69+
&::-webkit-scrollbar-thumb:hover {
70+
background: #a8a8a8;
71+
}
72+
`;
73+
74+
const WorkspaceItem = styled.div<{ isActive?: boolean }>`
75+
display: flex;
76+
align-items: center;
77+
padding: 10px 16px;
78+
cursor: pointer;
79+
transition: background-color 0.2s;
80+
background-color: ${props => props.isActive ? '#f0f5ff' : 'transparent'};
81+
82+
&:hover {
83+
background-color: ${props => props.isActive ? '#f0f5ff' : '#f8f9fa'};
84+
}
85+
`;
86+
87+
const WorkspaceName = styled.div`
88+
flex: 1;
89+
font-size: 13px;
90+
color: #222222;
91+
overflow: hidden;
92+
text-overflow: ellipsis;
93+
white-space: nowrap;
94+
`;
95+
96+
const ActiveIcon = styled(CheckoutIcon)`
97+
width: 16px;
98+
height: 16px;
99+
color: #4965f2;
100+
margin-left: 8px;
101+
`;
102+
103+
const CreateWorkspaceItem = styled.div`
104+
display: flex;
105+
align-items: center;
106+
padding: 12px 16px;
107+
cursor: pointer;
108+
transition: background-color 0.2s;
109+
font-size: 13px;
110+
color: #4965f2;
111+
font-weight: 500;
112+
113+
&:hover {
114+
background-color: #f0f5ff;
115+
color: #3651d4;
116+
}
117+
118+
svg {
119+
width: 16px;
120+
height: 16px;
121+
margin-right: 10px;
122+
color: #4965f2;
123+
}
124+
125+
&:hover svg {
126+
color: #3651d4;
127+
}
128+
`;
129+
130+
const EmptyState = styled.div`
131+
padding: 20px 16px;
132+
text-align: center;
133+
color: #8b8fa3;
134+
font-size: 13px;
135+
`;
136+
137+
const PaginationContainer = styled.div`
138+
padding: 12px 16px;
139+
border-top: 1px solid #f0f0f0;
140+
display: flex;
141+
justify-content: center;
142+
143+
.ant-pagination {
144+
margin: 0;
145+
146+
.ant-pagination-item {
147+
min-width: 24px;
148+
height: 24px;
149+
line-height: 22px;
150+
font-size: 12px;
151+
margin-right: 4px;
152+
}
153+
154+
.ant-pagination-prev,
155+
.ant-pagination-next {
156+
min-width: 24px;
157+
height: 24px;
158+
line-height: 22px;
159+
margin-right: 4px;
160+
}
161+
162+
.ant-pagination-item-link {
163+
font-size: 11px;
164+
}
165+
}
166+
`;
167+
168+
const LoadingContainer = styled.div`
169+
display: flex;
170+
align-items: center;
171+
justify-content: center;
172+
padding: 24px 16px;
173+
`;
174+
175+
// Component Props
176+
interface WorkspaceSectionProps {
177+
user: User;
178+
isDropdownOpen: boolean;
179+
onClose: () => void;
180+
}
181+
182+
// Main Component
183+
export default function WorkspaceSectionComponent({
184+
user,
185+
isDropdownOpen,
186+
onClose
187+
}: WorkspaceSectionProps) {
188+
const dispatch = useDispatch();
189+
const sysConfig = useSelector(selectSystemConfig);
190+
191+
// Use our custom hook
192+
const {
193+
searchTerm,
194+
currentPage,
195+
totalCount,
196+
isLoading,
197+
displayWorkspaces,
198+
handleSearchChange,
199+
handlePageChange,
200+
pageSize,
201+
} = useWorkspaceManager({});
202+
203+
// Early returns for better performance
204+
if (!showSwitchOrg(user, sysConfig)) return null;
205+
206+
// Event handlers
207+
const handleOrgSwitch = (orgId: string) => {
208+
if (user.currentOrgId !== orgId) {
209+
dispatch(switchOrg(orgId));
210+
}
211+
onClose();
212+
};
213+
214+
const handleCreateOrg = () => {
215+
dispatch(createOrgAction(user.orgs));
216+
history.push(ORGANIZATION_SETTING);
217+
onClose();
218+
};
219+
220+
return (
221+
<WorkspaceSection>
222+
<SectionHeader>{trans("profile.switchOrg")}</SectionHeader>
223+
224+
{/* Search Input - Only show if more than 3 workspaces */}
225+
<SearchContainer>
226+
<StyledSearchInput
227+
placeholder="Search workspaces..."
228+
value={searchTerm}
229+
onChange={(e) => handleSearchChange(e.target.value)}
230+
prefix={<SearchIcon style={{ color: "#8b8fa3" }} />}
231+
size="small"
232+
/>
233+
</SearchContainer>
234+
235+
{/* Workspace List */}
236+
<WorkspaceList>
237+
{isLoading ? (
238+
<LoadingContainer>
239+
<Spin size="small" />
240+
</LoadingContainer>
241+
) : displayWorkspaces.length > 0 ? (
242+
displayWorkspaces.map((org: Org) => (
243+
<WorkspaceItem
244+
key={org.id}
245+
isActive={user.currentOrgId === org.id}
246+
onClick={() => handleOrgSwitch(org.id)}
247+
>
248+
<WorkspaceName title={org.name}>{org.name}</WorkspaceName>
249+
{user.currentOrgId === org.id && <ActiveIcon />}
250+
</WorkspaceItem>
251+
))
252+
) : (
253+
<EmptyState>
254+
{searchTerm.trim()
255+
? "No workspaces found"
256+
: "No workspaces available"
257+
}
258+
</EmptyState>
259+
)}
260+
</WorkspaceList>
261+
262+
{/* Pagination - Only show when needed */}
263+
{totalCount > pageSize && !isLoading && (
264+
<PaginationContainer>
265+
<Pagination
266+
current={currentPage}
267+
total={totalCount}
268+
pageSize={pageSize}
269+
size="small"
270+
showSizeChanger={false}
271+
showQuickJumper={false}
272+
showTotal={(total, range) =>
273+
`${range[0]}-${range[1]} of ${total}`
274+
}
275+
onChange={handlePageChange}
276+
simple={totalCount > 100} // Simple mode for large datasets
277+
/>
278+
</PaginationContainer>
279+
)}
280+
281+
{/* Create Workspace Button */}
282+
<CreateWorkspaceItem onClick={handleCreateOrg}>
283+
<AddIcon />
284+
{trans("profile.createOrg")}
285+
</CreateWorkspaceItem>
286+
</WorkspaceSection>
287+
);
288+
}

0 commit comments

Comments
 (0)