From 95c729d7d1d6df56417fdf86b91e05a9269b6241 Mon Sep 17 00:00:00 2001 From: balajisoundar Date: Sun, 12 Sep 2021 22:06:43 +0530 Subject: [PATCH] feat: Introduce a Welcome screen and a form to create a super user, on a fresh installation (#6806) This commit introduces a super user signup form, that appears only on an empty instance, to create a super user. fixes: #6934 --- app/client/src/AppRouter.tsx | 3 + app/client/src/actions/userActions.ts | 3 +- app/client/src/api/UserApi.tsx | 20 ++ app/client/src/components/ads/Toggle.tsx | 3 + .../components/ads/formFields/TextField.tsx | 2 +- app/client/src/constants/ApiConstants.tsx | 1 + app/client/src/constants/DefaultTheme.tsx | 12 ++ .../src/constants/ThirdPartyConstants.tsx | 2 + app/client/src/constants/forms.ts | 9 + app/client/src/constants/routes.ts | 1 + app/client/src/constants/userConstants.ts | 1 + app/client/src/pages/UserAuth/Login.tsx | 11 +- app/client/src/pages/common/AppHeader.tsx | 2 + .../src/pages/setup/DataCollectionForm.tsx | 61 ++++++ app/client/src/pages/setup/DetailsForm.tsx | 154 +++++++++++++++ app/client/src/pages/setup/Landing.tsx | 92 +++++++++ app/client/src/pages/setup/NewsletterForm.tsx | 57 ++++++ app/client/src/pages/setup/SetupForm.tsx | 179 ++++++++++++++++++ app/client/src/pages/setup/common.tsx | 60 ++++++ app/client/src/pages/setup/constants.ts | 50 +++++ app/client/src/pages/setup/index.tsx | 35 ++++ .../src/reducers/uiReducers/usersReducer.ts | 17 +- app/client/src/sagas/userSagas.tsx | 22 ++- 23 files changed, 782 insertions(+), 15 deletions(-) create mode 100644 app/client/src/pages/setup/DataCollectionForm.tsx create mode 100644 app/client/src/pages/setup/DetailsForm.tsx create mode 100644 app/client/src/pages/setup/Landing.tsx create mode 100644 app/client/src/pages/setup/NewsletterForm.tsx create mode 100644 app/client/src/pages/setup/SetupForm.tsx create mode 100644 app/client/src/pages/setup/common.tsx create mode 100644 app/client/src/pages/setup/constants.ts create mode 100644 app/client/src/pages/setup/index.tsx diff --git a/app/client/src/AppRouter.tsx b/app/client/src/AppRouter.tsx index 4b21bcc2c7..9f08eec723 100644 --- a/app/client/src/AppRouter.tsx +++ b/app/client/src/AppRouter.tsx @@ -18,6 +18,7 @@ import { USERS_URL, PROFILE, UNSUBSCRIBE_EMAIL_URL, + SETUP, } from "constants/routes"; import OrganizationLoader from "pages/organization/loader"; import ApplicationListLoader from "pages/Applications/loader"; @@ -43,6 +44,7 @@ import { getSafeCrash, getSafeCrashCode } from "selectors/errorSelectors"; import UserProfile from "pages/UserProfile"; import { getCurrentUser } from "actions/authActions"; import { getFeatureFlagsFetched } from "selectors/usersSelectors"; +import Setup from "pages/setup"; const SentryRoute = Sentry.withSentryRouting(Route); @@ -132,6 +134,7 @@ class AppRouter extends React.Component { component={UnsubscribeEmail} path={UNSUBSCRIBE_EMAIL_URL} /> + diff --git a/app/client/src/actions/userActions.ts b/app/client/src/actions/userActions.ts index f3670ce6fa..cac51068d0 100644 --- a/app/client/src/actions/userActions.ts +++ b/app/client/src/actions/userActions.ts @@ -14,8 +14,9 @@ export const logoutUser = (payload?: { redirectURL: string }) => ({ payload, }); -export const logoutUserSuccess = () => ({ +export const logoutUserSuccess = (isEmptyInstance: boolean) => ({ type: ReduxActionTypes.LOGOUT_USER_SUCCESS, + payload: isEmptyInstance, }); export const logoutUserError = (error: any) => ({ diff --git a/app/client/src/api/UserApi.tsx b/app/client/src/api/UserApi.tsx index 488f789c7d..ef6eaf895b 100644 --- a/app/client/src/api/UserApi.tsx +++ b/app/client/src/api/UserApi.tsx @@ -55,6 +55,19 @@ export interface UpdateUserRequest { email?: string; } +export interface CreateSuperUserRequest { + email: string; + name: string; + source: "FORM"; + state: "ACTIVATED"; + isEnabled: boolean; + password: string; + role: "Developer"; + companyName: string; + allowCollectingAnonymousData: boolean; + signupForNewsletter: boolean; +} + class UserApi extends Api { static usersURL = "v1/users"; static forgotPasswordURL = `${UserApi.usersURL}/forgotPassword`; @@ -69,6 +82,7 @@ class UserApi extends Api { static currentUserURL = "v1/users/me"; static photoURL = "v1/users/photo"; static featureFlagsURL = "v1/users/features"; + static superUserURL = "v1/users/super"; static createUser( request: CreateUserRequest, @@ -150,6 +164,12 @@ class UserApi extends Api { static fetchFeatureFlags(): AxiosPromise { return Api.get(UserApi.featureFlagsURL); } + + static createSuperUser( + request: CreateSuperUserRequest, + ): AxiosPromise { + return Api.post(UserApi.superUserURL, request); + } } export default UserApi; diff --git a/app/client/src/components/ads/Toggle.tsx b/app/client/src/components/ads/Toggle.tsx index 5ae09a22ff..8e0ebf4b67 100644 --- a/app/client/src/components/ads/Toggle.tsx +++ b/app/client/src/components/ads/Toggle.tsx @@ -4,6 +4,7 @@ import styled from "styled-components"; import Spinner from "./Spinner"; type ToggleProps = CommonComponentProps & { + name?: string; onToggle: (value: boolean) => void; value: boolean; }; @@ -135,10 +136,12 @@ export default function Toggle(props: ToggleProps) { ) => onChangeHandler(e.target.checked) } type="checkbox" + value={value ? "true" : "false"} /> {props.isLoading ? ( diff --git a/app/client/src/components/ads/formFields/TextField.tsx b/app/client/src/components/ads/formFields/TextField.tsx index 1e1b62ab36..a618785c18 100644 --- a/app/client/src/components/ads/formFields/TextField.tsx +++ b/app/client/src/components/ads/formFields/TextField.tsx @@ -26,7 +26,7 @@ const renderComponent = ( ); }; -type FormTextFieldProps = { +export type FormTextFieldProps = { name: string; placeholder: string; type?: InputType; diff --git a/app/client/src/constants/ApiConstants.tsx b/app/client/src/constants/ApiConstants.tsx index 3360552b1e..69e554067e 100644 --- a/app/client/src/constants/ApiConstants.tsx +++ b/app/client/src/constants/ApiConstants.tsx @@ -29,6 +29,7 @@ export const GithubOAuthURL = `${OAuthURL}/github`; export const LOGIN_SUBMIT_PATH = "login"; export const SIGNUP_SUBMIT_PATH = "users"; +export const SUPER_USER_SUBMIT_PATH = `${SIGNUP_SUBMIT_PATH}/super`; export const getExportAppAPIRoute = (applicationId: string) => `/api/v1/applications/export/${applicationId}`; diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index c9cfa09913..9e945619da 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -1217,6 +1217,10 @@ type ColorType = { background: string; buttonBackgroundHover: string; }; + link: string; + welcomePage?: { + text: string; + }; }; const editorBottomBar = { @@ -2005,6 +2009,10 @@ export const dark: ColorType = { }, actionSidePane, pagesEditor, + link: "#f86a2b", + welcomePage: { + text: lightShades[5], + }, }; export const light: ColorType = { @@ -2591,6 +2599,10 @@ export const light: ColorType = { }, actionSidePane, pagesEditor, + link: "#f86a2b", + welcomePage: { + text: lightShades[5], + }, }; export const theme: Theme = { diff --git a/app/client/src/constants/ThirdPartyConstants.tsx b/app/client/src/constants/ThirdPartyConstants.tsx index c9f8112d05..3edf4054a4 100644 --- a/app/client/src/constants/ThirdPartyConstants.tsx +++ b/app/client/src/constants/ThirdPartyConstants.tsx @@ -1,3 +1,5 @@ export type ENVIRONMENT = "PRODUCTION" | "STAGING" | "LOCAL"; export const S3_BUCKET_URL = "https://s3.us-east-2.amazonaws.com/assets.appsmith.com"; +export const TELEMETRY_URL = "https://docs.appsmith.com/telemetry"; +export const DISCORD_URL = "https://discord.gg/rBTTVJp"; diff --git a/app/client/src/constants/forms.ts b/app/client/src/constants/forms.ts index 40969148e3..2f5391f0b5 100644 --- a/app/client/src/constants/forms.ts +++ b/app/client/src/constants/forms.ts @@ -22,3 +22,12 @@ export const SAAS_EDITOR_FORM = "SaaSEditorForm"; export const DATASOURCE_DB_FORM = "DatasourceDBForm"; export const DATASOURCE_REST_API_FORM = "DatasourceRestAPIForm"; export const DATASOURCE_SAAS_FORM = "DatasourceSaaSForm"; + +export const WELCOME_FORM_NAME = "WelcomeSetupForm"; +export const WELCOME_FORM_NAME_FIELD_NAME = "name"; +export const WELCOME_FORM_EMAIL_FIELD_NAME = "email"; +export const WELCOME_FORM_PASSWORD_FIELD_NAME = "password"; +export const WELCOME_FORM_VERIFY_PASSWORD_FIELD_NAME = "verify_password"; +export const WELCOME_FORM_ROLE_FIELD_NAME = "role"; +export const WELCOME_FORM_ROLE_NAME_FIELD_NAME = "role_name"; +export const WELCOME_FORM_USECASE_FIELD_NAME = "useCase"; diff --git a/app/client/src/constants/routes.ts b/app/client/src/constants/routes.ts index c3d1158bfb..2b68c182db 100644 --- a/app/client/src/constants/routes.ts +++ b/app/client/src/constants/routes.ts @@ -11,6 +11,7 @@ export const PROFILE = "/profile"; export const USERS_URL = "/users"; export const VIEWER_URL_REGEX = /applications\/.*?\/pages\/.*/; export const UNSUBSCRIBE_EMAIL_URL = "/unsubscribe/discussion/:threadId"; +export const SETUP = "/setup/welcome"; export type BuilderRouteParams = { applicationId: string; diff --git a/app/client/src/constants/userConstants.ts b/app/client/src/constants/userConstants.ts index 6f7e10ac34..c110241d13 100644 --- a/app/client/src/constants/userConstants.ts +++ b/app/client/src/constants/userConstants.ts @@ -8,6 +8,7 @@ export type User = { username: string; name: string; gender: Gender; + emptyInstance?: boolean; }; export interface UserApplication { diff --git a/app/client/src/pages/UserAuth/Login.tsx b/app/client/src/pages/UserAuth/Login.tsx index 706c3d388f..10bb8af60d 100644 --- a/app/client/src/pages/UserAuth/Login.tsx +++ b/app/client/src/pages/UserAuth/Login.tsx @@ -1,13 +1,13 @@ import React from "react"; -import { Link, useLocation } from "react-router-dom"; -import { connect } from "react-redux"; +import { Link, Redirect, useLocation } from "react-router-dom"; +import { connect, useSelector } from "react-redux"; import { InjectedFormProps, reduxForm, formValueSelector } from "redux-form"; import { LOGIN_FORM_NAME, LOGIN_FORM_EMAIL_FIELD_NAME, LOGIN_FORM_PASSWORD_FIELD_NAME, } from "constants/forms"; -import { FORGOT_PASSWORD_URL, SIGN_UP_URL } from "constants/routes"; +import { FORGOT_PASSWORD_URL, SETUP, SIGN_UP_URL } from "constants/routes"; import { LOGIN_PAGE_TITLE, LOGIN_PAGE_EMAIL_INPUT_LABEL, @@ -49,6 +49,7 @@ import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; import { getIsSafeRedirectURL } from "utils/helpers"; +import { getCurrentUser } from "selectors/usersSelectors"; const { enableGithubOAuth, enableGoogleOAuth } = getAppsmithConfigs(); const validate = (values: LoginFormValues) => { @@ -87,6 +88,10 @@ export function Login(props: LoginFormProps) { const queryParams = new URLSearchParams(location.search); let showError = false; + const currentUser = useSelector(getCurrentUser); + if (currentUser?.emptyInstance) { + return ; + } if (queryParams.get("error")) { showError = true; } diff --git a/app/client/src/pages/common/AppHeader.tsx b/app/client/src/pages/common/AppHeader.tsx index 333988ad87..45935b1955 100644 --- a/app/client/src/pages/common/AppHeader.tsx +++ b/app/client/src/pages/common/AppHeader.tsx @@ -7,6 +7,7 @@ import { APP_VIEW_URL, BASE_URL, BUILDER_URL, + SETUP, USER_AUTH_URL, } from "constants/routes"; import { withRouter, RouteComponentProps } from "react-router"; @@ -32,6 +33,7 @@ class AppHeader extends React.Component { + ); diff --git a/app/client/src/pages/setup/DataCollectionForm.tsx b/app/client/src/pages/setup/DataCollectionForm.tsx new file mode 100644 index 0000000000..f65cf4b739 --- /dev/null +++ b/app/client/src/pages/setup/DataCollectionForm.tsx @@ -0,0 +1,61 @@ +import { noop } from "lodash"; +import React, { memo } from "react"; +import styled from "styled-components"; +import Toggle from "components/ads/Toggle"; +import { ControlWrapper } from "components/propertyControls/StyledControls"; +import { + AllowToggle, + AllowToggleLabel, + AllowToggleWrapper, + FormBodyWrapper, + FormHeaderIndex, + FormHeaderLabel, + FormHeaderSubtext, + FormHeaderWrapper, + StyledLink as Link, +} from "./common"; +import { TELEMETRY_URL } from "constants/ThirdPartyConstants"; + +const DataCollectionFormWrapper = styled.div` + width: 100%; + position: relative; + padding-left: ${(props) => props.theme.spaces[17] * 2}px; +`; + +const StyledLink = styled(Link)` + display: inline-block; + margin-top: 8px; +`; + +export default memo(function DataCollectionForm() { + return ( + + + 2. + Usage data preference + + Your data. Your choice. Data is collected anonymously.
+ + List of tracked items + +
+
+ + + + + noop} + value + /> + + + Allow Appsmith to collect usage data anonymously + + + + +
+ ); +}); diff --git a/app/client/src/pages/setup/DetailsForm.tsx b/app/client/src/pages/setup/DetailsForm.tsx new file mode 100644 index 0000000000..e857370976 --- /dev/null +++ b/app/client/src/pages/setup/DetailsForm.tsx @@ -0,0 +1,154 @@ +import React from "react"; +import styled from "styled-components"; +import { + Field, + InjectedFormProps, + WrappedFieldInputProps, + WrappedFieldMetaProps, +} from "redux-form"; +import { + FormBodyWrapper, + FormHeaderIndex, + FormHeaderLabel, + FormHeaderWrapper, +} from "./common"; +import Dropdown from "components/ads/Dropdown"; +import StyledFormGroup from "components/ads/formFields/FormGroup"; +import { createMessage } from "constants/messages"; +import FormTextField, { + FormTextFieldProps, +} from "components/ads/formFields/TextField"; +import { DetailsFormValues } from "./SetupForm"; +import { ButtonWrapper } from "pages/Applications/ForkModalStyles"; +import Button, { Category, Size } from "components/ads/Button"; +import { OptionType, roleOptions, useCaseOptions } from "./constants"; + +const DetailsFormWrapper = styled.div` + width: 100%; + position: relative; + padding-left: ${(props) => props.theme.spaces[17] * 2}px; + padding-right: ${(props) => props.theme.spaces[4]}px; +`; + +const StyledFormBodyWrapper = styled(FormBodyWrapper)` + width: 260px; +`; + +const DropdownWrapper = styled(StyledFormGroup)` + && { + margin-bottom: 33px; + } + && .cs-text { + width: 100%; + } +`; + +function withDropdown(options: OptionType[]) { + return function Fieldropdown( + ComponentProps: FormTextFieldProps & { + meta: Partial; + input: Partial; + }, + ) { + function onSelect(value?: string) { + ComponentProps.input.onChange && ComponentProps.input.onChange(value); + ComponentProps.input.onBlur && ComponentProps.input.onBlur(value); + } + + const selected = + options.find((option) => option.value == ComponentProps.input.value) || + {}; + + return ( + + ); + }; +} + +export default function DetailsForm( + props: InjectedFormProps & DetailsFormValues & { onNext?: () => void }, +) { + const ref = React.createRef(); + + return ( + + + 1. + Let us get to know you better! + + + "Full Name")}> + + + "Email Id")}> + + + "Create Password")}> + + + "Verify Password")}> + + + "What Role Do You Play?")}> + + + {props.role == "other" && ( + "Role")}> + + + )} + "Tell Us About Your Use Case")} + > + + + +