diff --git a/app/client/docker/templates/nginx-linux.conf.template b/app/client/docker/templates/nginx-linux.conf.template index 7142c4355e..73636fb5fb 100644 --- a/app/client/docker/templates/nginx-linux.conf.template +++ b/app/client/docker/templates/nginx-linux.conf.template @@ -33,6 +33,9 @@ server { sub_filter __APPSMITH_TNC_PP__ '${APPSMITH_TNC_PP}'; sub_filter __APPSMITH_SENTRY_RELEASE__ '${APPSMITH_SENTRY_RELEASE}'; sub_filter __APPSMITH_SENTRY_ENVIRONMENT__ '${APPSMITH_SENTRY_ENVIRONMENT}'; + sub_filter __APPSMITH_VERSION_ID__ '${APPSMITH_VERSION_ID}'; + sub_filter __APPSMITH_VERSION_RELEASE_DATE__ '${APPSMITH_VERSION_RELEASE_DATE}'; + sub_filter __APPSMITH_INTERCOM_APP_ID__ '${APPSMITH_INTERCOM_APP_ID}'; } location /f { @@ -97,6 +100,9 @@ server { sub_filter __APPSMITH_TNC_PP__ '${APPSMITH_TNC_PP}'; sub_filter __APPSMITH_SENTRY_RELEASE__ '${APPSMITH_SENTRY_RELEASE}'; sub_filter __APPSMITH_SENTRY_ENVIRONMENT__ '${APPSMITH_SENTRY_ENVIRONMENT}'; + sub_filter __APPSMITH_VERSION_ID__ '${APPSMITH_VERSION_ID}'; + sub_filter __APPSMITH_VERSION_RELEASE_DATE__ '${APPSMITH_VERSION_RELEASE_DATE}'; + sub_filter __APPSMITH_INTERCOM_APP_ID__ '${APPSMITH_INTERCOM_APP_ID}'; } location /f { diff --git a/app/client/docker/templates/nginx-mac.conf.template b/app/client/docker/templates/nginx-mac.conf.template index 39ec75fd7b..5bf1e8081b 100644 --- a/app/client/docker/templates/nginx-mac.conf.template +++ b/app/client/docker/templates/nginx-mac.conf.template @@ -33,6 +33,9 @@ server { sub_filter __APPSMITH_TNC_PP__ '${APPSMITH_TNC_PP}'; sub_filter __APPSMITH_SENTRY_RELEASE__ '${APPSMITH_SENTRY_RELEASE}'; sub_filter __APPSMITH_SENTRY_ENVIRONMENT__ '${APPSMITH_SENTRY_ENVIRONMENT}'; + sub_filter __APPSMITH_VERSION_ID__ '${APPSMITH_VERSION_ID}'; + sub_filter __APPSMITH_VERSION_RELEASE_DATE__ '${APPSMITH_VERSION_RELEASE_DATE}'; + sub_filter __APPSMITH_INTERCOM_APP_ID__ '${APPSMITH_INTERCOM_APP_ID}'; } location /f { @@ -98,6 +101,9 @@ server { sub_filter __APPSMITH_TNC_PP__ '${APPSMITH_TNC_PP}'; sub_filter __APPSMITH_SENTRY_RELEASE__ '${APPSMITH_SENTRY_RELEASE}'; sub_filter __APPSMITH_SENTRY_ENVIRONMENT__ '${APPSMITH_SENTRY_ENVIRONMENT}'; + sub_filter __APPSMITH_VERSION_ID__ '${APPSMITH_VERSION_ID}'; + sub_filter __APPSMITH_VERSION_RELEASE_DATE__ '${APPSMITH_VERSION_RELEASE_DATE}'; + sub_filter __APPSMITH_INTERCOM_APP_ID__ '${APPSMITH_INTERCOM_APP_ID}'; } diff --git a/app/client/public/index.html b/app/client/public/index.html index 1df3d5fcc9..6b778f499f 100755 --- a/app/client/public/index.html +++ b/app/client/public/index.html @@ -76,9 +76,20 @@ logLevel: CONFIG_LOG_LEVEL_INDEX > -1 ? LOG_LEVELS[CONFIG_LOG_LEVEL_INDEX] : LOG_LEVELS[1], google: parseConfig("__APPSMITH_GOOGLE_MAPS_API_KEY__"), cloudHosting: parseConfig("__APPSMITH_CLOUD_HOSTING__").length > 0, - enableTNCPP: parseConfig("__APPSMITH_TNC_PP__").length > 0 + enableTNCPP: parseConfig("__APPSMITH_TNC_PP__").length > 0, + appVersion: { + id: parseConfig("__APPSMITH_VERSION_ID__"), + releaseDate: parseConfig("__APPSMITH_VERSION_RELEASE_DATE__") + }, + intercomAppID: parseConfig("__APPSMITH_INTERCOM_APP_ID__"), }; + diff --git a/app/client/src/assets/icons/help/discord.svg b/app/client/src/assets/icons/help/discord.svg new file mode 100644 index 0000000000..4613aa9ae8 --- /dev/null +++ b/app/client/src/assets/icons/help/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/client/src/assets/icons/help/document.svg b/app/client/src/assets/icons/help/document.svg index 60c75f6eaf..e0ba8bc5cb 100644 --- a/app/client/src/assets/icons/help/document.svg +++ b/app/client/src/assets/icons/help/document.svg @@ -1,9 +1,7 @@ - - diff --git a/app/client/src/assets/icons/help/github-icon.svg b/app/client/src/assets/icons/help/github-icon.svg new file mode 100644 index 0000000000..3f18b45241 --- /dev/null +++ b/app/client/src/assets/icons/help/github-icon.svg @@ -0,0 +1,27 @@ + + + + + + diff --git a/app/client/src/components/designSystems/appsmith/help/DocumentationSearch.tsx b/app/client/src/components/designSystems/appsmith/help/DocumentationSearch.tsx index f0d08555aa..d65df0a592 100644 --- a/app/client/src/components/designSystems/appsmith/help/DocumentationSearch.tsx +++ b/app/client/src/components/designSystems/appsmith/help/DocumentationSearch.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { SyntheticEvent } from "react"; import algoliasearch from "algoliasearch/lite"; import { InstantSearch, @@ -8,25 +8,29 @@ import { Configure, PoweredBy, } from "react-instantsearch-dom"; - import "instantsearch.css/themes/algolia.css"; - -import PropTypes from "prop-types"; -import { Icon } from "@blueprintjs/core"; -import { useDispatch, useSelector } from "react-redux"; -import { - setHelpModalVisibility, - setHelpDefaultRefinement, -} from "actions/helpActions"; +import { connect } from "react-redux"; import styled from "styled-components"; import { HelpIcons } from "icons/HelpIcons"; import { HelpBaseURL } from "constants/HelpConstants"; import { getDefaultRefinement } from "selectors/helpSelectors"; import { getAppsmithConfigs } from "configs"; -const { algolia } = getAppsmithConfigs(); +import { AppState } from "reducers"; +import { + setHelpDefaultRefinement, + setHelpModalVisibility, +} from "actions/helpActions"; +import { Icon } from "@blueprintjs/core"; +import moment from "moment"; + +const { algolia, appVersion, cloudHosting } = getAppsmithConfigs(); const searchClient = algoliasearch(algolia.apiId, algolia.apiKey); + const OenLinkIcon = HelpIcons.OPEN_LINK; const DocumentIcon = HelpIcons.DOCUMENT; +const GithubIcon = HelpIcons.GITHUB; +const ChatIcon = HelpIcons.CHAT; +const DiscordIcon = HelpIcons.DISCORD; const StyledOpenLinkIcon = styled(OenLinkIcon)` position: absolute; @@ -47,43 +51,78 @@ const StyledDocumentIcon = styled(DocumentIcon)` margin-top: 1px; position: absolute; `; -function Hit(props: any) { + +const StyledGithubIcon = styled(GithubIcon)` + margin-left: 14px; + margin-right: 10.8px; + margin-top: 1px; + position: absolute; +`; + +const StyledChatIcon = styled(ChatIcon)` + &&& { + margin-left: 14px; + margin-right: 10.8px; + margin-top: 1px; + position: absolute; + } +`; + +const StyledDiscordIcon = styled(DiscordIcon)` + &&& { + margin-left: 12px; + margin-right: 10.8px; + margin-top: 1px; + position: absolute; + } +`; + +const Hit = (props: { hit: { path: string } }) => { return (
{ - window.open( - (props.hit.path as string).replace("master", HelpBaseURL), - "_blank", - ); + window.open(props.hit.path.replace("master", HelpBaseURL), "_blank"); }} >
- + + />
); -} - -Hit.propTypes = { - hit: PropTypes.object.isRequired, }; -const Header = styled.div` - position: absolute; - width: 100%; - border-top-right-radius: 3px; - border-top-left-radius: 3px; -`; +const DefaultHelpMenuItem = (props: { + item: { label: string; link?: string; id?: string; icon: React.ReactNode }; + onSelect: Function; +}) => { + return ( +
  • +
    { + if (props.item.link) window.open(props.item.link, "_blank"); + props.onSelect(); + }} + > +
    + {props.item.icon} + {props.item.label} + +
    +
    +
  • + ); +}; const SearchContainer = styled.div` height: 100%; @@ -93,7 +132,7 @@ const SearchContainer = styled.div` position: relative; height: 30px; margin: 14px; - margin-top: 0; + margin-top: 10px; } .ais-SearchBox-form { @@ -107,8 +146,6 @@ const SearchContainer = styled.div` } .ais-Hits { - margin-top: 86px; - height: calc(100% - 86px); overflow: auto; border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; @@ -205,10 +242,19 @@ const SearchContainer = styled.div` } `; +const Header = styled.div` + padding: 10px 0; + position: absolute; + width: 100%; + border-top-right-radius: 3px; + border-top-left-radius: 3px; + height: 50px; +`; + const StyledPoweredBy = styled(PoweredBy)` position: absolute; right: 21px; - bottom: 23px; + top: 30px; z-index: 1; .ais-PoweredBy-text { @@ -216,73 +262,155 @@ const StyledPoweredBy = styled(PoweredBy)` } `; -export default function DocumentationSearch(props: { hitsPerPage: number }) { - const dispatch = useDispatch(); - const defaultRefinement = useSelector(getDefaultRefinement); - if (!algolia.enabled) return null; - return ( - - { - dispatch(setHelpModalVisibility(false)); - dispatch(setHelpDefaultRefinement("")); - }} - > -
    +const HelpContainer = styled.div` + height: 100%; + position: relative; + display: flex; + flex-direction: column; +`; + +const HelpFooter = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + border-top: 1px solid rgba(255, 255, 255, 0.1); + padding: 5px 10px; + height: 30px; + color: rgba(255, 255, 255, 0.7); +`; + +const HelpBody = styled.div` + padding-top: 60px; + flex: 5; +`; + +type Props = { hitsPerPage: number; defaultRefinement: string; dispatch: any }; +type State = { showResults: boolean }; + +type HelpItem = { + label: string; + link?: string; + id?: string; + icon: React.ReactNode; +}; + +const HELP_MENU_ITEMS: HelpItem[] = [ + { + icon: , + label: "Documentation", + link: "https://docs.appsmith.com/", + }, + { + icon: , + label: "Report a bug", + link: "https://github.com/appsmithorg/appsmith/issues/new/choose", + }, + { + icon: , + label: "Chat with us", + link: "https://github.com/appsmithorg/appsmith/discussions", + }, + { + icon: , + label: "Join our Discord", + link: "https://discord.gg/rBTTVJp", + }, +]; + +if (cloudHosting) { + HELP_MENU_ITEMS[2] = { + icon: , + label: "Chat with us", + id: "intercom-trigger", + }; +} + +class DocumentationSearch extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + showResults: props.defaultRefinement.length > 0, + }; + } + onSearchValueChange = (event: SyntheticEvent) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + const value = event.target.value; + if (value === "" && this.state.showResults) { + this.setState({ + showResults: false, + }); + } else if (value !== "" && !this.state.showResults) { + this.setState({ + showResults: true, + }); + } + }; + handleClose = () => { + this.props.dispatch(setHelpModalVisibility(false)); + this.props.dispatch(setHelpDefaultRefinement("")); + }; + render() { + if (!algolia.enabled) return null; + return ( + + - -
    -

    - - Documentation - -

    - - -
    - - + + +
    + + +
    + + {this.state.showResults ? ( + + ) : ( +
      + {HELP_MENU_ITEMS.map(item => ( + + ))} +
    + )} +
    + {appVersion.id && ( + + Appsmith {appVersion.id} + Released {moment(appVersion.releaseDate).fromNow()} + + )} +
    -
    -
    - ); + + ); + } } + +const mapStateToProps = (state: AppState) => ({ + defaultRefinement: getDefaultRefinement(state), +}); + +export default connect(mapStateToProps)(DocumentationSearch); diff --git a/app/client/src/components/designSystems/appsmith/help/HelpModal.tsx b/app/client/src/components/designSystems/appsmith/help/HelpModal.tsx index b00d79f189..710a6bcd01 100644 --- a/app/client/src/components/designSystems/appsmith/help/HelpModal.tsx +++ b/app/client/src/components/designSystems/appsmith/help/HelpModal.tsx @@ -1,12 +1,6 @@ -import React, { useContext } from "react"; +import React, { SyntheticEvent } from "react"; import DocumentationSearch from "components/designSystems/appsmith/help/DocumentationSearch"; - -import { useSelector } from "store"; -import { useDispatch } from "react-redux"; -import { - getHelpModalOpen, - getHelpModalDimensions, -} from "selectors/helpSelectors"; +import { getHelpModalOpen } from "selectors/helpSelectors"; import { setHelpDefaultRefinement, setHelpModalVisibility, @@ -14,17 +8,20 @@ import { import styled from "styled-components"; import { theme } from "constants/DefaultTheme"; import ModalComponent from "components/designSystems/blueprint/ModalComponent"; -import { LayersContext } from "constants/Layers"; import { HelpIcons } from "icons/HelpIcons"; import { getAppsmithConfigs } from "configs"; +import { LayersContext } from "constants/Layers"; +import { connect } from "react-redux"; +import { AppState } from "reducers"; + const { algolia } = getAppsmithConfigs(); -const HelpButton = styled.div<{ +const HelpButton = styled.button<{ highlight: boolean; layer: number; }>` &&&&& { position: absolute; - bottom: 46px; + bottom: 27px; right: 27px; z-index: ${props => props.layer}; background: ${props => @@ -47,48 +44,66 @@ const HelpButton = styled.div<{ } `; +const MODAL_WIDTH = 240; +const MODAL_HEIGHT = 210; +const MODAL_BOTTOM_DISTANCE = 45; +const MODAL_RIGHT_DISTANCE = 30; + const HelpIcon = HelpIcons.HELP_ICON; -export function HelpModal() { - const isHelpModalOpen = useSelector(getHelpModalOpen); - const helpDimensions = useSelector(getHelpModalDimensions); - const helpModalOpen = useSelector(getHelpModalOpen); - const dispatch = useDispatch(); - const layers = useContext(LayersContext); +type Props = { + isHelpModalOpen: boolean; + dispatch: any; +}; - return ( - <> - { - dispatch(setHelpModalVisibility(false)); - dispatch(setHelpDefaultRefinement("")); - }} - isOpen={isHelpModalOpen} - zIndex={layers.help} - > - - - {algolia.enabled && ( - { - dispatch(setHelpModalVisibility(!helpModalOpen)); +class HelpModal extends React.Component { + static contextType = LayersContext; + render() { + const { dispatch, isHelpModalOpen } = this.props; + const layers = this.context; + + return ( + <> + ) => { + dispatch(setHelpModalVisibility(false)); + dispatch(setHelpDefaultRefinement("")); + event.stopPropagation(); + event.preventDefault(); }} + isOpen={isHelpModalOpen} + zIndex={layers.help} > - - - )} - - ); + + + {algolia.enabled && ( + { + dispatch(setHelpModalVisibility(!isHelpModalOpen)); + }} + > + + + )} + + ); + } } + +const mapStateToProps = (state: AppState) => ({ + isHelpModalOpen: getHelpModalOpen(state), +}); + +export default connect(mapStateToProps)(HelpModal); diff --git a/app/client/src/configs/index.ts b/app/client/src/configs/index.ts index 24ba7f7a3a..5c9eecd009 100644 --- a/app/client/src/configs/index.ts +++ b/app/client/src/configs/index.ts @@ -24,10 +24,16 @@ type INJECTED_CONFIGS = { indexName: string; }; logLevel: "debug" | "error"; + appVersion: { + id: string; + releaseDate: string; + }; + intercomAppID: string; }; declare global { interface Window { APPSMITH_FEATURE_CONFIGS: INJECTED_CONFIGS; + Intercom: any; } } @@ -79,6 +85,11 @@ const getConfigsFromEnvVars = (): INJECTED_CONFIGS => { cloudHosting: process.env.REACT_APP_CLOUD_HOSTING ? process.env.REACT_APP_CLOUD_HOSTING.length > 0 : false, + appVersion: { + id: process.env.REACT_APP_VERSION_ID || "", + releaseDate: process.env.REACT_APP_VERSION_RELEASE_DATE || "", + }, + intercomAppID: process.env.REACT_APP_INTERCOM_APP_ID || "", }; }; @@ -192,5 +203,8 @@ export const getAppsmithConfigs = (): AppsmithUIConfigs => { ), logLevel: ENV_CONFIG.logLevel || APPSMITH_FEATURE_CONFIGS.logLevel, enableTNCPP: ENV_CONFIG.enableTNCPP || APPSMITH_FEATURE_CONFIGS.enableTNCPP, + appVersion: ENV_CONFIG.appVersion || APPSMITH_FEATURE_CONFIGS.appVersion, + intercomAppID: + ENV_CONFIG.intercomAppID || APPSMITH_FEATURE_CONFIGS.intercomAppID, }; }; diff --git a/app/client/src/configs/types.ts b/app/client/src/configs/types.ts index 7fbd8178c3..a638126994 100644 --- a/app/client/src/configs/types.ts +++ b/app/client/src/configs/types.ts @@ -61,4 +61,9 @@ export type AppsmithUIConfigs = { featureFlag?: FeatureFlagConfig; logLevel: LogLevelDesc; + appVersion: { + id: string; + releaseDate: string; + }; + intercomAppID: string; }; diff --git a/app/client/src/icons/HelpIcons.tsx b/app/client/src/icons/HelpIcons.tsx index 2e477bee0e..1f21b10abf 100644 --- a/app/client/src/icons/HelpIcons.tsx +++ b/app/client/src/icons/HelpIcons.tsx @@ -3,6 +3,9 @@ import { IconProps, IconWrapper } from "constants/IconConstants"; import { ReactComponent as OpenLinkIcon } from "assets/icons/help/openlink.svg"; import { ReactComponent as DocumentIcon } from "assets/icons/help/document.svg"; import { ReactComponent as HelpIcon } from "assets/icons/help/help.svg"; +import { ReactComponent as GithubIcon } from "assets/icons/help/github-icon.svg"; +import { ReactComponent as DiscordIcon } from "assets/icons/help/discord.svg"; +import { Icon } from "@blueprintjs/core"; /* eslint-disable react/display-name */ @@ -24,6 +27,21 @@ export const HelpIcons: { ), + GITHUB: (props: IconProps) => ( + + + + ), + CHAT: (props: IconProps) => ( + + + + ), + DISCORD: (props: IconProps) => ( + + + + ), }; export type HelpIconName = keyof typeof HelpIcons; diff --git a/app/client/src/pages/Editor/EditorHeader.tsx b/app/client/src/pages/Editor/EditorHeader.tsx index e8c77a8adf..f8340dc398 100644 --- a/app/client/src/pages/Editor/EditorHeader.tsx +++ b/app/client/src/pages/Editor/EditorHeader.tsx @@ -12,7 +12,7 @@ import AppInviteUsersForm from "pages/organization/AppInviteUsersForm"; import Button from "components/editorComponents/Button"; import StyledHeader from "components/designSystems/appsmith/StyledHeader"; import AnalyticsUtil from "utils/AnalyticsUtil"; -import { HelpModal } from "components/designSystems/appsmith/help/HelpModal"; +import HelpModal from "components/designSystems/appsmith/help/HelpModal"; import { FormDialogComponent } from "components/editorComponents/form/FormDialogComponent"; import { Colors } from "constants/Colors"; import AppsmithLogo from "assets/images/appsmith_logo_white.png"; @@ -257,7 +257,6 @@ export const EditorHeader = (props: EditorHeaderProps) => { /> - {} ); diff --git a/app/client/src/pages/Editor/index.tsx b/app/client/src/pages/Editor/index.tsx index bb5f02e457..971d3f15cf 100644 --- a/app/client/src/pages/Editor/index.tsx +++ b/app/client/src/pages/Editor/index.tsx @@ -36,6 +36,11 @@ import { } from "constants/Explorer"; import history from "utils/history"; import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper"; +import { getAppsmithConfigs } from "configs"; +import { getCurrentUser } from "selectors/usersSelectors"; +import { User } from "constants/userConstants"; + +const { cloudHosting, intercomAppID } = getAppsmithConfigs(); type EditorProps = { currentApplicationId?: string; @@ -45,6 +50,7 @@ type EditorProps = { isEditorLoading: boolean; isEditorInitialized: boolean; errorPublishing: boolean; + user?: User; }; type Props = EditorProps & RouteComponentProps; @@ -84,6 +90,7 @@ class Editor extends Component { }; componentDidMount() { + const { user } = this.props; editorInitializer().then(() => { this.setState({ registered: true }); }); @@ -91,8 +98,19 @@ class Editor extends Component { if (applicationId && pageId) { this.props.initEditor(applicationId, pageId); } + if (cloudHosting) { + window.Intercom("boot", { + // eslint-disable-next-line @typescript-eslint/camelcase + app_id: intercomAppID, + // eslint-disable-next-line @typescript-eslint/camelcase + custom_launcher_selector: "#intercom-trigger", + name: user?.username, + email: user?.email, + }); + } } componentDidUpdate(previously: Props) { + if (cloudHosting) window.Intercom("update"); if ( previously.isPublishing && !(this.props.isPublishing || this.props.errorPublishing) @@ -184,6 +202,7 @@ const mapStateToProps = (state: AppState) => ({ isPublishing: getIsPublishingApplication(state), isEditorLoading: getIsEditorLoading(state), isEditorInitialized: getIsEditorInitialized(state), + user: getCurrentUser(state), }); const mapDispatchToProps = (dispatch: any) => { diff --git a/app/client/src/reducers/uiReducers/helpReducer.ts b/app/client/src/reducers/uiReducers/helpReducer.ts index 0a5a5971a9..a6cba17d6e 100644 --- a/app/client/src/reducers/uiReducers/helpReducer.ts +++ b/app/client/src/reducers/uiReducers/helpReducer.ts @@ -4,8 +4,6 @@ import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; const initialState: HelpReduxState = { url: "", modalOpen: false, - height: 243, - width: 269, defaultRefinement: "", }; @@ -27,8 +25,6 @@ const helpReducer = createReducer(initialState, { export interface HelpReduxState { url: string; modalOpen: boolean; - height: number; - width: number; defaultRefinement: string; } diff --git a/app/client/src/selectors/helpSelectors.tsx b/app/client/src/selectors/helpSelectors.tsx index b9e04e752f..4cdb67bf60 100644 --- a/app/client/src/selectors/helpSelectors.tsx +++ b/app/client/src/selectors/helpSelectors.tsx @@ -1,18 +1,8 @@ import { AppState } from "reducers"; -export const getHelpUrl = (state: AppState): string => state.ui.help.url; export const getHelpModalOpen = (state: AppState): boolean => state.ui.help.modalOpen; -export const getHelpModalDimensions = ( - state: AppState, -): { height: number; width: number } => { - return { - height: state.ui.help.height, - width: state.ui.help.width, - }; -}; - export const getDefaultRefinement = (state: AppState): string => { return state.ui.help.defaultRefinement || ""; }; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationService.java index b682273306..87c2913c01 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationService.java @@ -19,6 +19,8 @@ public interface OrganizationService extends CrudService { Mono getNextUniqueSlug(String initialSlug); + Mono createPersonal(Organization organization, User user); + Mono create(Organization organization, User user); Mono findById(String id, AclPermission permission); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationServiceImpl.java index ba0110e16a..44ee06a410 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationServiceImpl.java @@ -46,9 +46,7 @@ import static java.util.stream.Collectors.toMap; @Service public class OrganizationServiceImpl extends BaseService implements OrganizationService { - private final OrganizationRepository repository; private final SettingService settingService; - private final GroupService groupService; private final PluginRepository pluginRepository; private final SessionUserService sessionUserService; private final UserOrganizationService userOrganizationService; @@ -63,16 +61,13 @@ public class OrganizationServiceImpl extends BaseService initialSlug + (max == 0 ? "" : (max + 1))); } + /** + * Creates the given organization as a personal organization for the given user. That is, the organization's name + * is changed to "[username]'s Personal Organization" and then created. The current value of the organization name + * is discarded. + * @param organization Organization object to be created. + * @param user User to whom this organization will belong to, as a personal organization. + * @return Publishes the saved organization. + */ + @Override + public Mono createPersonal(final Organization organization, User user) { + organization.setName(user.computeFirstName() + "'s Personal Organization"); + return create(organization, user); + } + /** * This function does the following: * 1. Creates the organization for the user @@ -112,10 +121,11 @@ public class OrganizationServiceImpl extends BaseService create(Organization organization, User user) { if (organization == null) { return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ORGANIZATION)); @@ -123,9 +133,7 @@ public class OrganizationServiceImpl extends BaseService policy.getPermission().equals(USER_MANAGE_ORGANIZATIONS.getValue())) - .findFirst() - .isPresent(); + .anyMatch(policy -> policy.getPermission().equals(USER_MANAGE_ORGANIZATIONS.getValue())); if (!isManageOrgPolicyPresent) { return Mono.error(new AppsmithException(AppsmithError.UNAUTHORIZED_ACCESS)); @@ -254,7 +262,7 @@ public class OrganizationServiceImpl extends BaseService organizationMono = repository.findById(orgId, ORGANIZATION_INVITE_USERS); Mono usernameMono = sessionUserService .getCurrentUser() - .map(user -> user.getUsername()); + .map(User::getUsername); return organizationMono .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ORGANIZATION, orgId))) @@ -280,7 +288,7 @@ public class OrganizationServiceImpl extends BaseService appsmithRolesMap = appsmithRoles .stream() - .collect(toMap(role -> role.getName(), AppsmithRole::getDescription)); + .collect(toMap(AppsmithRole::getName, AppsmithRole::getDescription)); return Mono.just(appsmithRolesMap); }); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java index bce6c0cb6b..25cbd7a3b3 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java @@ -66,6 +66,7 @@ public class UserServiceImpl extends BaseService i private final OrganizationRepository organizationRepository; private final UserOrganizationService userOrganizationService; private final RoleGraph roleGraph; + private final ConfigService configService; private static final String WELCOME_USER_EMAIL_TEMPLATE = "email/welcomeUserTemplate.html"; private static final String FORGOT_PASSWORD_EMAIL_TEMPLATE = "email/forgotPasswordTemplate.html"; @@ -92,7 +93,8 @@ public class UserServiceImpl extends BaseService i PolicyUtils policyUtils, OrganizationRepository organizationRepository, UserOrganizationService userOrganizationService, - RoleGraph roleGraph) { + RoleGraph roleGraph, + ConfigService configService) { super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService); this.organizationService = organizationService; this.analyticsService = analyticsService; @@ -105,6 +107,7 @@ public class UserServiceImpl extends BaseService i this.organizationRepository = organizationRepository; this.userOrganizationService = userOrganizationService; this.roleGraph = roleGraph; + this.configService = configService; } @Override @@ -429,30 +432,32 @@ public class UserServiceImpl extends BaseService i if (user.getPassword() == null || user.getPassword().isBlank()) { return Mono.error(new AppsmithException(AppsmithError.INVALID_CREDENTIALS)); } - user.setPassword(this.passwordEncoder.encode(user.getPassword())); + user.setPassword(passwordEncoder.encode(user.getPassword())); } - Organization personalOrg = new Organization(); - if (user.getName() == null) { + if (!StringUtils.hasText(user.getName())) { user.setName(user.getEmail()); } - String personalOrganizationName = user.computeFirstName() + "'s Personal Organization"; - personalOrg.setName(personalOrganizationName); - // Set the permissions for the user user.getPolicies().addAll(crudUserPolicy(user)); // Save the new user - Mono savedUserMono = Mono.just(user) + return Mono.just(user) .flatMap(this::validateObject) - .flatMap(repository::save); + .flatMap(repository::save) + .zipWith(configService.getTemplateOrganizationId().defaultIfEmpty("")) + .flatMap(tuple -> { + final String templateOrganizationId = tuple.getT2(); - return savedUserMono - .flatMap(savedUser -> { - // Creating the personal workspace and assigning the default groups to the new user - log.debug("Going to create organization: {} for user: {}", personalOrg, savedUser.getEmail()); - return organizationService.create(personalOrg, savedUser); + if (!StringUtils.hasText(templateOrganizationId)) { + // Since template organization is not configured, we create an empty personal organization. + final User savedUser = tuple.getT1(); + log.debug("Creating blank personal organization for user '{}'.", savedUser.getEmail()); + return organizationService.createPersonal(new Organization(), savedUser); + } + + return Mono.empty(); }) .then(repository.findByEmail(user.getUsername())) .flatMap(analyticsService::trackNewUser); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java index 3515cb9e96..cda1e02f58 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java @@ -117,9 +117,8 @@ public class ExamplesOrganizationCloner { if (!CollectionUtils.isEmpty(organization.getUserRoles())) { organization.getUserRoles().clear(); } - organization.setName(user.computeFirstName() + "'s Examples"); organization.setSlug(null); - return organizationService.create(organization, user); + return organizationService.createPersonal(organization, user); }) .flatMap(newOrganization -> { User userUpdate = new User(); diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExampleApplicationsAreMarked.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExampleApplicationsAreMarked.java index 84205cebe7..eab99c20ec 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExampleApplicationsAreMarked.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExampleApplicationsAreMarked.java @@ -63,6 +63,7 @@ public class ExampleApplicationsAreMarked { .flatMap(tuple -> { final Organization organization = tuple.getT1(); + assert organization.getId() != null; Mockito.when(configService.getTemplateOrganizationId()).thenReturn(Mono.just(organization.getId())); final Application app1 = new Application(); diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java index 8417650018..fda88a9560 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java @@ -26,7 +26,6 @@ import com.appsmith.server.services.LayoutActionService; import com.appsmith.server.services.OrganizationService; import com.appsmith.server.services.PageService; import com.appsmith.server.services.SessionUserService; -import com.appsmith.server.services.UserService; import lombok.extern.slf4j.Slf4j; import net.minidev.json.JSONObject; import org.bson.types.ObjectId; @@ -68,9 +67,6 @@ import static org.assertj.core.api.Assertions.assertThat; @DirtiesContext public class ExamplesOrganizationClonerTests { - @Autowired - UserService userService; - @Autowired private ExamplesOrganizationCloner examplesOrganizationCloner; @@ -161,7 +157,7 @@ public class ExamplesOrganizationClonerTests { .assertNext(data -> { assertThat(data.organization).isNotNull(); assertThat(data.organization.getId()).isNotNull(); - assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getName()).isEqualTo("api_user's Personal Organization"); assertThat(data.organization.getPolicies()).isNotEmpty(); assertThat(data.applications).isEmpty(); @@ -203,7 +199,7 @@ public class ExamplesOrganizationClonerTests { .assertNext(data -> { assertThat(data.organization).isNotNull(); assertThat(data.organization.getId()).isNotNull(); - assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getName()).isEqualTo("api_user's Personal Organization"); assertThat(data.organization.getPolicies()).isNotEmpty(); assertThat(data.applications).hasSize(1); @@ -255,7 +251,7 @@ public class ExamplesOrganizationClonerTests { .assertNext(data -> { assertThat(data.organization).isNotNull(); assertThat(data.organization.getId()).isNotNull(); - assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getName()).isEqualTo("api_user's Personal Organization"); assertThat(data.organization.getPolicies()).isNotEmpty(); assertThat(data.applications).hasSize(2); @@ -310,7 +306,7 @@ public class ExamplesOrganizationClonerTests { .assertNext(data -> { assertThat(data.organization).isNotNull(); assertThat(data.organization.getId()).isNotNull(); - assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getName()).isEqualTo("api_user's Personal Organization"); assertThat(data.organization.getPolicies()).isNotEmpty(); assertThat(data.applications).isEmpty(); @@ -361,7 +357,7 @@ public class ExamplesOrganizationClonerTests { .assertNext(data -> { assertThat(data.organization).isNotNull(); assertThat(data.organization.getId()).isNotNull(); - assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getName()).isEqualTo("api_user's Personal Organization"); assertThat(data.organization.getPolicies()).isNotEmpty(); assertThat(data.datasources).hasSize(2); @@ -437,7 +433,7 @@ public class ExamplesOrganizationClonerTests { .assertNext(data -> { assertThat(data.organization).isNotNull(); assertThat(data.organization.getId()).isNotNull(); - assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getName()).isEqualTo("api_user's Personal Organization"); assertThat(data.organization.getPolicies()).isNotEmpty(); assertThat(data.applications).hasSize(2); @@ -561,9 +557,7 @@ public class ExamplesOrganizationClonerTests { newPageAction.setPageId(page.getId()); return applicationPageService.addPageToApplication(app, page, false) .then(actionCollectionService.createAction(newPageAction)) - .flatMap(savedAction -> { - return layoutActionService.updateAction(savedAction.getId(), savedAction); - }) + .flatMap(savedAction -> layoutActionService.updateAction(savedAction.getId(), savedAction)) .then(pageService.findById(page.getId(), READ_PAGES)); }) .map(tuple2 -> { @@ -578,16 +572,14 @@ public class ExamplesOrganizationClonerTests { }) .then(examplesOrganizationCloner.cloneOrganizationForUser(organization.getId(), tuple.getT2())); }) - .doOnError(error -> { - log.error("Error preparing data for test", error); - }) + .doOnError(error -> log.error("Error preparing data for test", error)) .flatMap(this::loadOrganizationData); StepVerifier.create(resultMono) .assertNext(data -> { assertThat(data.organization).isNotNull(); assertThat(data.organization.getId()).isNotNull(); - assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getName()).isEqualTo("api_user's Personal Organization"); assertThat(data.organization.getPolicies()).isNotEmpty(); assertThat(data.applications).hasSize(2); @@ -596,10 +588,13 @@ public class ExamplesOrganizationClonerTests { "second application" ); - final Application firstApplication = data.applications.stream().filter(app -> app.getName().equals("first application")).findFirst().get(); + final Application firstApplication = data.applications.stream().filter(app -> app.getName().equals("first application")).findFirst().orElse(null); + assert firstApplication != null; final Page newPage = mongoTemplate.findOne(Query.query(Criteria.where("applicationId").is(firstApplication.getId()).and("name").is("A New Page")), Page.class); + assert newPage != null; final String actionId = newPage.getLayouts().get(0).getLayoutOnLoadActions().get(0).iterator().next().getId(); final Action newPageAction = mongoTemplate.findOne(Query.query(Criteria.where("id").is(actionId)), Action.class); + assert newPageAction != null; assertThat(newPageAction.getOrganizationId()).isEqualTo(data.organization.getId()); assertThat(data.datasources).hasSize(2); @@ -628,7 +623,7 @@ public class ExamplesOrganizationClonerTests { return applicationService .findByOrganizationId(organization.getId(), READ_APPLICATIONS) .flatMap(application -> pageService.findByApplicationId(application.getId(), READ_PAGES)) - .flatMap(page -> actionService.get(new LinkedMultiValueMap( + .flatMap(page -> actionService.get(new LinkedMultiValueMap<>( Map.of(FieldName.PAGE_ID, Collections.singletonList(page.getId()))))); } } diff --git a/deploy/template/nginx_app.conf.sh b/deploy/template/nginx_app.conf.sh index cee14a0d15..9ea2abfed0 100644 --- a/deploy/template/nginx_app.conf.sh +++ b/deploy/template/nginx_app.conf.sh @@ -44,6 +44,9 @@ $NGINX_SSL_CMNT server_name $custom_domain ; sub_filter __APPSMITH_CLIENT_LOG_LEVEL__ '\''${APPSMITH_CLIENT_LOG_LEVEL}'\''; sub_filter __APPSMITH_GOOGLE_MAPS_API_KEY__ '\''${APPSMITH_GOOGLE_MAPS_API_KEY}'\''; sub_filter __APPSMITH_TNC_PP__ '\''${APPSMITH_TNC_PP}'\''; + sub_filter __APPSMITH_VERSION_ID__ '\''${APPSMITH_VERSION_ID}'\''; + sub_filter __APPSMITH_VERSION_RELEASE_DATE__ '\''${APPSMITH_VERSION_RELEASE_DATE}'\''; + sub_filter __APPSMITH_INTERCOM_APP_ID__ '\''${APPSMITH_INTERCOM_APP_ID}'\''; } location /f { @@ -96,6 +99,9 @@ $NGINX_SSL_CMNT sub_filter __APPSMITH_ALGOLIA_API_KEY__ '\''${APPSMITH_AL $NGINX_SSL_CMNT sub_filter __APPSMITH_CLIENT_LOG_LEVEL__ '\''${APPSMITH_CLIENT_LOG_LEVEL}'\''; $NGINX_SSL_CMNT sub_filter __APPSMITH_GOOGLE_MAPS_API_KEY__ '\''${APPSMITH_GOOGLE_MAPS_API_KEY}'\''; $NGINX_SSL_CMNT sub_filter __APPSMITH_TNC_PP__ '\''${APPSMITH_TNC_PP}'\''; +$NGINX_SSL_CMNT sub_filter __APPSMITH_VERSION_ID__ '\''${APPSMITH_VERSION_ID}'\''; +$NGINX_SSL_CMNT sub_filter __APPSMITH_VERSION_RELEASE_DATE__ '\''${APPSMITH_VERSION_RELEASE_DATE}'\''; +$NGINX_SSL_CMNT sub_filter __APPSMITH_INTERCOM_APP_ID__ '\''${APPSMITH_INTERCOM_APP_ID}'\''; $NGINX_SSL_CMNT } $NGINX_SSL_CMNT $NGINX_SSL_CMNT location /f {