import React, {
Component,
useContext,
useEffect,
useRef,
useState,
} from "react";
import styled, { ThemeContext } from "styled-components";
import { connect, useDispatch, useSelector } from "react-redux";
import { useLocation } from "react-router-dom";
import { AppState } from "reducers";
import { Classes as BlueprintClasses } from "@blueprintjs/core";
import {
thinScrollbar,
truncateTextUsingEllipsis,
} from "constants/DefaultTheme";
import {
getApplicationList,
getApplicationSearchKeyword,
getCreateApplicationError,
getIsCreatingApplication,
getIsDeletingApplication,
getIsDuplicatingApplication,
getIsFetchingApplications,
getIsSavingOrgInfo,
getUserApplicationsOrgs,
getUserApplicationsOrgsList,
} from "selectors/applicationSelectors";
import {
ApplicationPayload,
ReduxActionTypes,
} from "constants/ReduxActionConstants";
import PageWrapper from "pages/common/PageWrapper";
import SubHeader from "pages/common/SubHeader";
import ApplicationCard from "./ApplicationCard";
import OrgInviteUsersForm from "pages/organization/OrgInviteUsersForm";
import { isPermitted, PERMISSION_TYPE } from "./permissionHelpers";
import FormDialogComponent from "components/editorComponents/form/FormDialogComponent";
import Dialog from "components/ads/DialogComponent";
// import OnboardingHelper from "components/editorComponents/Onboarding/Helper";
import { User } from "constants/userConstants";
import { getCurrentUser } from "selectors/usersSelectors";
import { CREATE_ORGANIZATION_FORM_NAME } from "constants/forms";
import {
DropdownOnSelectActions,
getOnSelectAction,
} from "pages/common/CustomizedDropdown/dropdownHelpers";
import Button, { Size, Category } from "components/ads/Button";
import Text, { TextType } from "components/ads/Text";
import Icon, { IconName, IconSize } from "components/ads/Icon";
import MenuItem from "components/ads/MenuItem";
import {
duplicateApplication,
updateApplication,
} from "actions/applicationActions";
import { Classes } from "components/ads/common";
import Menu from "components/ads/Menu";
import { Position } from "@blueprintjs/core/lib/esm/common/position";
import { UpdateApplicationPayload } from "api/ApplicationApi";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import { loadingUserOrgs } from "./ApplicationLoaders";
import { creatingApplicationMap } from "reducers/uiReducers/applicationsReducer";
import EditableText, {
EditInteractionKind,
SavingState,
} from "components/ads/EditableText";
import { notEmptyValidator } from "components/ads/TextInput";
import { saveOrg } from "actions/orgActions";
import { leaveOrganization } from "actions/userActions";
import CenteredWrapper from "../../components/designSystems/appsmith/CenteredWrapper";
import NoSearchImage from "../../assets/images/NoSearchResult.svg";
import { getNextEntityName, getRandomPaletteColor } from "utils/AppsmithUtils";
import { AppIconCollection } from "components/ads/AppIcon";
import ProductUpdatesModal from "pages/Applications/ProductUpdatesModal";
import WelcomeHelper from "components/editorComponents/Onboarding/WelcomeHelper";
import { useIntiateOnboarding } from "components/editorComponents/Onboarding/utils";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { createOrganizationSubmitHandler } from "../organization/helpers";
import ImportApplicationModal from "./ImportApplicationModal";
import ImportAppViaGitModal from "pages/Editor/gitSync/ImportAppViaGitModal";
import {
createMessage,
DOCUMENTATION,
ORGANIZATIONS_HEADING,
SEARCH_APPS,
WELCOME_TOUR,
NO_APPS_FOUND,
} from "constants/messages";
import { ReactComponent as NoAppsFoundIcon } from "assets/svg/no-apps-icon.svg";
import { howMuchTimeBeforeText } from "utils/helpers";
import { setHeaderMeta } from "actions/themeActions";
import getFeatureFlags from "utils/featureFlags";
import { setIsImportAppViaGitModalOpen } from "actions/gitSyncActions";
import SharedUserList from "pages/common/SharedUserList";
import { getOnboardingOrganisations } from "selectors/onboardingSelectors";
import { getAppsmithConfigs } from "configs";
const OrgDropDown = styled.div`
display: flex;
padding: ${(props) => props.theme.spaces[4]}px
${(props) => props.theme.spaces[4]}px;
font-size: ${(props) => props.theme.fontSizes[1]}px;
justify-content: space-between;
align-items: center;
`;
const ApplicationCardsWrapper = styled.div`
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: ${(props) => props.theme.fontSizes[4]}px;
padding: 10px;
`;
const OrgSection = styled.div`
margin-bottom: 40px;
`;
const PaddingWrapper = styled.div`
display: flex;
align-items: baseline;
justify-content: center;
width: ${(props) => props.theme.card.minWidth}px;
@media screen and (min-width: 1500px) {
.bp3-card {
width: ${(props) => props.theme.card.minWidth}px;
height: ${(props) => props.theme.card.minHeight}px;
}
}
@media screen and (min-width: 1500px) and (max-width: 1512px) {
width: ${(props) =>
props.theme.card.minWidth + props.theme.spaces[4] * 2}px;
.bp3-card {
width: ${(props) => props.theme.card.minWidth - 5}px;
height: ${(props) => props.theme.card.minHeight - 5}px;
}
}
@media screen and (min-width: 1478px) and (max-width: 1500px) {
width: ${(props) =>
props.theme.card.minWidth + props.theme.spaces[4] * 2}px;
.bp3-card {
width: ${(props) => props.theme.card.minWidth - 8}px;
height: ${(props) => props.theme.card.minHeight - 8}px;
}
}
@media screen and (min-width: 1447px) and (max-width: 1477px) {
width: ${(props) =>
props.theme.card.minWidth + props.theme.spaces[3] * 2}px;
.bp3-card {
width: ${(props) => props.theme.card.minWidth - 8}px;
height: ${(props) => props.theme.card.minHeight - 8}px;
}
}
@media screen and (min-width: 1417px) and (max-width: 1446px) {
width: ${(props) =>
props.theme.card.minWidth + props.theme.spaces[3] * 2}px;
.bp3-card {
width: ${(props) => props.theme.card.minWidth - 11}px;
height: ${(props) => props.theme.card.minHeight - 11}px;
}
}
@media screen and (min-width: 1400px) and (max-width: 1417px) {
width: ${(props) =>
props.theme.card.minWidth + props.theme.spaces[2] * 2}px;
.bp3-card {
width: ${(props) => props.theme.card.minWidth - 15}px;
height: ${(props) => props.theme.card.minHeight - 15}px;
}
}
@media screen and (max-width: 1400px) {
width: ${(props) =>
props.theme.card.minWidth + props.theme.spaces[2] * 2}px;
.bp3-card {
width: ${(props) => props.theme.card.minWidth - 15}px;
height: ${(props) => props.theme.card.minHeight - 15}px;
}
}
`;
const LeftPaneWrapper = styled.div`
overflow: auto;
width: ${(props) => props.theme.homePage.sidebar}px;
height: 100%;
display: flex;
padding-left: 16px;
padding-top: 16px;
flex-direction: column;
position: fixed;
top: ${(props) => props.theme.homePage.header}px;
box-shadow: 1px 0px 0px #ededed;
`;
const ApplicationContainer = styled.div`
height: calc(100vh - ${(props) => props.theme.homePage.search.height - 40}px);
overflow: auto;
padding-right: ${(props) => props.theme.homePage.leftPane.rightMargin}px;
padding-top: 16px;
margin-left: ${(props) =>
props.theme.homePage.leftPane.width +
props.theme.homePage.leftPane.rightMargin +
props.theme.homePage.leftPane.leftPadding}px;
width: calc(
100% -
${(props) =>
props.theme.homePage.leftPane.width +
props.theme.homePage.leftPane.rightMargin +
props.theme.homePage.leftPane.leftPadding}px
);
scroll-behavior: smooth;
`;
const ItemWrapper = styled.div`
padding: 9px 15px;
`;
const StyledIcon = styled(Icon)`
margin-right: 11px;
`;
const OrgShareUsers = styled.div`
display: flex;
align-items: center;
& .t--options-icon {
margin-left: 8px;
svg {
path {
fill: #090707;
}
}
}
& .t--new-button {
margin-left: 8px;
}
& button,
& a {
padding: 4px 12px;
}
`;
const NoAppsFound = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
& > span {
margin-bottom: 24px;
}
`;
function Item(props: {
label: string;
textType: TextType;
icon?: IconName;
isFetchingApplications: boolean;
}) {
return (
{props.icon && }
{" "}
{props.label}
);
}
const LeftPaneDataSection = styled.div`
position: relative;
height: calc(100vh - ${(props) => props.theme.homePage.header + 24}px);
`;
function LeftPaneSection(props: {
heading: string;
children?: any;
isFetchingApplications: boolean;
}) {
return (
{/*
);
}
const StyledAnchor = styled.a`
position: relative;
top: -24px;
`;
const WorkpsacesNavigator = styled.div`
overflow: auto;
height: calc(100vh - ${(props) => props.theme.homePage.header + 252}px);
${thinScrollbar};
/* padding-bottom: 160px; */
`;
const LeftPaneBottomSection = styled.div`
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding-bottom: 8px;
background-color: #fff;
& .ads-dialog-trigger {
margin-top: 4px;
}
& .ads-dialog-trigger > div {
position: initial;
width: 92%;
padding: 0 14px;
}
`;
const LeftPaneVersionData = styled.div`
display: flex;
justify-content: space-between;
color: #121826;
font-size: 8px;
width: 92%;
margin-top: 8px;
`;
const textIconStyles = (props: { color: string; hover: string }) => {
return `
&&&&&& {
.${Classes.TEXT},.${Classes.ICON} svg path {
color: ${props.color};
stroke: ${props.color};
fill: ${props.color};
}
&:hover {
.${Classes.TEXT},.${Classes.ICON} svg path {
color: ${props.hover};
stroke: ${props.hover};
fill: ${props.hover};
}
}
}
`;
};
function OrgMenuItem({ isFetchingApplications, org, selected }: any) {
const menuRef = useRef(null);
useEffect(() => {
if (selected) {
menuRef.current?.scrollIntoView({ behavior: "smooth" });
menuRef.current?.click();
}
}, [selected]);
return (
);
}
const submitCreateOrganizationForm = async (data: any, dispatch: any) => {
const result = await createOrganizationSubmitHandler(data, dispatch);
return result;
};
function LeftPane() {
const dispatch = useDispatch();
const fetchedUserOrgs = useSelector(getUserApplicationsOrgs);
const onboardingOrgs = useSelector(getOnboardingOrganisations);
const isFetchingApplications = useSelector(getIsFetchingApplications);
const { appVersion } = getAppsmithConfigs();
const howMuchTimeBefore = howMuchTimeBeforeText(appVersion.releaseDate);
let userOrgs;
if (!isFetchingApplications) {
userOrgs = fetchedUserOrgs;
} else {
userOrgs = loadingUserOrgs as any;
}
const location = useLocation();
const urlHash = location.hash.slice(1);
const initiateOnboarding = useIntiateOnboarding();
return (
{userOrgs &&
userOrgs.map((org: any) => (
))}
{!isFetchingApplications && fetchedUserOrgs && (
);
}
const CreateNewLabel = styled(Text)`
margin-top: 18px;
`;
const OrgNameElement = styled(Text)`
max-width: 500px;
${truncateTextUsingEllipsis}
`;
const OrgNameHolder = styled(Text)`
display: flex;
align-items: center;
`;
const OrgNameWrapper = styled.div<{ disabled?: boolean }>`
${(props) => {
const color = props.disabled
? props.theme.colors.applications.orgColor
: props.theme.colors.applications.hover.orgColor[9];
return `${textIconStyles({
color: color,
hover: color,
})}`;
}}
.${Classes.ICON} {
display: ${(props) => (!props.disabled ? "inline" : "none")};;
margin-left: 8px;
color: ${(props) => props.theme.colors.applications.iconColor};
}
`;
const OrgRename = styled(EditableText)`
padding: 0 2px;
`;
const NoSearchResultImg = styled.img`
margin: 1em;
`;
function ApplicationsSection(props: any) {
const enableImportExport = true;
const dispatch = useDispatch();
const theme = useContext(ThemeContext);
const isSavingOrgInfo = useSelector(getIsSavingOrgInfo);
const isFetchingApplications = useSelector(getIsFetchingApplications);
const userOrgs = useSelector(getUserApplicationsOrgsList);
const creatingApplicationMap = useSelector(getIsCreatingApplication);
const currentUser = useSelector(getCurrentUser);
const deleteApplication = (applicationId: string) => {
if (applicationId && applicationId.length > 0) {
dispatch({
type: ReduxActionTypes.DELETE_APPLICATION_INIT,
payload: {
applicationId,
},
});
}
};
const [warnLeavingOrganization, setWarnLeavingOrganization] = useState(false);
const [orgToOpenMenu, setOrgToOpenMenu] = useState(null);
const updateApplicationDispatch = (
id: string,
data: UpdateApplicationPayload,
) => {
dispatch(updateApplication(id, data));
};
const duplicateApplicationDispatch = (applicationId: string) => {
dispatch(duplicateApplication(applicationId));
};
const [selectedOrgId, setSelectedOrgId] = useState();
const [
selectedOrgIdForImportApplication,
setSelectedOrgIdForImportApplication,
] = useState();
const Form: any = OrgInviteUsersForm;
const leaveOrg = (orgId: string) => {
setWarnLeavingOrganization(false);
setOrgToOpenMenu(null);
dispatch(leaveOrganization(orgId));
};
const OrgNameChange = (newName: string, orgId: string) => {
dispatch(
saveOrg({
id: orgId as string,
name: newName,
}),
);
};
function OrgMenuTarget(props: {
orgName: string;
disabled?: boolean;
orgSlug: string;
}) {
const { disabled, orgName, orgSlug } = props;
return (
{orgName}
);
}
const createNewApplication = (applicationName: string, orgId: string) => {
const color = getRandomPaletteColor(theme.colors.appCardColors);
const icon =
AppIconCollection[Math.floor(Math.random() * AppIconCollection.length)];
return dispatch({
type: ReduxActionTypes.CREATE_APPLICATION_INIT,
payload: {
applicationName,
orgId,
icon,
color,
},
});
};
let updatedOrgs;
if (!isFetchingApplications) {
updatedOrgs = userOrgs;
} else {
updatedOrgs = loadingUserOrgs as any;
}
let organizationsListComponent;
if (
!isFetchingApplications &&
props.searchKeyword &&
props.searchKeyword.trim().length > 0 &&
updatedOrgs.length === 0
) {
organizationsListComponent = (
{createMessage(NO_APPS_FOUND)}
);
} else {
organizationsListComponent = updatedOrgs.map(
(organizationObject: any, index: number) => {
const { applications, organization, userRoles } = organizationObject;
const hasManageOrgPermissions = isPermitted(
organization.userPermissions,
PERMISSION_TYPE.MANAGE_ORGANIZATION,
);
return (
{(currentUser || isFetchingApplications) &&
OrgMenuTarget({
orgName: organization.name,
orgSlug: organization.slug,
})}
{selectedOrgIdForImportApplication && (
setSelectedOrgIdForImportApplication("")}
organizationId={selectedOrgIdForImportApplication}
/>
)}
{hasManageOrgPermissions && (
)}
{isPermitted(
organization.userPermissions,
PERMISSION_TYPE.INVITE_USER_TO_ORGANIZATION,
) &&
!isFetchingApplications && (
}
/>
{isPermitted(
organization.userPermissions,
PERMISSION_TYPE.CREATE_APPLICATION,
) &&
!isFetchingApplications &&
applications.length !== 0 && (
)}
{applications.map((application: any) => {
return (
);
})}
{applications.length === 0 && (
There’s nothing inside this organization
{/* below component is duplicate. This is because of cypress test were failing */}
)}
);
},
);
}
return (
{organizationsListComponent}
{getFeatureFlags().GIT && }
);
}
type ApplicationProps = {
applicationList: ApplicationPayload[];
searchApplications: (keyword: string) => void;
isCreatingApplication: creatingApplicationMap;
isFetchingApplications: boolean;
createApplicationError?: string;
deleteApplication: (id: string) => void;
deletingApplication: boolean;
duplicatingApplication: boolean;
getAllApplication: () => void;
userOrgs: any;
currentUser?: User;
searchKeyword: string | undefined;
setHeaderMetaData: (
hideHeaderShadow: boolean,
showHeaderSeparator: boolean,
) => void;
};
class Applications extends Component<
ApplicationProps,
{ selectedOrgId: string; showOnboardingForm: boolean }
> {
constructor(props: ApplicationProps) {
super(props);
this.state = {
selectedOrgId: "",
showOnboardingForm: false,
};
}
componentDidMount() {
PerformanceTracker.stopTracking(PerformanceTransactionName.LOGIN_CLICK);
PerformanceTracker.stopTracking(PerformanceTransactionName.SIGN_UP);
this.props.getAllApplication();
this.props.setHeaderMetaData(true, true);
}
componentWillUnmount() {
this.props.setHeaderMetaData(false, false);
}
public render() {
return (
);
}
}
const mapStateToProps = (state: AppState) => ({
applicationList: getApplicationList(state),
isFetchingApplications: getIsFetchingApplications(state),
isCreatingApplication: getIsCreatingApplication(state),
createApplicationError: getCreateApplicationError(state),
deletingApplication: getIsDeletingApplication(state),
duplicatingApplication: getIsDuplicatingApplication(state),
userOrgs: getUserApplicationsOrgsList(state),
currentUser: getCurrentUser(state),
searchKeyword: getApplicationSearchKeyword(state),
});
const mapDispatchToProps = (dispatch: any) => ({
getAllApplication: () => {
dispatch({ type: ReduxActionTypes.GET_ALL_APPLICATION_INIT });
},
searchApplications: (keyword: string) => {
dispatch({
type: ReduxActionTypes.SEARCH_APPLICATIONS,
payload: {
keyword,
},
});
},
setHeaderMetaData: (
hideHeaderShadow: boolean,
showHeaderSeparator: boolean,
) => {
dispatch(setHeaderMeta(hideHeaderShadow, showHeaderSeparator));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Applications);