feat: onboarding flow revamp for first time admin users: ce (#23581)

Onboarding flow revamp for first time admin users.
Uses the new ADS2.0 Components.
Shows telemetry popup.

zeplin:
https://app.zeplin.io/project/642f9f61af65ec6928659130/screen/64476316d729f744cb695232

#### PR fixes following issue(s)
Fixes #https://github.com/appsmithorg/cloud-services/issues/673

#### Type of change
> Please delete options that are not relevant.
- Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- This change requires a documentation update
>
>
>
## Testing
>
#### How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Also
list any relevant details for your test configuration.
> Delete anything that is not relevant
- [x] Manual
- [ ] Jest
- [x] Cypress
>
>
#### Test Plan
> 
> https://github.com/appsmithorg/TestSmith/issues/2401
>
#### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)
>
>
>
## Checklist:
#### Dev activity
- [x] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Test-plan-implementation#speedbreaker-features-to-consider-for-every-change)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans/_edit#areas-of-interest)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
This commit is contained in:
Dipyaman Biswas 2023-05-25 23:51:56 +05:30 committed by GitHub
parent 55056dacda
commit 94f5744800
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 412 additions and 431 deletions

View File

@ -1,18 +1,18 @@
{
"getStarted": ".t--welcome-form-get-started",
"fullName": ".t--welcome-form-full-name input",
"email": ".t--welcome-form-email input",
"password": ".t--welcome-form-password input",
"verifyPassword": ".t--welcome-form-verify-password input",
"firstName":".t--welcome-form-first-name input",
"lastName":".t--welcome-form-last-name input",
"email":".t--welcome-form-email input",
"password":".t--welcome-form-password input",
"verifyPassword":".t--welcome-form-verify-password input",
"roleDropdown": ".t--welcome-form-role-dropdown .setup-dropdown",
"roleDropdownOption": ".rc-select-item-option",
"roleInput": ".t--welcome-form-role-input input",
"useCaseDropdown": ".t--welcome-form-role-usecase",
"useCaseDropdownOption": ".rc-select-item-option-content",
"nextButton": ".t--welcome-form-next-button",
"dataCollection": ".ads-v2-switch__label:last",
"newsLetter": ".ads-v2-switch__label:first",
"submitButton": ".t--welcome-form-submit-button",
"dataCollection": ".t--welcome-form-datacollection",
"newsLetter": ".t--welcome-form-newsletter",
"createButton": ".t--welcome-form-create-button",
"createSuperUser": "#super-user-form",
"superUserForm": "[data-testid='super-user-form']"
}
}

View File

