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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy