import React, { Component, useCallback, useContext, useEffect, useRef, useState, } from "react"; import styled, { ThemeContext } from "styled-components"; import { connect, useDispatch, useSelector } from "react-redux"; import MediaQuery from "react-responsive"; import { useLocation } from "react-router-dom"; import { AppState } from "@appsmith/reducers"; import { Classes as BlueprintClasses } from "@blueprintjs/core"; import { thinScrollbar, truncateTextUsingEllipsis, } from "constants/DefaultTheme"; import { getApplicationList, getApplicationSearchKeyword, getCreateApplicationError, getIsCreatingApplication, getIsDeletingApplication, getIsDuplicatingApplication, getIsFetchingApplications, getIsSavingWorkspaceInfo, getUserApplicationsWorkspaces, getUserApplicationsWorkspacesList, } from "selectors/applicationSelectors"; import { ApplicationPayload, ReduxActionTypes, } from "@appsmith/constants/ReduxActionConstants"; import PageWrapper from "pages/common/PageWrapper"; import SubHeader from "pages/common/SubHeader"; import ApplicationCard from "./ApplicationCard"; import WorkspaceInviteUsersForm from "@appsmith/pages/workspace/WorkspaceInviteUsersForm"; import FormDialogComponent from "components/editorComponents/form/FormDialogComponent"; import { User } from "constants/userConstants"; import { getCurrentUser } from "selectors/usersSelectors"; import { CREATE_WORKSPACE_FORM_NAME } from "@appsmith/constants/forms"; import { DropdownOnSelectActions, getOnSelectAction, } from "pages/common/CustomizedDropdown/dropdownHelpers"; import { AppIconCollection, Button, Category, Classes, EditableText, EditInteractionKind, DialogComponent as Dialog, Icon, IconName, IconSize, Menu, MenuItem, notEmptyValidator, SavingState, Size, Text, TextType, } from "design-system"; import { duplicateApplication, updateApplication, } from "actions/applicationActions"; import { Position } from "@blueprintjs/core/lib/esm/common/position"; import { UpdateApplicationPayload } from "api/ApplicationApi"; import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; import { loadingUserWorkspaces } from "./ApplicationLoaders"; import { creatingApplicationMap } from "@appsmith/reducers/uiReducers/applicationsReducer"; import { deleteWorkspace, saveWorkspace, } from "@appsmith/actions/workspaceActions"; import { leaveWorkspace } from "actions/userActions"; import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper"; import NoSearchImage from "assets/images/NoSearchResult.svg"; import { getNextEntityName, getRandomPaletteColor } from "utils/AppsmithUtils"; import { createWorkspaceSubmitHandler } from "@appsmith/pages/workspace/helpers"; import ImportApplicationModal from "./ImportApplicationModal"; import { createMessage, INVITE_USERS_MESSAGE, INVITE_USERS_PLACEHOLDER, NO_APPS_FOUND, SEARCH_APPS, WORKSPACES_HEADING, } from "@appsmith/constants/messages"; import { ReactComponent as NoAppsFoundIcon } from "assets/svg/no-apps-icon.svg"; import { setHeaderMeta } from "actions/themeActions"; import SharedUserList from "pages/common/SharedUserList"; import { useIsMobileDevice } from "utils/hooks/useDeviceDetect"; import { Indices } from "constants/Layers"; import GitSyncModal from "pages/Editor/gitSync/GitSyncModal"; import DisconnectGitModal from "pages/Editor/gitSync/DisconnectGitModal"; import ReconnectDatasourceModal from "pages/Editor/gitSync/ReconnectDatasourceModal"; import LeftPaneBottomSection from "pages/Home/LeftPaneBottomSection"; import { MOBILE_MAX_WIDTH } from "constants/AppConstants"; import urlBuilder from "entities/URLRedirect/URLAssembly"; import RepoLimitExceededErrorModal from "../Editor/gitSync/RepoLimitExceededErrorModal"; import { resetEditorRequest } from "actions/initActions"; import { hasCreateNewAppPermission, isPermitted, PERMISSION_TYPE, } from "@appsmith/utils/permissionHelpers"; import { getTenantPermissions } from "@appsmith/selectors/tenantSelectors"; const WorkspaceDropDown = styled.div<{ isMobile?: boolean }>` display: flex; padding: ${(props) => (props.isMobile ? `10px 16px` : `10px 10px`)}; font-size: ${(props) => props.theme.fontSizes[1]}px; justify-content: space-between; align-items: center; ${({ isMobile }) => isMobile && ` position: sticky; top: 0; background-color: #fff; z-index: ${Indices.Layer8}; `} `; const ApplicationCardsWrapper = styled.div<{ isMobile?: boolean }>` display: flex; flex-wrap: wrap; gap: ${({ isMobile }) => (isMobile ? 12 : 20)}px; font-size: ${(props) => props.theme.fontSizes[4]}px; padding: ${({ isMobile }) => (isMobile ? `10px 16px` : `10px`)}; `; const WorkspaceSection = styled.div<{ isMobile?: boolean }>` margin-bottom: ${({ isMobile }) => (isMobile ? `8` : `40`)}px; `; const PaddingWrapper = styled.div<{ isMobile?: boolean }>` 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; } } ${({ isMobile }) => isMobile && ` width: 100% !important; `} `; 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<{ isMobile?: boolean }>` padding-right: ${(props) => props.theme.homePage.leftPane.rightMargin}px; padding-top: 16px; ${({ isMobile }) => isMobile && ` margin-left: 0; width: 100%; padding: 0; `} `; const ItemWrapper = styled.div` padding: 9px 15px; `; const StyledIcon = styled(Icon)` margin-right: 11px; `; const WorkspaceShareUsers = 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); display: flex; flex-direction: column; `; function LeftPaneSection(props: { heading: string; children?: any; isFetchingApplications: boolean; }) { return ( {/* */} {props.children} ); } const StyledAnchor = styled.a` position: relative; top: -24px; `; const WorkpsacesNavigator = styled.div` overflow: auto; ${thinScrollbar}; /* padding-bottom: 160px; */ `; 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 WorkspaceMenuItem({ isFetchingApplications, selected, workspace, }: any) { const menuRef = useRef(null); useEffect(() => { if (selected) { menuRef.current?.scrollIntoView({ behavior: "smooth" }); menuRef.current?.click(); } }, [selected]); return ( ); } const submitCreateWorkspaceForm = async (data: any, dispatch: any) => { const result = await createWorkspaceSubmitHandler(data, dispatch); return result; }; function LeftPane() { const dispatch = useDispatch(); const fetchedUserWorkspaces = useSelector(getUserApplicationsWorkspaces); const isFetchingApplications = useSelector(getIsFetchingApplications); const isMobile = useIsMobileDevice(); let userWorkspaces; if (!isFetchingApplications) { userWorkspaces = fetchedUserWorkspaces; } else { userWorkspaces = loadingUserWorkspaces as any; } const tenantPermissions = useSelector(getTenantPermissions); const canCreateWorkspace = isPermitted( tenantPermissions, PERMISSION_TYPE.CREATE_WORKSPACE, ); const location = useLocation(); const urlHash = location.hash.slice(1); if (isMobile) return null; return ( {!isFetchingApplications && fetchedUserWorkspaces && canCreateWorkspace && ( submitCreateWorkspaceForm( { name: getNextEntityName( "Untitled workspace ", fetchedUserWorkspaces.map( (el: any) => el.workspace.name, ), ), }, dispatch, ) } text={CREATE_WORKSPACE_FORM_NAME} /> )} {userWorkspaces && userWorkspaces.map((workspace: any) => ( ))} ); } const CreateNewLabel = styled(Text)` margin-top: 18px; `; const WorkspaceNameElement = styled(Text)<{ isMobile?: boolean }>` max-width: ${({ isMobile }) => (isMobile ? 220 : 500)}px; ${truncateTextUsingEllipsis} `; const WorkspaceNameHolder = styled(Text)` display: flex; align-items: center; `; const WorkspaceNameWrapper = styled.div<{ disabled?: boolean }>` ${(props) => { const color = props.disabled ? props.theme.colors.applications.workspaceColor : props.theme.colors.applications.hover.workspaceColor[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 WorkspaceRename = styled(EditableText)` padding: 0 2px; `; const NoSearchResultImg = styled.img` margin: 1em; `; const ApplicationsWrapper = styled.div<{ isMobile: boolean }>` height: calc(100vh - ${(props) => props.theme.homePage.search.height - 40}px); overflow: auto; 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; ${({ isMobile }) => isMobile && ` margin-left: 0; width: 100%; padding: 0; `} `; function ApplicationsSection(props: any) { const enableImportExport = true; const dispatch = useDispatch(); const theme = useContext(ThemeContext); const isSavingWorkspaceInfo = useSelector(getIsSavingWorkspaceInfo); const isFetchingApplications = useSelector(getIsFetchingApplications); const userWorkspaces = useSelector(getUserApplicationsWorkspacesList); const creatingApplicationMap = useSelector(getIsCreatingApplication); const currentUser = useSelector(getCurrentUser); const isMobile = useIsMobileDevice(); const deleteApplication = (applicationId: string) => { if (applicationId && applicationId.length > 0) { dispatch({ type: ReduxActionTypes.DELETE_APPLICATION_INIT, payload: { applicationId, }, }); } }; const [warnLeavingWorkspace, setWarnLeavingWorkspace] = useState(false); const [warnDeleteWorkspace, setWarnDeleteWorkspace] = useState(false); const [workspaceToOpenMenu, setWorkspaceToOpenMenu] = useState( null, ); const updateApplicationDispatch = ( id: string, data: UpdateApplicationPayload, ) => { dispatch(updateApplication(id, data)); }; useEffect(() => { // Clears URL params cache urlBuilder.resetURLParams(); }, []); const duplicateApplicationDispatch = (applicationId: string) => { dispatch(duplicateApplication(applicationId)); }; const [selectedWorkspaceId, setSelectedWorkspaceId] = useState< string | undefined >(); const [ selectedWorkspaceIdForImportApplication, setSelectedWorkspaceIdForImportApplication, ] = useState(); const Form: any = WorkspaceInviteUsersForm; const leaveWS = (workspaceId: string) => { setWarnLeavingWorkspace(false); setWorkspaceToOpenMenu(null); dispatch(leaveWorkspace(workspaceId)); }; const handleDeleteWorkspace = useCallback( (workspaceId: string) => { setWarnDeleteWorkspace(false); setWorkspaceToOpenMenu(null); dispatch(deleteWorkspace(workspaceId)); }, [dispatch], ); const WorkspaceNameChange = (newName: string, workspaceId: string) => { dispatch( saveWorkspace({ id: workspaceId as string, name: newName, }), ); }; function WorkspaceMenuTarget(props: { workspaceName: string; disabled?: boolean; workspaceSlug: string; }) { const { disabled, workspaceName, workspaceSlug } = props; return ( {workspaceName} ); } const createNewApplication = ( applicationName: string, workspaceId: 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, workspaceId, icon, color, }, }); }; let updatedWorkspaces; if (!isFetchingApplications) { updatedWorkspaces = userWorkspaces; } else { updatedWorkspaces = loadingUserWorkspaces as any; } let workspacesListComponent; if ( !isFetchingApplications && props.searchKeyword && props.searchKeyword.trim().length > 0 && updatedWorkspaces.length === 0 ) { workspacesListComponent = ( {createMessage(NO_APPS_FOUND)} ); } else { workspacesListComponent = updatedWorkspaces.map( (workspaceObject: any, index: number) => { const { applications, workspace } = workspaceObject; const hasManageWorkspacePermissions = isPermitted( workspace.userPermissions, PERMISSION_TYPE.MANAGE_WORKSPACE, ); const hasCreateNewApplicationPermission = hasCreateNewAppPermission(workspace.userPermissions) && !isMobile; const onClickAddNewButton = (workspaceId: string) => { if ( Object.entries(creatingApplicationMap).length === 0 || (creatingApplicationMap && !creatingApplicationMap[workspaceId]) ) { createNewApplication( getNextEntityName( "Untitled application ", applications.map((el: any) => el.name), ), workspaceId, ); } }; return ( {(currentUser || isFetchingApplications) && WorkspaceMenuTarget({ workspaceName: workspace.name, workspaceSlug: workspace.id, })} {hasManageWorkspacePermissions && ( setSelectedWorkspaceId("")} title={`Invite Users to ${workspace.name}`} >
)} {selectedWorkspaceIdForImportApplication && ( setSelectedWorkspaceIdForImportApplication("")} workspaceId={selectedWorkspaceIdForImportApplication} /> )} {!isFetchingApplications && ( {isPermitted( workspace.userPermissions, PERMISSION_TYPE.INVITE_USER_TO_WORKSPACE, ) && !isMobile && ( } workspaceId={workspace.id} /> )} {hasCreateNewApplicationPermission && !isFetchingApplications && applications.length !== 0 && (