Skip to content

Commit bb1c055

Browse files
authored
feat: add multi-organization dropdown for organization navigation (#40967)
1 parent 24ec795 commit bb1c055

File tree

10 files changed

+472
-2
lines changed

10 files changed

+472
-2
lines changed

app/client/src/ce/actions/organizationActions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@ export const updateOrganizationConfig = (
1919
type: ReduxActionTypes.UPDATE_ORGANIZATION_CONFIG,
2020
payload,
2121
});
22+
23+
export const fetchMyOrganizations = () => {
24+
return {
25+
type: ReduxActionTypes.FETCH_MY_ORGANIZATIONS_INIT,
26+
};
27+
};

app/client/src/ce/api/OrganizationApi.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,20 @@ export interface UpdateOrganizationConfigRequest {
2020
apiConfig?: AxiosRequestConfig;
2121
}
2222

23+
export type FetchMyOrganizationsResponse = ApiResponse<{
24+
organizations: Organization[];
25+
}>;
26+
27+
export interface Organization {
28+
organizationId: string;
29+
organizationName: string;
30+
organizationUrl: string;
31+
state: string;
32+
}
33+
2334
export class OrganizationApi extends Api {
2435
static tenantsUrl = "v1/tenants";
36+
static meUrl = "v1/users/me";
2537

2638
static async fetchCurrentOrganizationConfig(): Promise<
2739
AxiosPromise<FetchCurrentOrganizationConfigResponse>
@@ -41,6 +53,12 @@ export class OrganizationApi extends Api {
4153
},
4254
);
4355
}
56+
57+
static async fetchMyOrganizations(): Promise<
58+
AxiosPromise<FetchMyOrganizationsResponse>
59+
> {
60+
return Api.get(`${OrganizationApi.meUrl}/organizations`);
61+
}
4462
}
4563

4664
export default OrganizationApi;

app/client/src/ce/constants/ReduxActionConstants.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,13 +1229,16 @@ const OrganizationActionTypes = {
12291229
FETCH_PRODUCT_ALERT_INIT: "FETCH_PRODUCT_ALERT_INIT",
12301230
FETCH_PRODUCT_ALERT_SUCCESS: "FETCH_PRODUCT_ALERT_SUCCESS",
12311231
UPDATE_PRODUCT_ALERT_CONFIG: "UPDATE_PRODUCT_ALERT_CONFIG",
1232+
FETCH_MY_ORGANIZATIONS_INIT: "FETCH_MY_ORGANIZATIONS_INIT",
1233+
FETCH_MY_ORGANIZATIONS_SUCCESS: "FETCH_MY_ORGANIZATIONS_SUCCESS",
12321234
};
12331235

12341236
const OrganizationActionErrorTypes = {
12351237
FETCH_CURRENT_ORGANIZATION_CONFIG_ERROR:
12361238
"FETCH_CURRENT_ORGANIZATION_CONFIG_ERROR",
12371239
UPDATE_ORGANIZATION_CONFIG_ERROR: "UPDATE_ORGANIZATION_CONFIG_ERROR",
12381240
FETCH_PRODUCT_ALERT_FAILED: "FETCH_PRODUCT_ALERT_FAILED",
1241+
FETCH_MY_ORGANIZATIONS_ERROR: "FETCH_MY_ORGANIZATIONS_ERROR",
12391242
};
12401243

