Skip to content

[Feat]: Improve Profile Dropdown, and Workspaces Page using "myorg" endpoint #1787

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 29 commits into from
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
035fc26
redesign profile dropdown
iamfaran Jun 16, 2025
a42f83c
testing workspaces endpoint
iamfaran Jun 16, 2025
8b40672
fix profile dropdown
iamfaran Jun 16, 2025
98695fb
setup redux, sagas for the new myorg endpoint
iamfaran Jun 16, 2025
35b7c68
fix profile dropdown create workspace issue
iamfaran Jun 16, 2025
a5d372a
test
iamfaran Jun 17, 2025
1b63471
fix params
iamfaran Jun 17, 2025
049d372
make currentOrg selector
iamfaran Jun 17, 2025
a902532
add page size param
iamfaran Jun 17, 2025
7709d58
replace orgs data with myorg for dropdown
iamfaran Jun 17, 2025
e202fcb
remove dispatch from the profile dropdown
iamfaran Jun 17, 2025
818fdf8
Merge branch 'dev' of github.com:lowcoder-org/lowcoder into profileDr…
iamfaran Jun 18, 2025
f851353
fetch 10 workspaces initially
iamfaran Jun 18, 2025
9ea107b
add pagination and filtering for the dropdown
iamfaran Jun 18, 2025
28a2101
refactor profile dropdown
iamfaran Jun 18, 2025
0d59610
fix debouncing
iamfaran Jun 18, 2025
c7edbd1
fix loading when search
iamfaran Jun 18, 2025
238698d
fix shrinking issues when page 1 to page 2
iamfaran Jun 18, 2025
0611910
add antD loader in UI
iamfaran Jun 18, 2025
ffa7757
fix delete sync
iamfaran Jun 19, 2025
96acd6a
fix sync after edit
iamfaran Jun 19, 2025
db11f0e
add pagination/filtering for the Workspaces page
iamfaran Jun 19, 2025
2f291f7
add selector for the current org
iamfaran Jun 19, 2025
b3abc61
add active indicator in the workspaces page
iamfaran Jun 19, 2025
23fcbf9
add the ability to switch workspaces from workspaces page
iamfaran Jun 19, 2025
cd92f7c
disable row click on switch
iamfaran Jun 19, 2025
73a64b4
fix switch org button
iamfaran Jun 19, 2025
763bd98
Merge branch 'dev' of github.com:lowcoder-org/lowcoder into feat/myor…
iamfaran Jun 19, 2025
24e04c0
Merge branch 'dev' into feat/myorg-endpoint
raheeliftikhar5 Jun 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion client/packages/lowcoder/src/api/userApi.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -60,10 +60,23 @@ export interface FetchApiKeysResponse extends ApiResponse {

export type GetCurrentUserResponse = GenericApiResponse<CurrentUser>;

export interface GetMyOrgsResponse extends ApiResponse {
data: {
data: Array<{
orgId: string;
orgName: string;
}>;
pageNum: number;
pageSize: number;
total: number;
};
}

class UserApi extends Api {
static thirdPartyLoginURL = "/auth/tp/#";
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";
Expand Down Expand Up @@ -127,6 +140,19 @@ class UserApi extends Api {
static getCurrentUser(): AxiosPromise<GetCurrentUserResponse> {
return Api.get(UserApi.currentUserURL);
}
static getMyOrgs(
pageNum: number = 1,
pageSize: number = 20,
orgName?: string
): AxiosPromise<GetMyOrgsResponse> {
const params = new URLSearchParams({
pageNum: pageNum.toString(),
pageSize: pageSize.toString(),
...(orgName && { orgName })
});

return Api.get(`${UserApi.myOrgsURL}?${params}`);
}

static getRawCurrentUser(): AxiosPromise<GetCurrentUserResponse> {
return Api.get(UserApi.rawCurrentUserURL);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ 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",



/* plugin RELATED */
FETCH_DATA_SOURCE_TYPES: "FETCH_DATA_SOURCE_TYPES",
FETCH_DATA_SOURCE_TYPES_SUCCESS: "FETCH_DATA_SOURCE_TYPES_SUCCESS",
Expand Down
1 change: 1 addition & 0 deletions client/packages/lowcoder/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
288 changes: 288 additions & 0 deletions client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { Input, Pagination, Spin } 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,
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 LoadingContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
`;

// 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({});

// Early returns for better performance
if (!showSwitchOrg(user, sysConfig)) 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 (
<WorkspaceSection>
<SectionHeader>{trans("profile.switchOrg")}</SectionHeader>

{/* Search Input - Only show if more than 3 workspaces */}
<SearchContainer>
<StyledSearchInput
placeholder="Search workspaces..."
value={searchTerm}
onChange={(e) => handleSearchChange(e.target.value)}
prefix={<SearchIcon style={{ color: "#8b8fa3" }} />}
size="small"
/>
</SearchContainer>

{/* Workspace List */}
<WorkspaceList>
{isLoading ? (
<LoadingContainer>
<Spin size="small" />
</LoadingContainer>
) : displayWorkspaces.length > 0 ? (
displayWorkspaces.map((org: Org) => (
<WorkspaceItem
key={org.id}
isActive={user.currentOrgId === org.id}
onClick={() => handleOrgSwitch(org.id)}
>
<WorkspaceName title={org.name}>{org.name}</WorkspaceName>
{user.currentOrgId === org.id && <ActiveIcon />}
</WorkspaceItem>
))
) : (
<EmptyState>
{searchTerm.trim()
? "No workspaces found"
: "No workspaces available"
}
</EmptyState>
)}
</WorkspaceList>

{/* Pagination - Only show when needed */}
{totalCount > pageSize && !isLoading && (
<PaginationContainer>
<Pagination
current={currentPage}
total={totalCount}
pageSize={pageSize}
size="small"
showSizeChanger={false}
showQuickJumper={false}
showTotal={(total, range) =>
`${range[0]}-${range[1]} of ${total}`
}
onChange={handlePageChange}
simple={totalCount > 100} // Simple mode for large datasets
/>
</PaginationContainer>
)}

{/* Create Workspace Button */}
<CreateWorkspaceItem onClick={handleCreateOrg}>
<AddIcon />
{trans("profile.createOrg")}
</CreateWorkspaceItem>
</WorkspaceSection>
);
}
Loading
Loading