@ -267,7 +267,7 @@ Cypress.Commands.add("Signup", (uname, pword) => {
cy.get(signupPage.dropdownOption).click();
cy.get(signupPage.useCaseDropdown).click();
cy.get(signupPage.dropdownOption).click();
cy.get(signupPage.roleUsecaseSubmit).click();
cy.get(signupPage.roleUsecaseSubmit).click({ force: true });
cy.wait("@getMe");
cy.wait(3000);
@ -1275,73 +1275,36 @@ Cypress.Commands.add("createJSObject", (JSCode) => {
Cypress.Commands.add("createSuperUser", () => {
cy.wait(1000);
cy.get(welcomePage.getStarted).should("be.visible");
cy.get(welcomePage.getStarted).should("not.be.disabled");
cy.get(welcomePage.getStarted).click();
cy.get(welcomePage.fullName).should("be.visible");
cy.get(welcomePage.firstName).should("be.visible");
cy.get(welcomePage.lastName).should("be.visible");
cy.get(welcomePage.email).should("be.visible");
cy.get(welcomePage.password).should("be.visible");
cy.get(welcomePage.verifyPassword).should("be.visible");
cy.get(welcomePage.roleDropdown).should("be.visible");
cy.get(welcomePage.useCaseDropdown).should("be.visible");
cy.get(welcomePage.nextButton).should("be.disabled");
cy.get(welcomePage.submitButton).should("be.disabled");
cy.get(welcomePage.fullName).type(Cypress.env("USERNAME"));
cy.get(welcomePage.nextButton).should("be.disabled");
cy.get(welcomePage.firstName).type(Cypress.env("USERNAME"));
cy.get(welcomePage.submitButton).should("be.disabled");
cy.get(welcomePage.email).type(Cypress.env("USERNAME"));
cy.get(welcomePage.nextButton).should("be.disabled");
cy.get(welcomePage.submitButton).should("be.disabled");
cy.get(welcomePage.password).type(Cypress.env("PASSWORD"));
cy.get(welcomePage.nextButton).should("be.disabled");
cy.get(welcomePage.submitButton).should("be.disabled");
cy.get(welcomePage.verifyPassword).type(Cypress.env("PASSWORD"));
cy.get(welcomePage.nextButton).should("be.disabled");
cy.get(welcomePage.submitButton).should("not.be.disabled");
cy.get(welcomePage.submitButton).click();
cy.get(welcomePage.roleDropdown).click();
cy.get(welcomePage.roleDropdownOption).eq(1).click();
cy.get(welcomePage.nextButton).should("be.disabled");
cy.get(welcomePage.submitButton).should("be.disabled");
cy.get(welcomePage.useCaseDropdown).click();
cy.get(welcomePage.useCaseDropdownOption).eq(1).click();
cy.get(welcomePage.nextButton).should("not.be.disabled");
cy.get(welcomePage.nextButton).click();
if (Cypress.env("AIRGAPPED")) {
cy.get(welcomePage.newsLetter).should("not.exist");
cy.get(welcomePage.dataCollection).should("not.exist");
cy.get(welcomePage.createButton).should("not.exist");
} else {
cy.get(welcomePage.superUserForm).should("be.visible");
cy.get(welcomePage.newsLetter).should("be.visible");
cy.get(welcomePage.dataCollection).should("be.visible");
cy.get(welcomePage.createButton).should("be.visible");
cy.get(welcomePage.createButton).trigger("mouseover").click();
cy.wait("@createSuperUser").then((interception) => {
expect(interception.request.body).contains(
"allowCollectingAnonymousData=true",
);
expect(interception.request.body).contains("signupForNewsletter=true");
});
}
//cy.get(welcomePage.newsLetter).trigger("mouseover").click();
//cy.get(welcomePage.newsLetter).find("input").uncheck();//not working
//cy.get(welcomePage.dataCollection).trigger("mouseover").click();
//cy.wait(1000); //for toggles to settle
//Seeing issue with above also, trying multiple click as below
//cy.get(welcomePage.createButton).click({ multiple: true });
//cy.get(welcomePage.createButton).trigger("click");
//Submit also not working
//cy.get(welcomePage.createSuperUser).submit();
//cy.wait(5000); //waiting a bit before attempting logout
// cy.get("body").then(($ele) => {
// if ($ele.find(locator._spanButton("Next").length) > 0) {
// agHelper.GetNClick(locator._spanButton("Next"));
// } else agHelper.GetNClick(locator._spanButton("Make your first App"));
// });
//trying jquery way - also not working
// cy.get(welcomePage.createButton).then(($createBtn) => {
// const $jQueryButton = Cypress.$($createBtn); // wrap the button element in jQuery
// $jQueryButton.trigger("click"); // click on the button using jQuery
// });
cy.get(welcomePage.submitButton).should("not.be.disabled");
cy.get(welcomePage.submitButton).click();
cy.wait("@createSuperUser").then((interception) => {
expect(interception.request.body).contains(
"allowCollectingAnonymousData=true",
);
expect(interception.request.body).contains("signupForNewsletter=true");
});
cy.LogOut();
cy.wait(2000);

View File

@ -999,6 +999,9 @@ export const ONBOARDING_CHECKLIST_DEPLOY_APPLICATIONS = {
export const ONBOARDING_CHECKLIST_FOOTER = () =>
"Not sure where to start? Take the welcome tour";
export const ONBOARDING_TELEMETRY_POPUP = () =>
"We only collect usage data to make Appsmith better for everyone. Visit admin settings to toggle this off.";
//Introduction modal
export const HOW_APPSMITH_WORKS = () =>
"Heres a quick overview of how Appsmith works. ";
@ -1062,9 +1065,8 @@ export const USE_SNIPPET = () => "Snippet";
export const SNIPPET_TOOLTIP = () => "Search code snippets";
//Welcome page
export const WELCOME_HEADER = () => "Welcome!";
export const WELCOME_BODY = () =>
"Let us setup your account so you can make awesome applications!";
export const WELCOME_HEADER = () => "Almost there";
export const WELCOME_BODY = () => "Let's setup your account first";
export const WELCOME_ACTION = () => "Get started";
// API Editor
@ -1081,10 +1083,11 @@ export const ACTION_EXECUTION_MESSAGE = (actionType: string) =>
export const ACTION_EXECUTION_CANCEL = () => "Cancel request";
export const WELCOME_FORM_HEADER = () => "Let us get to know you better!";
export const WELCOME_FORM_FULL_NAME = () => "Full Name";
export const WELCOME_FORM_EMAIL_ID = () => "Email Id";
export const WELCOME_FORM_CREATE_PASSWORD = () => "Create Password";
export const WELCOME_FORM_VERIFY_PASSWORD = () => "Verify Password";
export const WELCOME_FORM_FIRST_NAME = () => "First name";
export const WELCOME_FORM_LAST_NAME = () => "Last name";
export const WELCOME_FORM_EMAIL_ID = () => "Email";
export const WELCOME_FORM_CREATE_PASSWORD = () => "Enter password";
export const WELCOME_FORM_VERIFY_PASSWORD = () => "Verify password";
export const WELCOME_FORM_ROLE_DROPDOWN = () =>
"Tell us about your primary skillset";
export const WELCOME_FORM_ROLE_DROPDOWN_PLACEHOLDER = () =>

View File

@ -0,0 +1,38 @@
import React from "react";
import { Callout } from "design-system";
import {
ADMIN_SETTINGS,
LEARN_MORE,
ONBOARDING_TELEMETRY_POPUP,
createMessage,
} from "@appsmith/constants/messages";
import { ADMIN_SETTINGS_CATEGORY_DEFAULT_PATH } from "constants/routes";
import { TELEMETRY_DOCS_PAGE_URL } from "./constants";
export default function AnonymousDataPopup(props: {
onCloseCallout: () => void;
}) {
return (
<div className="absolute top-5">
<Callout
isClosable
kind="info"
links={[
{
children: createMessage(ADMIN_SETTINGS),
to: ADMIN_SETTINGS_CATEGORY_DEFAULT_PATH,
},
{
children: createMessage(LEARN_MORE),
to: TELEMETRY_DOCS_PAGE_URL,
},
]}
onClose={() => {
props.onCloseCallout();
}}
>
{createMessage(ONBOARDING_TELEMETRY_POPUP)}
</Callout>
</div>
);
}

View File

@ -21,7 +21,7 @@ import {
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import { INTEGRATION_TABS } from "constants/routes";
import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants";
import React from "react";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
@ -33,14 +33,25 @@ import {
getDatasources,
getPageActions,
} from "selectors/entitiesSelector";
import { getFirstTimeUserOnboardingModal } from "selectors/onboardingSelectors";
import styled from "styled-components";
import AnalyticsUtil from "utils/AnalyticsUtil";
import history from "utils/history";
import IntroductionModal from "./IntroductionModal";
import { integrationEditorURL } from "RouteBuilder";
import { getAssetUrl } from "@appsmith/utils/airgapHelpers";
import { getAssetUrl, isAirgapped } from "@appsmith/utils/airgapHelpers";
import AnonymousDataPopup from "./AnonymousDataPopup";
import {
getFirstTimeUserOnboardingComplete,
getFirstTimeUserOnboardingModal,
getIsFirstTimeUserOnboardingEnabled,
} from "selectors/onboardingSelectors";
import { getCurrentUser } from "selectors/usersSelectors";
import {
getFirstTimeUserOnboardingTelemetryCalloutIsAlreadyShown,
setFirstTimeUserOnboardingTelemetryCalloutVisibility,
} from "utils/storage";
import { ANONYMOUS_DATA_POPOP_TIMEOUT } from "./constants";
import { DatasourceCreateEntryPoints } from "constants/Datasource";
import IntroductionModal from "./IntroductionModal";
const Wrapper = styled.div`
width: 100%;
@ -92,6 +103,12 @@ const getOnboardingQueryImg = () => `${ASSETS_CDN_URL}/onboarding-query.svg`;
const getOnboardingWidgetImg = () => `${ASSETS_CDN_URL}/onboarding-widget.svg`;
export default function OnboardingTasks() {
const [isAnonymousDataPopupOpen, setisAnonymousDataPopupOpen] =
useState(false);
const isFirstTimeUserOnboardingEnabled = useSelector(
getIsFirstTimeUserOnboardingEnabled,
);
const applicationId = useSelector(getCurrentApplicationId);
const pageId = useSelector(getCurrentPageId);
let content;
@ -99,7 +116,44 @@ export default function OnboardingTasks() {
const actions = useSelector(getPageActions(pageId));
const widgets = useSelector(getCanvasWidgets);
const dispatch = useDispatch();
const user = useSelector(getCurrentUser);
const isAdmin = user?.isSuperUser || false;
const isOnboardingCompleted = useSelector(getFirstTimeUserOnboardingComplete);
const showModal = useSelector(getFirstTimeUserOnboardingModal);
const hideAnonymousDataPopup = () => {
setisAnonymousDataPopupOpen(false);
setFirstTimeUserOnboardingTelemetryCalloutVisibility(true);
};
const showShowAnonymousDataPopup = async () => {
const shouldPopupShow =
!isAirgapped() &&
isFirstTimeUserOnboardingEnabled &&
isAdmin &&
!isOnboardingCompleted;
if (shouldPopupShow) {
const isAnonymousDataPopupAlreadyOpen =
await getFirstTimeUserOnboardingTelemetryCalloutIsAlreadyShown();
//true if the modal was already shown else show the modal and set to already shown, also hide the modal after 10 secs
if (isAnonymousDataPopupAlreadyOpen) {
setisAnonymousDataPopupOpen(false);
} else {
setisAnonymousDataPopupOpen(true);
setTimeout(() => {
hideAnonymousDataPopup();
}, ANONYMOUS_DATA_POPOP_TIMEOUT);
await setFirstTimeUserOnboardingTelemetryCalloutVisibility(true);
}
} else {
setisAnonymousDataPopupOpen(shouldPopupShow);
}
};
useEffect(() => {
showShowAnonymousDataPopup();
}, []);
if (!datasources.length && !actions.length) {
content = (
<CenteredContainer>
@ -272,7 +326,10 @@ export default function OnboardingTasks() {
return (
<Wrapper data-testid="onboarding-tasks-wrapper">
{content}
{showModal && (
{isAnonymousDataPopupOpen && (
<AnonymousDataPopup onCloseCallout={hideAnonymousDataPopup} />
)}
{!isAdmin && showModal && (
<IntroductionModal
close={() => {
dispatch({

View File

@ -0,0 +1,6 @@
//Hide Anonymous Data Popup after 15 seconds
export const ANONYMOUS_DATA_POPOP_TIMEOUT = 15000;
//Telemetry Docs Page
export const TELEMETRY_DOCS_PAGE_URL =
"https://docs.appsmith.com/product/telemetry";

View File

@ -1,69 +0,0 @@
import React, { memo, useState } from "react";
import styled from "styled-components";
import { Switch, Link } from "design-system";
import { ControlWrapper } from "components/propertyControls/StyledControls";
import {
AllowToggle,
AllowToggleWrapper,
FormBodyWrapper,
FormHeaderIndex,
FormHeaderLabel,
FormHeaderSubtext,
} from "./common";
import { TELEMETRY_URL } from "constants/ThirdPartyConstants";
import {
createMessage,
WELCOME_FORM_DATA_COLLECTION_BODY,
WELCOME_FORM_DATA_COLLECTION_HEADER,
WELCOME_FORM_DATA_COLLECTION_LABEL_ENABLE,
WELCOME_FORM_DATA_COLLECTION_LINK,
} from "@appsmith/constants/messages";
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() {
const [allowCollection, setAllowCollection] = useState(true);
return (
<DataCollectionFormWrapper>
<div className="relative flex flex-col items-start">
<FormHeaderIndex className="absolute -left-6">2.</FormHeaderIndex>
<FormHeaderLabel>
{createMessage(WELCOME_FORM_DATA_COLLECTION_HEADER)}
</FormHeaderLabel>
<FormHeaderSubtext>
{createMessage(WELCOME_FORM_DATA_COLLECTION_BODY)}
<br />
<StyledLink kind="primary" target="_blank" to={TELEMETRY_URL}>
{createMessage(WELCOME_FORM_DATA_COLLECTION_LINK)}
</StyledLink>
</FormHeaderSubtext>
</div>
<FormBodyWrapper>
<ControlWrapper>
<AllowToggleWrapper>
<AllowToggle>
<Switch
className="t--welcome-form-datacollection"
isSelected={allowCollection}
name="allowCollectingAnonymousData"
onChange={(value: boolean) => setAllowCollection(value)}
value={allowCollection.toString()}
>
{createMessage(WELCOME_FORM_DATA_COLLECTION_LABEL_ENABLE)}
</Switch>
</AllowToggle>
</AllowToggleWrapper>
</ControlWrapper>
</FormBodyWrapper>
</DataCollectionFormWrapper>
);
});

View File

@ -1,44 +1,51 @@
import React from "react";
import React, { useEffect, useMemo, useState } from "react";
import styled from "styled-components";
import { Field } from "redux-form";
import {
DropdownWrapper,
FormBodyWrapper,
FormHeaderIndex,
FormHeaderLabel,
withDropdown,
} from "./common";
import { DropdownWrapper, FormBodyWrapper, withDropdown } from "./common";
import {
createMessage,
WELCOME_FORM_EMAIL_ID,
WELCOME_FORM_FULL_NAME,
WELCOME_FORM_FIRST_NAME,
WELCOME_FORM_LAST_NAME,
WELCOME_FORM_CREATE_PASSWORD,
WELCOME_FORM_VERIFY_PASSWORD,
WELCOME_FORM_ROLE_DROPDOWN,
WELCOME_FORM_ROLE,
WELCOME_FORM_USE_CASE,
WELCOME_FORM_CUSTOM_USE_CASE,
WELCOME_FORM_HEADER,
WELCOME_FORM_ROLE_DROPDOWN_PLACEHOLDER,
WELCOME_FORM_USE_CASE_PLACEHOLDER,
CONTINUE,
ONBOARDING_STATUS_GET_STARTED,
} from "@appsmith/constants/messages";
import FormTextField from "components/utils/ReduxFormTextField";
import type { SetupFormProps } from "./SetupForm";
import { ButtonWrapper } from "pages/Applications/ForkModalStyles";
import { FormGroup } from "design-system-old";
import { Button } from "design-system";
import { Button, Checkbox } from "design-system";
import { roleOptions, useCaseOptions } from "./constants";
import { isAirgapped } from "@appsmith/utils/airgapHelpers";
import { setFirstTimeUserOnboardingTelemetryCalloutVisibility } from "utils/storage";
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 StyledFormBodyWrapper = styled(FormBodyWrapper)``;
const StyledTabIndicatorWrapper = styled.div`
display: flex;
`;
const StyledTabIndicator = styled.div<{ isFirstPage?: boolean }>`
width: 48px;
height: 3px;
margin: 0 6px 0 0;
background-color: ${(props) =>
props.isFirstPage
? `var(--ads-v2-color-bg-emphasis);`
: `var(--ads-v2-color-bg-brand);`};
`;
const StyledFormGroup = styled(FormGroup)`
@ -52,107 +59,143 @@ export default function DetailsForm(
) {
const ref = React.createRef<HTMLDivElement>();
const isAirgappedInstance = isAirgapped();
const [formState, setFormState] = useState(0);
const isFirstPage = useMemo(() => formState === 0, [formState]);
useEffect(() => {
const setTelemetryVisibleFalse = async () => {
await setFirstTimeUserOnboardingTelemetryCalloutVisibility(false);
};
setTelemetryVisibleFalse();
}, []);
return (
<DetailsFormWrapper ref={ref}>
<div className="relative flex-col items-start">
<FormHeaderIndex className="absolute -left-6">1.</FormHeaderIndex>
<FormHeaderLabel>{createMessage(WELCOME_FORM_HEADER)}</FormHeaderLabel>
</div>
<StyledTabIndicatorWrapper>
<StyledTabIndicator />
<StyledTabIndicator isFirstPage={isFirstPage} />
</StyledTabIndicatorWrapper>
<StyledFormBodyWrapper>
<StyledFormGroup
className="t--welcome-form-full-name"
label={createMessage(WELCOME_FORM_FULL_NAME)}
>
<FormTextField
autoFocus
name="name"
placeholder="John Doe"
type="text"
/>
</StyledFormGroup>
<StyledFormGroup
className="t--welcome-form-email"
label={createMessage(WELCOME_FORM_EMAIL_ID)}
>
<FormTextField
name="email"
placeholder="How can we reach you?"
type="email"
/>
</StyledFormGroup>
<StyledFormGroup
className="t--welcome-form-password"
label={createMessage(WELCOME_FORM_CREATE_PASSWORD)}
>
<FormTextField
name="password"
placeholder="Make it strong!"
type="password"
/>
</StyledFormGroup>
<StyledFormGroup
className="t--welcome-form-verify-password"
label={createMessage(WELCOME_FORM_VERIFY_PASSWORD)}
>
<FormTextField
data-testid="verifyPassword"
name="verifyPassword"
placeholder="Re-enter password"
type="password"
/>
</StyledFormGroup>
<DropdownWrapper
className="t--welcome-form-role-dropdown"
label={createMessage(WELCOME_FORM_ROLE_DROPDOWN)}
>
<Field
asyncControl
component={withDropdown(roleOptions)}
name="role"
placeholder={createMessage(WELCOME_FORM_ROLE_DROPDOWN_PLACEHOLDER)}
type="text"
/>
</DropdownWrapper>
{props.role == "other" && (
<StyledFormGroup
className="t--welcome-form-role-input"
label={createMessage(WELCOME_FORM_ROLE)}
>
<FormTextField name="role_name" placeholder="" type="text" />
<div className={isFirstPage ? "block" : "hidden"}>
<div className="flex flex-row justify-between w-100">
<StyledFormGroup className="!w-52 t--welcome-form-first-name">
<FormTextField
autoFocus
label={createMessage(WELCOME_FORM_FIRST_NAME)}
name="firstName"
placeholder="John"
type="text"
/>
</StyledFormGroup>
<StyledFormGroup className="!w-52 t--welcome-form-last-name">
<FormTextField
label={createMessage(WELCOME_FORM_LAST_NAME)}
name="lastName"
placeholder="Doe"
type="text"
/>
</StyledFormGroup>
</div>
<StyledFormGroup className="t--welcome-form-email">
<FormTextField
label={createMessage(WELCOME_FORM_EMAIL_ID)}
name="email"
placeholder="How can we reach you?"
type="email"
/>
</StyledFormGroup>
)}
<DropdownWrapper
className="t--welcome-form-role-usecase"
label={createMessage(WELCOME_FORM_USE_CASE)}
>
<Field
asyncControl
component={withDropdown(useCaseOptions)}
name="useCase"
placeholder={createMessage(WELCOME_FORM_USE_CASE_PLACEHOLDER)}
type="text"
/>
</DropdownWrapper>
{props.useCase == "other" && (
<StyledFormGroup
className="t--welcome-form-use-case-input"
label={createMessage(WELCOME_FORM_CUSTOM_USE_CASE)}
>
<FormTextField name="custom_useCase" placeholder="" type="text" />
<StyledFormGroup className="t--welcome-form-password">
<FormTextField
label={createMessage(WELCOME_FORM_CREATE_PASSWORD)}
name="password"
placeholder="Make it strong!"
type="password"
/>
</StyledFormGroup>
<StyledFormGroup className="t--welcome-form-verify-password">
<FormTextField
data-testid="verifyPassword"
label={createMessage(WELCOME_FORM_VERIFY_PASSWORD)}
name="verifyPassword"
placeholder="Type correctly"
type="password"
/>
</StyledFormGroup>
</div>
{!isFirstPage && (
<div>
<DropdownWrapper
className="t--welcome-form-role-dropdown"
label={createMessage(WELCOME_FORM_ROLE_DROPDOWN)}
>
<Field
asyncControl
component={withDropdown(roleOptions)}
name="role"
placeholder={createMessage(
WELCOME_FORM_ROLE_DROPDOWN_PLACEHOLDER,
)}
size="md"
type="text"
/>
</DropdownWrapper>
{props.role == "other" && (
<StyledFormGroup className="t--welcome-form-role-input">
<FormTextField
label={createMessage(WELCOME_FORM_ROLE)}
name="role_name"
placeholder=""
type="text"
/>
</StyledFormGroup>
)}
<DropdownWrapper
className="t--welcome-form-role-usecase"
label={createMessage(WELCOME_FORM_USE_CASE)}
>
<Field
asyncControl
component={withDropdown(useCaseOptions)}
name="useCase"
placeholder={createMessage(WELCOME_FORM_USE_CASE_PLACEHOLDER)}
type="text"
/>
</DropdownWrapper>
{props.useCase == "other" && (
<StyledFormGroup className="t--welcome-form-use-case-input">
<FormTextField
label={createMessage(WELCOME_FORM_CUSTOM_USE_CASE)}
name="custom_useCase"
placeholder=""
type="text"
/>
</StyledFormGroup>
)}
{!isAirgapped() && (
<Checkbox defaultSelected name="signupForNewsletter" value="true">
I want security and product updates.
</Checkbox>
)}
</div>
)}
<ButtonWrapper>
<Button
className="t--welcome-form-next-button"
className="t--welcome-form-submit-button w-100"
isDisabled={props.invalid}
kind="secondary"
onClick={!isAirgappedInstance ? props.onNext : undefined}
kind="primary"
onClick={() => {
if (isFirstPage) setFormState(1);
}}
size="md"
type={!isAirgappedInstance ? "button" : "submit"}
type={isFirstPage ? "button" : "submit"}
>
Next
{isFirstPage
? createMessage(CONTINUE)
: createMessage(ONBOARDING_STATUS_GET_STARTED)}
</Button>
</ButtonWrapper>
</StyledFormBodyWrapper>

View File

@ -22,6 +22,7 @@ import { Field, formValueSelector, reduxForm } from "redux-form";
import styled from "styled-components";
import { DropdownWrapper, withDropdown } from "./common";
import { roleOptions, useCaseOptions } from "./constants";
import SetupForm from "./SetupForm";
const ActionContainer = styled.div`
margin-top: ${(props) => props.theme.spaces[15]}px;
@ -41,16 +42,10 @@ type NonSuperUserFormData = {
role_name?: string;
};
export function SuperUserForm(props: UserFormProps) {
export function SuperUserForm() {
return (
<ActionContainer>
<StyledButton
className="t--welcome-form-get-started"
onClick={() => props.onGetStarted && props.onGetStarted()}
size="md"
>
{createMessage(WELCOME_ACTION)}
</StyledButton>
<SetupForm />
</ActionContainer>
);
}
@ -124,15 +119,9 @@ function NonSuperUser(
<StyledButton
className="t--get-started-button"
isDisabled={props.invalid}
onClick={() =>
!props.invalid && // temp fix - design system needs to be fixed for disabling click
props.onGetStarted &&
props.onGetStarted(
props.role !== "other" ? props.role : props.role_name,
props.useCase,
)
}
size="md"
kind="primary"
renderAs="button"
type="submit"
>
{createMessage(WELCOME_ACTION)}
</StyledButton>

View File

@ -1,63 +0,0 @@
import { noop } from "lodash";
import React from "react";
import styled from "styled-components";
import { Button, Switch } from "design-system";
import {
AllowToggle,
AllowToggleWrapper,
ButtonWrapper,
FormBodyWrapper,
FormHeaderIndex,
FormHeaderLabel,
} from "./common";
import { memo } from "react";
import {
createMessage,
WELCOME_FORM_NEWLETTER_HEADER,
WELCOME_FORM_NEWLETTER_LABEL,
WELCOME_FORM_SUBMIT_LABEL,
} from "@appsmith/constants/messages";
const NewsletterContainer = styled.div`
width: 100%;
position: relative;
padding-left: ${(props) => props.theme.spaces[17] * 2}px;
margin-top: ${(props) => props.theme.spaces[12] * 2}px;
`;
export default memo(function NewsletterForm() {
return (
<NewsletterContainer>
<div className="relative flex-col items-start">
<FormHeaderIndex className="absolute -left-6">3.</FormHeaderIndex>
<FormHeaderLabel>
{createMessage(WELCOME_FORM_NEWLETTER_HEADER)}
</FormHeaderLabel>
</div>
<FormBodyWrapper>
<AllowToggleWrapper>
<AllowToggle>
<Switch
className="t--welcome-form-newsletter"
defaultSelected
name="signupForNewsletter"
onChange={() => noop}
value={"true"}
>
{createMessage(WELCOME_FORM_NEWLETTER_LABEL)}
</Switch>
</AllowToggle>
</AllowToggleWrapper>
<ButtonWrapper>
<Button
className="t--welcome-form-create-button"
size="md"
type="submit"
>
{createMessage(WELCOME_FORM_SUBMIT_LABEL)}
</Button>
</ButtonWrapper>
</FormBodyWrapper>
</NewsletterContainer>
);
});

View File

@ -1,10 +1,7 @@
import React, { useRef } from "react";
import styled from "styled-components";
import { connect } from "react-redux";
import DataCollectionForm from "./DataCollectionForm";
import DetailsForm from "./DetailsForm";
import NewsletterForm from "./NewsletterForm";
import AppsmithLogo from "assets/images/appsmith_logo.png";
import {
WELCOME_FORM_USECASE_FIELD_NAME,
WELCOME_FORM_EMAIL_FIELD_NAME,
@ -23,48 +20,29 @@ import type { AppState } from "@appsmith/reducers";
import { SUPER_USER_SUBMIT_PATH } from "@appsmith/constants/ApiConstants";
import { useState } from "react";
import { isAirgapped } from "@appsmith/utils/airgapHelpers";
import { noop } from "utils/AppsmithUtils";
const PageWrapper = styled.div`
width: 100%;
display: flex;
justify-content: center;
height: 100vh;
justify-content: start;
overflow: auto;
position: relative;
z-index: 100;
`;
const SetupFormContainer = styled.div`
padding: 120px 42px 0px 0px;
`;
const SetupFormContainer = styled.div``;
const SetupStep = styled.div<{ active: boolean }>`
display: ${(props) => (props.active ? "block" : "none")};
`;
const LogoContainer = styled.div`
padding-left: ${(props) => props.theme.spaces[17] * 2}px;
padding-top: ${(props) => props.theme.spaces[12] * 2}px;
transform: translate(-11px, 0);
background-color: ${(props) => props.theme.colors.homepageBackground};
position: fixed;
width: 566px;
height: 112px;
z-index: 1;
top: 0;
`;
const AppsmithLogoImg = styled.img`
max-width: 170px;
`;
const SpaceFiller = styled.div`
height: 100px;
`;
export type DetailsFormValues = {
name?: string;
firstName?: string;
lastName?: string;
email?: string;
password?: string;
verifyPassword?: string;
@ -76,20 +54,20 @@ export type DetailsFormValues = {
const validate = (values: DetailsFormValues) => {
const errors: DetailsFormValues = {};
if (!values.name) {
errors.name = "Please enter a valid Full Name";
if (!values.firstName) {
errors.firstName = "This field is required.";
}
if (!values.email || !isEmail(values.email)) {
errors.email = "Please enter a valid Email address";
errors.email = "Enter a valid email address.";
}
if (!values.password || !isStrongPassword(values.password)) {
errors.password = "Please enter a strong password";
errors.password = "Please enter a strong password.";
}
if (!values.verifyPassword || values.password != values.verifyPassword) {
errors.verifyPassword = "Please reenter the password";
errors.verifyPassword = "Passwords don't match.";
}
if (!values.role) {
@ -120,15 +98,35 @@ export type SetupFormProps = DetailsFormValues & {
>;
function SetupForm(props: SetupFormProps) {
const isAirgappedInstance = isAirgapped();
const signupURL = `/api/v1/${SUPER_USER_SUBMIT_PATH}`;
const [showDetailsForm, setShowDetailsForm] = useState(true);
const formRef = useRef<HTMLFormElement>(null);
const isAirgappedFlag = isAirgapped();
const onSubmit = () => {
const form: HTMLFormElement = formRef.current as HTMLFormElement;
const verifyPassword: HTMLInputElement = document.querySelector(
`[name="verifyPassword"]`,
) as HTMLInputElement;
verifyPassword.removeAttribute("name");
const firstName: HTMLInputElement = document.querySelector(
`[name="firstName"]`,
) as HTMLInputElement;
const lastName: HTMLInputElement = document.querySelector(
`[name="lastName"]`,
) as HTMLInputElement;
if (firstName && lastName) {
const fullName = document.createElement("input");
fullName.type = "text";
fullName.name = "name";
fullName.style.display = "none";
fullName.value = `${firstName.value} ${lastName.value}`;
form.appendChild(fullName);
}
const roleInput = document.createElement("input");
verifyPassword.removeAttribute("name");
roleInput.type = "text";
@ -138,10 +136,6 @@ function SetupForm(props: SetupFormProps) {
roleInput.value = props.role as string;
} else {
roleInput.value = props.role_name as string;
const roleNameInput: HTMLInputElement = document.querySelector(
`[name="role_name"]`,
) as HTMLInputElement;
if (roleNameInput) roleNameInput.remove();
}
form.appendChild(roleInput);
const useCaseInput = document.createElement("input");
@ -152,12 +146,20 @@ function SetupForm(props: SetupFormProps) {
useCaseInput.value = props.useCase as string;
} else {
useCaseInput.value = props.custom_useCase as string;
const customUseCaseInput: HTMLInputElement = document.querySelector(
`[name="custom_useCase"]`,
) as HTMLInputElement;
if (customUseCaseInput) customUseCaseInput.remove();
}
form.appendChild(useCaseInput);
const anonymousDataInput = document.createElement("input");
anonymousDataInput.type = "checkbox";
anonymousDataInput.value = isAirgappedFlag ? "false" : "true";
anonymousDataInput.checked = isAirgappedFlag ? false : true;
anonymousDataInput.name = "allowCollectingAnonymousData";
anonymousDataInput.style.display = "none";
form.appendChild(anonymousDataInput);
const signupForNewsletter: HTMLInputElement = document.querySelector(
`[name="signupForNewsletter"]`,
) as HTMLInputElement;
if (signupForNewsletter)
signupForNewsletter.value = signupForNewsletter.checked.toString();
return true;
};
@ -191,9 +193,6 @@ function SetupForm(props: SetupFormProps) {
return (
<PageWrapper>
<SetupFormContainer>
<LogoContainer>
<AppsmithLogoImg alt="Appsmith logo" src={AppsmithLogo} />
</LogoContainer>
<form
action={signupURL}
data-testid="super-user-form"
@ -204,17 +203,8 @@ function SetupForm(props: SetupFormProps) {
ref={formRef}
>
<SetupStep active={showDetailsForm}>
<DetailsForm
{...props}
onNext={!isAirgappedInstance ? onNext : () => noop}
/>
<DetailsForm {...props} onNext={onNext} />
</SetupStep>
{!isAirgappedInstance && (
<SetupStep active={!showDetailsForm}>
<DataCollectionForm />
<NewsletterForm />
</SetupStep>
)}
</form>
<SpaceFiller />
</SetupFormContainer>

View File

@ -1,4 +1,4 @@
import React, { memo, useState } from "react";
import React, { memo } from "react";
import styled from "styled-components";
import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants";
import { useEffect } from "react";
@ -11,49 +11,64 @@ import {
import NonSuperUserForm, { SuperUserForm } from "./GetStarted";
import { getAssetUrl } from "@appsmith/utils/airgapHelpers";
const LandingPageWrapper = styled.div<{ hide: boolean }>`
width: ${(props) => props.theme.pageContentWidth}px;
const LandingPageWrapper = styled.div`
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
align-items: flex-start;
margin: 0 auto;
opacity: ${(props) => (props.hide ? 0 : 1)};
`;
const LandingPageContent = styled.div`
width: 100%;
height: 100%;
display: flex;
align-items: center;
align-items: start;
justify-content: center;
position: relative;
z-index: 100;
`;
const StyledTextBanner = styled.div`
min-width: ${(props) => props.theme.pageContentWidth * 0.55}px;
padding-left: 64px;
width: 60%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 6%;
`;
const StyledBannerHeader = styled.h1`
font-family: "Paytone One", sans-serif;
const StyledBannerHeader = styled.div`
font-size: 72px;
margin: 0px 0px;
color: var(--ads-v2-color-fg-emphasis-plus);
font-weight: 600;
margin-right: 1rem;
width: 100%;
text-align: center;
`;
const StyledBannerBody = styled.p`
font-family: "Montserrat", sans-serif;
const StyledBannerBody = styled.div`
font-size: 24px;
margin: ${(props) => props.theme.spaces[7]}px 0px;
width: 400px;
font-weight: 500;
margin-right: 8rem;
width: 100%;
text-align: center;
color: var(--ads-v2-color-fg);
`;
const StyledImageBanner = styled.div`
min-width: ${(props) => props.theme.pageContentWidth * 0.45}px;
width: 40%;
display: flex;
justify-content: center;
height: 100%;
flex-direction: column;
align-items: end;
`;
const getWelcomeImage = () => `${ASSETS_CDN_URL}/welcome-banner.svg`;
const getWelcomeImage = () => `${ASSETS_CDN_URL}/welcome-banner-v2.svg`;
const getAppsmithLogo = () => `${ASSETS_CDN_URL}/appsmith-logo.svg`;
type LandingPageProps = {
onGetStarted?: (role?: string, useCase?: string) => void;
@ -62,25 +77,6 @@ type LandingPageProps = {
const WELCOME_PAGE_ANIMATION_CONTAINER = "welcome-page-animation-container";
const includeFonts = () => {
const preconnectGoogleapis = document.createElement("link");
preconnectGoogleapis.rel = "preconnect";
preconnectGoogleapis.href = "https://fonts.googleapis.com";
document.head.appendChild(preconnectGoogleapis);
const preconnectGstatic = document.createElement("link") as any;
preconnectGstatic.rel = "preconnect";
preconnectGstatic.href = "https://fonts.gstatic.com";
preconnectGstatic.crossorigin = "crossorigin";
document.head.appendChild(preconnectGstatic);
const fonts = document.createElement("link");
fonts.rel = "stylesheet";
fonts.href =
"https://fonts.googleapis.com/css2?family=Montserrat&family=Paytone+One&display=swap";
document.head.appendChild(fonts);
};
function Banner() {
return (
<>
@ -91,32 +87,30 @@ function Banner() {
}
export default memo(function LandingPage(props: LandingPageProps) {
const [fontsInjected, setFontsInjected] = useState(false);
useEffect(() => {
includeFonts();
playWelcomeAnimation(`#${WELCOME_PAGE_ANIMATION_CONTAINER}`);
//wait for the fonts to be loaded
setTimeout(() => {
setFontsInjected(true);
}, 100);
}, []);
return (
<LandingPageWrapper
data-testid={"welcome-page"}
hide={!fontsInjected}
id={WELCOME_PAGE_ANIMATION_CONTAINER}
>
<LandingPageContent>
<StyledTextBanner>
<Banner />
{props.forSuperUser ? (
<SuperUserForm onGetStarted={props.onGetStarted} />
<SuperUserForm />
) : (
<NonSuperUserForm onGetStarted={props.onGetStarted} />
)}
</StyledTextBanner>
<StyledImageBanner>
<img src={getAssetUrl(getWelcomeImage())} />
<div className="flex self-start w-2/6 h-16 ml-56">
<img src={getAssetUrl(getAppsmithLogo())} />
</div>
<div className="flex w-5/6 my-1 h-4/6">
<img className="w-full" src={getAssetUrl(getWelcomeImage())} />
</div>
</StyledImageBanner>
</LandingPageContent>
</LandingPageWrapper>

View File

@ -60,9 +60,6 @@ export const StyledLink = styled.a`
const DROPDOWN_CLASSNAME = "setup-dropdown";
export const DropdownWrapper = styled(StyledFormGroup)`
&& {
margin-bottom: 33px;
}
&& .cs-text {
width: 100%;
}

View File

@ -20,6 +20,8 @@ export const STORAGE_KEYS: {
"FIRST_TIME_USER_ONBOARDING_INTRO_MODAL_VISIBILITY",
HIDE_CONCURRENT_EDITOR_WARNING_TOAST: "HIDE_CONCURRENT_EDITOR_WARNING_TOAST",
APP_THEMING_BETA_SHOWN: "APP_THEMING_BETA_SHOWN",
FIRST_TIME_USER_ONBOARDING_TELEMETRY_CALLOUT_VISIBILITY:
"FIRST_TIME_USER_ONBOARDING_TELEMETRY_CALLOUT_VISIBILITY",
};
const store = localforage.createInstance({
@ -384,3 +386,34 @@ export const setTemplateNotificationSeen = async (flag: boolean) => {
return false;
}
};
export const getFirstTimeUserOnboardingTelemetryCalloutIsAlreadyShown =
async () => {
try {
const flag = await store.getItem(
STORAGE_KEYS.FIRST_TIME_USER_ONBOARDING_TELEMETRY_CALLOUT_VISIBILITY,
);
return flag;
} catch (error) {
log.error(
"An error occurred while fetching FIRST_TIME_USER_ONBOARDING_TELEMETRY_CALLOUT_VISIBILITY",
);
log.error(error);
}
};
export const setFirstTimeUserOnboardingTelemetryCalloutVisibility = async (
flag: boolean,
) => {
try {
await store.setItem(
STORAGE_KEYS.FIRST_TIME_USER_ONBOARDING_TELEMETRY_CALLOUT_VISIBILITY,
flag,
);
return true;
} catch (error) {
log.error(
"An error occurred while fetching FIRST_TIME_USER_ONBOARDING_TELEMETRY_CALLOUT_VISIBILITY",
);
log.error(error);
}
};