feat: add multi-organization dropdown for organization navigation (#40967)
This commit is contained in:
parent
24ec7954a2
commit
bb1c055126
|
|
@ -19,3 +19,9 @@ export const updateOrganizationConfig = (
|
||||||
type: ReduxActionTypes.UPDATE_ORGANIZATION_CONFIG,
|
type: ReduxActionTypes.UPDATE_ORGANIZATION_CONFIG,
|
||||||
payload,
|
payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const fetchMyOrganizations = () => {
|
||||||
|
return {
|
||||||
|
type: ReduxActionTypes.FETCH_MY_ORGANIZATIONS_INIT,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,20 @@ export interface UpdateOrganizationConfigRequest {
|
||||||
apiConfig?: AxiosRequestConfig;
|
apiConfig?: AxiosRequestConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FetchMyOrganizationsResponse = ApiResponse<{
|
||||||
|
organizations: Organization[];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export interface Organization {
|
||||||
|
organizationId: string;
|
||||||
|
organizationName: string;
|
||||||
|
organizationUrl: string;
|
||||||
|
state: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class OrganizationApi extends Api {
|
export class OrganizationApi extends Api {
|
||||||
static tenantsUrl = "v1/tenants";
|
static tenantsUrl = "v1/tenants";
|
||||||
|
static meUrl = "v1/users/me";
|
||||||
|
|
||||||
static async fetchCurrentOrganizationConfig(): Promise<
|
static async fetchCurrentOrganizationConfig(): Promise<
|
||||||
AxiosPromise<FetchCurrentOrganizationConfigResponse>
|
AxiosPromise<FetchCurrentOrganizationConfigResponse>
|
||||||
|
|
@ -41,6 +53,12 @@ export class OrganizationApi extends Api {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async fetchMyOrganizations(): Promise<
|
||||||
|
AxiosPromise<FetchMyOrganizationsResponse>
|
||||||
|
> {
|
||||||
|
return Api.get(`${OrganizationApi.meUrl}/organizations`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OrganizationApi;
|
export default OrganizationApi;
|
||||||
|
|
|
||||||
|
|
@ -1229,6 +1229,8 @@ const OrganizationActionTypes = {
|
||||||
FETCH_PRODUCT_ALERT_INIT: "FETCH_PRODUCT_ALERT_INIT",
|
FETCH_PRODUCT_ALERT_INIT: "FETCH_PRODUCT_ALERT_INIT",
|
||||||
FETCH_PRODUCT_ALERT_SUCCESS: "FETCH_PRODUCT_ALERT_SUCCESS",
|
FETCH_PRODUCT_ALERT_SUCCESS: "FETCH_PRODUCT_ALERT_SUCCESS",
|
||||||
UPDATE_PRODUCT_ALERT_CONFIG: "UPDATE_PRODUCT_ALERT_CONFIG",
|
UPDATE_PRODUCT_ALERT_CONFIG: "UPDATE_PRODUCT_ALERT_CONFIG",
|
||||||
|
FETCH_MY_ORGANIZATIONS_INIT: "FETCH_MY_ORGANIZATIONS_INIT",
|
||||||
|
FETCH_MY_ORGANIZATIONS_SUCCESS: "FETCH_MY_ORGANIZATIONS_SUCCESS",
|
||||||
};
|
};
|
||||||
|
|
||||||
const OrganizationActionErrorTypes = {
|
const OrganizationActionErrorTypes = {
|
||||||
|
|
@ -1236,6 +1238,7 @@ const OrganizationActionErrorTypes = {
|
||||||
"FETCH_CURRENT_ORGANIZATION_CONFIG_ERROR",
|
"FETCH_CURRENT_ORGANIZATION_CONFIG_ERROR",
|
||||||
UPDATE_ORGANIZATION_CONFIG_ERROR: "UPDATE_ORGANIZATION_CONFIG_ERROR",
|
UPDATE_ORGANIZATION_CONFIG_ERROR: "UPDATE_ORGANIZATION_CONFIG_ERROR",
|
||||||
FETCH_PRODUCT_ALERT_FAILED: "FETCH_PRODUCT_ALERT_FAILED",
|
FETCH_PRODUCT_ALERT_FAILED: "FETCH_PRODUCT_ALERT_FAILED",
|
||||||
|
FETCH_MY_ORGANIZATIONS_ERROR: "FETCH_MY_ORGANIZATIONS_ERROR",
|
||||||
};
|
};
|
||||||
|
|
||||||
const AnalyticsActionTypes = {
|
const AnalyticsActionTypes = {
|
||||||
|
|
|
||||||
|
|
@ -2717,3 +2717,5 @@ export const MULTI_ORG_FOOTER_CREATE_ORG_LEFT_TEXT = () =>
|
||||||
"Looking to create one?";
|
"Looking to create one?";
|
||||||
export const MULTI_ORG_FOOTER_CREATE_ORG_RIGHT_TEXT = () =>
|
export const MULTI_ORG_FOOTER_CREATE_ORG_RIGHT_TEXT = () =>
|
||||||
"Create an organization";
|
"Create an organization";
|
||||||
|
|
||||||
|
export const PENDING_INVITATIONS = () => "Pending invitations";
|
||||||
|
|
|
||||||
|
|
@ -99,8 +99,11 @@ import {
|
||||||
getIsFetchingApplications,
|
getIsFetchingApplications,
|
||||||
} from "ee/selectors/selectedWorkspaceSelectors";
|
} from "ee/selectors/selectedWorkspaceSelectors";
|
||||||
import {
|
import {
|
||||||
|
getIsFetchingMyOrganizations,
|
||||||
|
getMyOrganizations,
|
||||||
getOrganizationPermissions,
|
getOrganizationPermissions,
|
||||||
shouldShowLicenseBanner,
|
shouldShowLicenseBanner,
|
||||||
|
activeOrganizationId,
|
||||||
} from "ee/selectors/organizationSelectors";
|
} from "ee/selectors/organizationSelectors";
|
||||||
import { getWorkflowsList } from "ee/selectors/workflowSelectors";
|
import { getWorkflowsList } from "ee/selectors/workflowSelectors";
|
||||||
import {
|
import {
|
||||||
|
|
@ -141,6 +144,10 @@ import {
|
||||||
} from "git";
|
} from "git";
|
||||||
import OldRepoLimitExceededErrorModal from "pages/Editor/gitSync/RepoLimitExceededErrorModal";
|
import OldRepoLimitExceededErrorModal from "pages/Editor/gitSync/RepoLimitExceededErrorModal";
|
||||||
import { trackCurrentDomain } from "utils/multiOrgDomains";
|
import { trackCurrentDomain } from "utils/multiOrgDomains";
|
||||||
|
import OrganizationDropdown from "components/OrganizationDropdown";
|
||||||
|
import { fetchMyOrganizations } from "ee/actions/organizationActions";
|
||||||
|
import type { Organization } from "ee/api/OrganizationApi";
|
||||||
|
import { useIsCloudBillingEnabled } from "hooks";
|
||||||
|
|
||||||
function GitModals() {
|
function GitModals() {
|
||||||
const isGitModEnabled = useGitModEnabled();
|
const isGitModEnabled = useGitModEnabled();
|
||||||
|
|
@ -434,25 +441,45 @@ export const submitCreateWorkspaceForm = async (data: any, dispatch: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface LeftPaneProps {
|
export interface LeftPaneProps {
|
||||||
isBannerVisible?: boolean;
|
activeOrganizationId?: string;
|
||||||
isFetchingWorkspaces: boolean;
|
|
||||||
workspaces: Workspace[];
|
|
||||||
activeWorkspaceId?: string;
|
activeWorkspaceId?: string;
|
||||||
|
isBannerVisible?: boolean;
|
||||||
|
isFetchingOrganizations: boolean;
|
||||||
|
isFetchingWorkspaces: boolean;
|
||||||
|
organizations: Organization[];
|
||||||
|
workspaces: Workspace[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LeftPane(props: LeftPaneProps) {
|
export function LeftPane(props: LeftPaneProps) {
|
||||||
const {
|
const {
|
||||||
|
activeOrganizationId,
|
||||||
activeWorkspaceId,
|
activeWorkspaceId,
|
||||||
isBannerVisible = false,
|
isBannerVisible = false,
|
||||||
|
isFetchingOrganizations,
|
||||||
isFetchingWorkspaces,
|
isFetchingWorkspaces,
|
||||||
|
organizations = [],
|
||||||
workspaces = [],
|
workspaces = [],
|
||||||
} = props;
|
} = props;
|
||||||
const isMobile = useIsMobileDevice();
|
const isMobile = useIsMobileDevice();
|
||||||
|
const isCloudBillingEnabled = useIsCloudBillingEnabled();
|
||||||
|
|
||||||
if (isMobile) return null;
|
if (isMobile) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LeftPaneWrapper isBannerVisible={isBannerVisible}>
|
<LeftPaneWrapper isBannerVisible={isBannerVisible}>
|
||||||
|
{isCloudBillingEnabled &&
|
||||||
|
!isFetchingOrganizations &&
|
||||||
|
organizations.length > 0 && (
|
||||||
|
<OrganizationDropdown
|
||||||
|
organizations={organizations}
|
||||||
|
selectedOrganization={
|
||||||
|
organizations.find(
|
||||||
|
(organization) =>
|
||||||
|
organization.organizationId === activeOrganizationId,
|
||||||
|
) || organizations[0]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<LeftPaneSection
|
<LeftPaneSection
|
||||||
heading={createMessage(WORKSPACES_HEADING)}
|
heading={createMessage(WORKSPACES_HEADING)}
|
||||||
isBannerVisible={isBannerVisible}
|
isBannerVisible={isBannerVisible}
|
||||||
|
|
@ -992,6 +1019,9 @@ export const ApplictionsMainPage = (props: any) => {
|
||||||
const isHomePage = useRouteMatch("/applications")?.isExact;
|
const isHomePage = useRouteMatch("/applications")?.isExact;
|
||||||
const isLicensePage = useRouteMatch("/license")?.isExact;
|
const isLicensePage = useRouteMatch("/license")?.isExact;
|
||||||
const isBannerVisible = showBanner && (isHomePage || isLicensePage);
|
const isBannerVisible = showBanner && (isHomePage || isLicensePage);
|
||||||
|
const organizations = useSelector(getMyOrganizations);
|
||||||
|
const isFetchingOrganizations = useSelector(getIsFetchingMyOrganizations);
|
||||||
|
const currentOrganizationId = useSelector(activeOrganizationId);
|
||||||
|
|
||||||
// TODO: Fix this the next time the file is edited
|
// TODO: Fix this the next time the file is edited
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|
@ -1013,6 +1043,10 @@ export const ApplictionsMainPage = (props: any) => {
|
||||||
workspaceIdFromQueryParams ? workspaceIdFromQueryParams : workspaces[0]?.id,
|
workspaceIdFromQueryParams ? workspaceIdFromQueryParams : workspaces[0]?.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchMyOrganizations());
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveWorkspaceId(
|
setActiveWorkspaceId(
|
||||||
workspaceIdFromQueryParams
|
workspaceIdFromQueryParams
|
||||||
|
|
@ -1056,9 +1090,12 @@ export const ApplictionsMainPage = (props: any) => {
|
||||||
return (
|
return (
|
||||||
<PageWrapper displayName="Applications">
|
<PageWrapper displayName="Applications">
|
||||||
<LeftPane
|
<LeftPane
|
||||||
|
activeOrganizationId={currentOrganizationId}
|
||||||
activeWorkspaceId={activeWorkspaceId}
|
activeWorkspaceId={activeWorkspaceId}
|
||||||
isBannerVisible={isBannerVisible}
|
isBannerVisible={isBannerVisible}
|
||||||
|
isFetchingOrganizations={isFetchingOrganizations}
|
||||||
isFetchingWorkspaces={isFetchingWorkspaces}
|
isFetchingWorkspaces={isFetchingWorkspaces}
|
||||||
|
organizations={organizations}
|
||||||
workspaces={workspaces}
|
workspaces={workspaces}
|
||||||
/>
|
/>
|
||||||
<MediaQuery maxWidth={MOBILE_MAX_WIDTH}>
|
<MediaQuery maxWidth={MOBILE_MAX_WIDTH}>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
createBrandColorsFromPrimaryColor,
|
createBrandColorsFromPrimaryColor,
|
||||||
} from "utils/BrandingUtils";
|
} from "utils/BrandingUtils";
|
||||||
import { createReducer } from "utils/ReducerUtils";
|
import { createReducer } from "utils/ReducerUtils";
|
||||||
|
import type { Organization } from "ee/api/OrganizationApi";
|
||||||
|
|
||||||
export interface OrganizationReduxState<T> {
|
export interface OrganizationReduxState<T> {
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
|
|
@ -21,6 +22,8 @@ export interface OrganizationReduxState<T> {
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
isWithinAnOrganization: boolean;
|
isWithinAnOrganization: boolean;
|
||||||
|
myOrganizations: Organization[];
|
||||||
|
isFetchingMyOrganizations: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultBrandingConfig = {
|
export const defaultBrandingConfig = {
|
||||||
|
|
@ -43,6 +46,8 @@ export const initialState: OrganizationReduxState<any> = {
|
||||||
instanceId: "",
|
instanceId: "",
|
||||||
tenantId: "",
|
tenantId: "",
|
||||||
isWithinAnOrganization: false,
|
isWithinAnOrganization: false,
|
||||||
|
myOrganizations: [],
|
||||||
|
isFetchingMyOrganizations: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handlers = {
|
export const handlers = {
|
||||||
|
|
@ -113,6 +118,32 @@ export const handlers = {
|
||||||
...state,
|
...state,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
}),
|
}),
|
||||||
|
[ReduxActionTypes.FETCH_MY_ORGANIZATIONS_INIT]: (
|
||||||
|
// TODO: Fix this the next time the file is edited
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
state: OrganizationReduxState<any>,
|
||||||
|
) => ({
|
||||||
|
...state,
|
||||||
|
isFetchingMyOrganizations: true,
|
||||||
|
}),
|
||||||
|
[ReduxActionTypes.FETCH_MY_ORGANIZATIONS_SUCCESS]: (
|
||||||
|
// TODO: Fix this the next time the file is edited
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
state: OrganizationReduxState<any>,
|
||||||
|
action: ReduxAction<Organization[]>,
|
||||||
|
) => ({
|
||||||
|
...state,
|
||||||
|
myOrganizations: action.payload,
|
||||||
|
isFetchingMyOrganizations: false,
|
||||||
|
}),
|
||||||
|
[ReduxActionErrorTypes.FETCH_MY_ORGANIZATIONS_ERROR]: (
|
||||||
|
// TODO: Fix this the next time the file is edited
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
state: OrganizationReduxState<any>,
|
||||||
|
) => ({
|
||||||
|
...state,
|
||||||
|
isFetchingMyOrganizations: false,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createReducer(initialState, handlers);
|
export default createReducer(initialState, handlers);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@ import {
|
||||||
} from "ee/constants/ReduxActionConstants";
|
} from "ee/constants/ReduxActionConstants";
|
||||||
import { call, put } from "redux-saga/effects";
|
import { call, put } from "redux-saga/effects";
|
||||||
import type { APIResponseError, ApiResponse } from "api/ApiResponses";
|
import type { APIResponseError, ApiResponse } from "api/ApiResponses";
|
||||||
import type { UpdateOrganizationConfigRequest } from "ee/api/OrganizationApi";
|
import type {
|
||||||
|
FetchMyOrganizationsResponse,
|
||||||
|
UpdateOrganizationConfigRequest,
|
||||||
|
} from "ee/api/OrganizationApi";
|
||||||
import { OrganizationApi } from "ee/api/OrganizationApi";
|
import { OrganizationApi } from "ee/api/OrganizationApi";
|
||||||
import { validateResponse } from "sagas/ErrorSagas";
|
import { validateResponse } from "sagas/ErrorSagas";
|
||||||
import { safeCrashAppRequest } from "actions/errorActions";
|
import { safeCrashAppRequest } from "actions/errorActions";
|
||||||
|
|
@ -158,3 +161,26 @@ export function* updateOrganizationConfigSaga(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function* fetchMyOrganizationsSaga() {
|
||||||
|
try {
|
||||||
|
const response: FetchMyOrganizationsResponse = yield call(
|
||||||
|
OrganizationApi.fetchMyOrganizations,
|
||||||
|
);
|
||||||
|
const isValidResponse: boolean = yield validateResponse(response);
|
||||||
|
|
||||||
|
if (isValidResponse) {
|
||||||
|
yield put({
|
||||||
|
type: ReduxActionTypes.FETCH_MY_ORGANIZATIONS_SUCCESS,
|
||||||
|
payload: response.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
yield put({
|
||||||
|
type: ReduxActionErrorTypes.FETCH_MY_ORGANIZATIONS_ERROR,
|
||||||
|
payload: {
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,3 +67,15 @@ export const isFreePlan = (state: DefaultRootState) => true;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export const isWithinAnOrganization = (state: DefaultRootState) => true;
|
export const isWithinAnOrganization = (state: DefaultRootState) => true;
|
||||||
|
|
||||||
|
export const getMyOrganizations = (state: DefaultRootState) => {
|
||||||
|
return state.organization?.myOrganizations || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIsFetchingMyOrganizations = (state: DefaultRootState) => {
|
||||||
|
return state.organization?.isFetchingMyOrganizations || false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const activeOrganizationId = (state: DefaultRootState) => {
|
||||||
|
return state.organization?.tenantId;
|
||||||
|
};
|
||||||
|
|
|
||||||
205
app/client/src/components/OrganizationDropdown/index.tsx
Normal file
205
app/client/src/components/OrganizationDropdown/index.tsx
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
import { Avatar, Icon } from "@appsmith/ads";
|
||||||
|
import type { Organization } from "ee/api/OrganizationApi";
|
||||||
|
import { createMessage, PENDING_INVITATIONS } from "ee/constants/messages";
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
DropdownContainer,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownTrigger,
|
||||||
|
MenuItem,
|
||||||
|
MenuItemIcon,
|
||||||
|
MenuItemText,
|
||||||
|
SectionDivider,
|
||||||
|
SectionHeader,
|
||||||
|
TriggerContent,
|
||||||
|
TriggerText,
|
||||||
|
} from "./styles";
|
||||||
|
|
||||||
|
export interface PendingInvitation {
|
||||||
|
id: string;
|
||||||
|
organizationName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrganizationDropdownProps {
|
||||||
|
"data-testid"?: string;
|
||||||
|
organizations: Organization[];
|
||||||
|
selectedOrganization: Organization;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrganizationDropdown: React.FC<OrganizationDropdownProps> = ({
|
||||||
|
"data-testid": testId,
|
||||||
|
organizations = [],
|
||||||
|
selectedOrganization,
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const safeOrganizations = organizations || [];
|
||||||
|
const activeOrganizations = safeOrganizations.filter(
|
||||||
|
(org) => org.state === "ACTIVE",
|
||||||
|
);
|
||||||
|
const pendingInvitations = safeOrganizations.filter(
|
||||||
|
(org) => org.state === "INVITED",
|
||||||
|
);
|
||||||
|
|
||||||
|
const generateInitials = (name: string): string => {
|
||||||
|
if (!name) return "";
|
||||||
|
|
||||||
|
return name.charAt(0).toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggle = useCallback(() => {
|
||||||
|
setIsOpen((prev) => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSelect = useCallback((organization: Organization) => {
|
||||||
|
if (organization.organizationUrl) {
|
||||||
|
const url = `https://${organization.organizationUrl}`;
|
||||||
|
|
||||||
|
window.open(url, "_blank", "noopener,noreferrer");
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
setIsOpen(false);
|
||||||
|
triggerRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
dropdownRef.current &&
|
||||||
|
!dropdownRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const displayText = selectedOrganization?.organizationName;
|
||||||
|
|
||||||
|
const renderOrgAvatar = (orgName: string) => {
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
firstLetter={generateInitials(orgName)}
|
||||||
|
label={generateInitials(orgName)}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownContainer data-testid={testId} ref={dropdownRef}>
|
||||||
|
<DropdownTrigger
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-label={`Current organization: ${displayText}`}
|
||||||
|
onClick={handleToggle}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
ref={triggerRef}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<TriggerContent>
|
||||||
|
{renderOrgAvatar(displayText)}
|
||||||
|
<TriggerText>{displayText}</TriggerText>
|
||||||
|
</TriggerContent>
|
||||||
|
<Icon name="dropdown" size="md" />
|
||||||
|
</DropdownTrigger>
|
||||||
|
|
||||||
|
<DropdownMenu aria-label="Organizations" isOpen={isOpen} role="listbox">
|
||||||
|
{activeOrganizations
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aIsSelected =
|
||||||
|
a.organizationId === selectedOrganization?.organizationId;
|
||||||
|
|
||||||
|
const bIsSelected =
|
||||||
|
b.organizationId === selectedOrganization?.organizationId;
|
||||||
|
|
||||||
|
if (aIsSelected && !bIsSelected) return -1;
|
||||||
|
|
||||||
|
if (!aIsSelected && bIsSelected) return 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
.map((org) => {
|
||||||
|
const isSelected =
|
||||||
|
org.organizationId === selectedOrganization?.organizationId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
aria-selected={isSelected}
|
||||||
|
isSelected={isSelected}
|
||||||
|
key={org.organizationId}
|
||||||
|
onClick={!isSelected ? () => handleSelect(org) : undefined}
|
||||||
|
onKeyDown={
|
||||||
|
!isSelected
|
||||||
|
? (e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelect(org);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
role="option"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{renderOrgAvatar(org.organizationName)}
|
||||||
|
<MenuItemText>
|
||||||
|
{org.organizationName}{" "}
|
||||||
|
{org.organizationId ===
|
||||||
|
selectedOrganization?.organizationId && "(current)"}
|
||||||
|
</MenuItemText>
|
||||||
|
|
||||||
|
{!isSelected && (
|
||||||
|
<MenuItemIcon
|
||||||
|
className="hover-icon color-fg-muted"
|
||||||
|
name="share-box-line"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{pendingInvitations.length > 0 && (
|
||||||
|
<>
|
||||||
|
<SectionDivider />
|
||||||
|
<SectionHeader>{createMessage(PENDING_INVITATIONS)}</SectionHeader>
|
||||||
|
{pendingInvitations.map((invitation) => (
|
||||||
|
<MenuItem
|
||||||
|
key={invitation.organizationId}
|
||||||
|
onClick={() => handleSelect(invitation)}
|
||||||
|
role="option"
|
||||||
|
>
|
||||||
|
{renderOrgAvatar(invitation.organizationName)}
|
||||||
|
<MenuItemText>{invitation.organizationName}</MenuItemText>
|
||||||
|
|
||||||
|
<MenuItemIcon
|
||||||
|
className="hover-icon color-fg-muted"
|
||||||
|
name="share-box-line"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenu>
|
||||||
|
</DropdownContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrganizationDropdown;
|
||||||
130
app/client/src/components/OrganizationDropdown/styles.ts
Normal file
130
app/client/src/components/OrganizationDropdown/styles.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { Icon } from "@appsmith/ads";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
export const DropdownContainer = styled.div`
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
margin: var(--ads-v2-spaces-3) 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const DropdownTrigger = styled.button`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--ads-v2-spaces-3) var(--ads-v2-spaces-4);
|
||||||
|
background: var(--ads-v2-color-bg);
|
||||||
|
border: 1px solid var(--ads-v2-color-border);
|
||||||
|
border-radius: var(--ads-v2-border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--ads-v2-color-fg);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-height: 40px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TriggerContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--ads-v2-spaces-3);
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TriggerText = styled.span`
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 400;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const DropdownMenu = styled.div<{ isOpen: boolean }>`
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 12px);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--ads-v2-color-bg);
|
||||||
|
padding: var(--ads-v2-spaces-2);
|
||||||
|
border: 1px solid var(--ads-v2-color-border);
|
||||||
|
border-radius: var(--ads-v2-border-radius);
|
||||||
|
box-shadow: var(--ads-v2-shadow-popovers);
|
||||||
|
z-index: var(--ads-v2-z-index-7);
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
opacity: ${({ isOpen }) => (isOpen ? 1 : 0)};
|
||||||
|
visibility: ${({ isOpen }) => (isOpen ? "visible" : "hidden")};
|
||||||
|
transform: ${({ isOpen }) => (isOpen ? "translateY(0)" : "translateY(-8px)")};
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MenuItem = styled.div<{
|
||||||
|
isSelected?: boolean;
|
||||||
|
}>`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--ads-v2-spaces-3);
|
||||||
|
padding: var(--ads-v2-spaces-3) var(--ads-v2-spaces-4);
|
||||||
|
margin-bottom: var(--ads-v2-spaces-2);
|
||||||
|
border-radius: var(--ads-v2-border-radius);
|
||||||
|
cursor: ${({ isSelected }) => (isSelected ? "default" : "pointer")};
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--ads-v2-color-fg);
|
||||||
|
background: ${({ isSelected }) =>
|
||||||
|
isSelected ? "var(--ads-v2-color-bg-muted)" : "transparent"};
|
||||||
|
|
||||||
|
.hover-icon {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${({ isSelected }) =>
|
||||||
|
isSelected
|
||||||
|
? "var(--ads-v2-color-bg-muted)"
|
||||||
|
: "var(--ads-v2-color-bg-subtle)"};
|
||||||
|
|
||||||
|
.hover-icon {
|
||||||
|
opacity: ${({ isSelected }) => (isSelected ? 0 : 1)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
background: ${({ isSelected }) =>
|
||||||
|
isSelected
|
||||||
|
? "var(--ads-v2-color-bg-muted)"
|
||||||
|
: "var(--ads-v2-color-bg-subtle)"};
|
||||||
|
|
||||||
|
.hover-icon {
|
||||||
|
opacity: ${({ isSelected }) => (isSelected ? 0 : 1)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MenuItemIcon = styled(Icon)`
|
||||||
|
color: var(--ads-v2-color-fg-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MenuItemText = styled.span`
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SectionDivider = styled.div`
|
||||||
|
height: 1px;
|
||||||
|
background: var(--ads-v2-color-border);
|
||||||
|
margin: var(--ads-v2-spaces-2) 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SectionHeader = styled.div`
|
||||||
|
padding: var(--ads-v2-spaces-2) var(--ads-v2-spaces-4);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--ads-v2-color-fg-muted);
|
||||||
|
`;
|
||||||
Loading…
Reference in New Issue
Block a user