12411244
const AnalyticsActionTypes = {

app/client/src/ce/constants/messages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2717,3 +2717,5 @@ export const MULTI_ORG_FOOTER_CREATE_ORG_LEFT_TEXT = () =>
27172717
"Looking to create one?";
27182718
export const MULTI_ORG_FOOTER_CREATE_ORG_RIGHT_TEXT = () =>
27192719
"Create an organization";
2720+
2721+
export const PENDING_INVITATIONS = () => "Pending invitations";

app/client/src/ce/pages/Applications/index.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,11 @@ import {
9999
getIsFetchingApplications,
100100
} from "ee/selectors/selectedWorkspaceSelectors";
101101
import {
102+
getIsFetchingMyOrganizations,
103+
getMyOrganizations,
102104
getOrganizationPermissions,
103105
shouldShowLicenseBanner,
106+
activeOrganizationId,
104107
} from "ee/selectors/organizationSelectors";
105108
import { getWorkflowsList } from "ee/selectors/workflowSelectors";
106109
import {
@@ -141,6 +144,10 @@ import {
141144
} from "git";
142145
import OldRepoLimitExceededErrorModal from "pages/Editor/gitSync/RepoLimitExceededErrorModal";
143146
import { trackCurrentDomain } from "utils/multiOrgDomains";
147+
import OrganizationDropdown from "components/OrganizationDropdown";
148+
import { fetchMyOrganizations } from "ee/actions/organizationActions";
149+
import type { Organization } from "ee/api/OrganizationApi";
150+
import { useIsCloudBillingEnabled } from "hooks";
144151

145152
function GitModals() {
146153
const isGitModEnabled = useGitModEnabled();
@@ -434,25 +441,45 @@ export const submitCreateWorkspaceForm = async (data: any, dispatch: any) => {
434441
};
435442

436443
export interface LeftPaneProps {
444+
activeOrganizationId?: string;
445+
activeWorkspaceId?: string;
437446
isBannerVisible?: boolean;
447+
isFetchingOrganizations: boolean;
438448
isFetchingWorkspaces: boolean;
449+
organizations: Organization[];
439450
workspaces: Workspace[];
440-
activeWorkspaceId?: string;
441451
}
442452

443453
export function LeftPane(props: LeftPaneProps) {
444454
const {
455+
activeOrganizationId,
445456
activeWorkspaceId,
446457
isBannerVisible = false,
458+
isFetchingOrganizations,
447459
isFetchingWorkspaces,
460+
organizations = [],
448461
workspaces = [],
449462
} = props;
450463
const isMobile = useIsMobileDevice();
464+
const isCloudBillingEnabled = useIsCloudBillingEnabled();
451465

452466
if (isMobile) return null;
453467

454468
return (
455469
<LeftPaneWrapper isBannerVisible={isBannerVisible}>
470+
{isCloudBillingEnabled &&
471+
!isFetchingOrganizations &&
472+
organizations.length > 0 && (
473+
<OrganizationDropdown
474+
organizations={organizations}
475+
selectedOrganization={
476+
organizations.find(
477+
(organization) =>
478+
organization.organizationId === activeOrganizationId,
479+
) || organizations[0]
480+
}
481+
/>
482+
)}
456483
<LeftPaneSection
457484
heading={createMessage(WORKSPACES_HEADING)}
458485
isBannerVisible={isBannerVisible}
@@ -992,6 +1019,9 @@ export const ApplictionsMainPage = (props: any) => {
9921019
const isHomePage = useRouteMatch("/applications")?.isExact;
9931020
const isLicensePage = useRouteMatch("/license")?.isExact;
9941021
const isBannerVisible = showBanner && (isHomePage || isLicensePage);
1022+
const organizations = useSelector(getMyOrganizations);
1023+
const isFetchingOrganizations = useSelector(getIsFetchingMyOrganizations);
1024+
const currentOrganizationId = useSelector(activeOrganizationId);
9951025

9961026
// TODO: Fix this the next time the file is edited
9971027
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1013,6 +1043,10 @@ export const ApplictionsMainPage = (props: any) => {
10131043
workspaceIdFromQueryParams ? workspaceIdFromQueryParams : workspaces[0]?.id,
10141044
);
10151045

1046+
useEffect(() => {
1047+
dispatch(fetchMyOrganizations());
1048+
}, []);
1049+
10161050
useEffect(() => {
10171051
setActiveWorkspaceId(
10181052
workspaceIdFromQueryParams
@@ -1056,9 +1090,12 @@ export const ApplictionsMainPage = (props: any) => {
10561090
return (
10571091
<PageWrapper displayName="Applications">
10581092
<LeftPane
1093+
activeOrganizationId={currentOrganizationId}
10591094
activeWorkspaceId={activeWorkspaceId}
10601095
isBannerVisible={isBannerVisible}
1096+
isFetchingOrganizations={isFetchingOrganizations}
10611097
isFetchingWorkspaces={isFetchingWorkspaces}
1098+
organizations={organizations}
10621099
workspaces={workspaces}
10631100
/>
10641101
<MediaQuery maxWidth={MOBILE_MAX_WIDTH}>

app/client/src/ce/reducers/organizationReducer.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
createBrandColorsFromPrimaryColor,
1111
} from "utils/BrandingUtils";
1212
import { createReducer } from "utils/ReducerUtils";
13+
import type { Organization } from "ee/api/OrganizationApi";
1314

1415
export interface OrganizationReduxState<T> {
1516
displayName?: string;
@@ -21,6 +22,8 @@ export interface OrganizationReduxState<T> {
2122
instanceId: string;
2223
tenantId: string;
2324
isWithinAnOrganization: boolean;
25+
myOrganizations: Organization[];
26+
isFetchingMyOrganizations: boolean;
2427
}
2528

2629
export const defaultBrandingConfig = {
@@ -43,6 +46,8 @@ export const initialState: OrganizationReduxState<any> = {
4346
instanceId: "",
4447
tenantId: "",
4548
isWithinAnOrganization: false,
49+
myOrganizations: [],
50+
isFetchingMyOrganizations: false,
4651
};
4752

4853
export const handlers = {
@@ -113,6 +118,32 @@ export const handlers = {
113118
...state,
114119
isLoading: false,
115120
}),
121+
[ReduxActionTypes.FETCH_MY_ORGANIZATIONS_INIT]: (
122+
// TODO: Fix this the next time the file is edited
123+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
124+
state: OrganizationReduxState<any>,
125+
) => ({
126+
...state,
127+
isFetchingMyOrganizations: true,
128+
}),
129+
[ReduxActionTypes.FETCH_MY_ORGANIZATIONS_SUCCESS]: (
130+
// TODO: Fix this the next time the file is edited
131+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
132+
state: OrganizationReduxState<any>,
133+
action: ReduxAction<Organization[]>,
134+
) => ({
135+
...state,
136+
myOrganizations: action.payload,
137+
isFetchingMyOrganizations: false,
138+
}),
139+
[ReduxActionErrorTypes.FETCH_MY_ORGANIZATIONS_ERROR]: (
140+
// TODO: Fix this the next time the file is edited
141+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
142+
state: OrganizationReduxState<any>,
143+
) => ({
144+
...state,
145+
isFetchingMyOrganizations: false,
146+
}),
116147
};
117148

118149
export default createReducer(initialState, handlers);

app/client/src/ce/sagas/organizationSagas.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import {
55
} from "ee/constants/ReduxActionConstants";
66
import { call, put } from "redux-saga/effects";
77
import type { APIResponseError, ApiResponse } from "api/ApiResponses";
8-
import type { UpdateOrganizationConfigRequest } from "ee/api/OrganizationApi";
8+
import type {
9+
FetchMyOrganizationsResponse,
10+
UpdateOrganizationConfigRequest,
11+
} from "ee/api/OrganizationApi";
912
import { OrganizationApi } from "ee/api/OrganizationApi";
1013
import { validateResponse } from "sagas/ErrorSagas";
1114
import { safeCrashAppRequest } from "actions/errorActions";
@@ -158,3 +161,26 @@ export function* updateOrganizationConfigSaga(
158161
});
159162
}
160163
}
164+
165+
export function* fetchMyOrganizationsSaga() {
166+
try {
167+
const response: FetchMyOrganizationsResponse = yield call(
168+
OrganizationApi.fetchMyOrganizations,
169+
);
170+
const isValidResponse: boolean = yield validateResponse(response);
171+
172+
if (isValidResponse) {
173+
yield put({
174+
type: ReduxActionTypes.FETCH_MY_ORGANIZATIONS_SUCCESS,
175+
payload: response.data,
176+
});
177+
}
178+
} catch (error) {
179+
yield put({
180+
type: ReduxActionErrorTypes.FETCH_MY_ORGANIZATIONS_ERROR,
181+
payload: {
182+
error,
183+
},
184+
});
185+
}
186+
}

app/client/src/ce/selectors/organizationSelectors.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,15 @@ export const isFreePlan = (state: DefaultRootState) => true;
6767

6868
// eslint-disable-next-line @typescript-eslint/no-unused-vars
6969
export const isWithinAnOrganization = (state: DefaultRootState) => true;
70+
71+
export const getMyOrganizations = (state: DefaultRootState) => {
72+
return state.organization?.myOrganizations || [];
73+
};
74+
75+
export const getIsFetchingMyOrganizations = (state: DefaultRootState) => {
76+
return state.organization?.isFetchingMyOrganizations || false;
77+
};
78+
79+
export const activeOrganizationId = (state: DefaultRootState) => {
80+
return state.organization?.tenantId;
81+
};

0 commit comments

Comments
 (0)