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
This commit is contained in:
balajisoundar 2021-09-12 22:06:43 +05:30 committed by GitHub
parent b949b7030f
commit 95c729d7d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 782 additions and 15 deletions

View File

@ -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<any, any> {
component={UnsubscribeEmail}
path={UNSUBSCRIBE_EMAIL_URL}
/>
<SentryRoute component={Setup} path={SETUP} />
<SentryRoute component={PageNotFound} />
</Switch>
</>

View File

@ -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) => ({

View File

@ -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<ApiResponse> {
return Api.get(UserApi.featureFlagsURL);
}
static createSuperUser(
request: CreateSuperUserRequest,
): AxiosPromise<CreateUserResponse> {
return Api.post(UserApi.superUserURL, request);
}
}
export default UserApi;

View File

@ -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) {
<input
checked={value}
disabled={props.disabled || props.isLoading}
name={props.name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChangeHandler(e.target.checked)
}
type="checkbox"
value={value ? "true" : "false"}
/>
<span className="slider" />
{props.isLoading ? (

View File

@ -26,7 +26,7 @@ const renderComponent = (
);
};
type FormTextFieldProps = {
export type FormTextFieldProps = {
name: string;
placeholder: string;
type?: InputType;

View File

@ -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}`;

View File

@ -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 = {

View File

@ -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";

View File

@ -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";

View File

@ -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;

View File

@ -8,6 +8,7 @@ export type User = {
username: string;
name: string;
gender: Gender;
emptyInstance?: boolean;
};
export interface UserApplication {

View File

@ -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 <Redirect to={SETUP} />;
}
if (queryParams.get("error")) {
showError = true;
}

View File

@ -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<Props, any> {
<Route component={AppEditorHeader} path={BUILDER_URL} />
<Route component={AppViewerHeader} path={APP_VIEW_URL} />
<Route component={LoginHeader} path={USER_AUTH_URL} />
<Route path={SETUP} />
<Route component={PageHeader} path={BASE_URL} />
</Switch>
);

View File

@ -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 (
<DataCollectionFormWrapper>
<FormHeaderWrapper>
<FormHeaderIndex>2.</FormHeaderIndex>
<FormHeaderLabel>Usage data preference</FormHeaderLabel>
<FormHeaderSubtext>
Your data. Your choice. Data is collected anonymously. <br />
<StyledLink href={TELEMETRY_URL} target="_blank">
List of tracked items
</StyledLink>
</FormHeaderSubtext>
</FormHeaderWrapper>
<FormBodyWrapper>
<ControlWrapper>
<AllowToggleWrapper>
<AllowToggle>
<Toggle
name="allowCollectingAnonymousData"
onToggle={() => noop}
value
/>
</AllowToggle>
<AllowToggleLabel>
Allow Appsmith to collect usage data anonymously
</AllowToggleLabel>
</AllowToggleWrapper>
</ControlWrapper>
</FormBodyWrapper>
</DataCollectionFormWrapper>
);
});

View File

@ -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<WrappedFieldMetaProps>;
input: Partial<WrappedFieldInputProps>;
},
) {
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 (
<Dropdown
onSelect={onSelect}
options={options}
selected={selected}
showLabelOnly
width="260px"
/>
);
};
}
export default function DetailsForm(
props: InjectedFormProps & DetailsFormValues & { onNext?: () => void },
) {
const ref = React.createRef<HTMLDivElement>();
return (
<DetailsFormWrapper ref={ref}>
<FormHeaderWrapper>
<FormHeaderIndex>1.</FormHeaderIndex>
<FormHeaderLabel>Let us get to know you better!</FormHeaderLabel>
</FormHeaderWrapper>
<StyledFormBodyWrapper>
<StyledFormGroup label={createMessage(() => "Full Name")}>
<FormTextField
autoFocus
name="name"
placeholder="John Doe"
type="text"
/>
</StyledFormGroup>
<StyledFormGroup label={createMessage(() => "Email Id")}>
<FormTextField
name="email"
placeholder="How can we reach you?"
type="email"
/>
</StyledFormGroup>
<StyledFormGroup label={createMessage(() => "Create Password")}>
<FormTextField
name="password"
placeholder="Make it strong!"
type="password"
/>
</StyledFormGroup>
<StyledFormGroup label={createMessage(() => "Verify Password")}>
<FormTextField
name="verifyPassword"
placeholder="Type correctly"
type="password"
/>
</StyledFormGroup>
<DropdownWrapper label={createMessage(() => "What Role Do You Play?")}>
<Field
asyncControl
component={withDropdown(roleOptions)}
name="role"
placeholder=""
type="text"
/>
</DropdownWrapper>
{props.role == "other" && (
<StyledFormGroup label={createMessage(() => "Role")}>
<FormTextField name="role_name" placeholder="" type="text" />
</StyledFormGroup>
)}
<DropdownWrapper
label={createMessage(() => "Tell Us About Your Use Case")}
>
<Field
asyncControl
component={withDropdown(useCaseOptions)}
name="useCase"
placeholder=""
type="text"
/>
</DropdownWrapper>
<ButtonWrapper>
<Button
category={Category.tertiary}
disabled={props.invalid}
onClick={props.onNext}
size={Size.medium}
tag="button"
text="Next"
type="button"
/>
</ButtonWrapper>
</StyledFormBodyWrapper>
</DetailsFormWrapper>
);
}

View File

@ -0,0 +1,92 @@
import React, { memo } from "react";
import styled from "styled-components";
import AppsmithLogo from "assets/images/appsmith_logo.png";
import Button, { Category, Size } from "components/ads/Button";
import { StyledLink } from "./common";
import { DISCORD_URL } from "constants/ThirdPartyConstants";
import { useEffect } from "react";
import { playOnboardingAnimation } from "utils/helpers";
const LandingPageWrapper = styled.div`
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
`;
const LandingPageContent = styled.div`
width: 735px;
text-align: center;
`;
const LogoContainer = styled.div``;
const AppsmithLogoImg = styled.img`
max-width: 170px;
`;
const ActionContainer = styled.div`
margin-top: 32px;
`;
const StyledBanner = styled.h2`
margin: 16px 0px;
font-weight: 500;
color: ${(props) => props.theme.colors.welcomePage.text};
`;
const StyledButton = styled(Button)`
width: 136px;
height: 38px;
margin: 0 auto;
`;
const Footer = styled.div`
position: fixed;
bottom: 24px;
left: 50%;
transform: translate(-50%, 0);
`;
type LandingPageProps = {
onGetStarted: () => void;
};
export default memo(function LandingPage(props: LandingPageProps) {
useEffect(() => {
playOnboardingAnimation();
}, []);
return (
<LandingPageWrapper>
<LandingPageContent>
<LogoContainer>
<AppsmithLogoImg alt="Appsmith logo" src={AppsmithLogo} />
</LogoContainer>
<StyledBanner>
Thank you for trying Appsmith.
<br />
Youll be building your new app very soon!
</StyledBanner>
<StyledBanner>
We have a few questions to set up your account.
</StyledBanner>
<ActionContainer>
<StyledButton
category={Category.primary}
onClick={props.onGetStarted}
size={Size.medium}
tag="button"
text="Get Started"
/>
</ActionContainer>
<Footer>
For more queries reach us on our&nbsp;
<StyledLink href={DISCORD_URL} rel="noreferrer" target="_blank">
Discord Server
</StyledLink>
</Footer>
</LandingPageContent>
</LandingPageWrapper>
);
});

View File

@ -0,0 +1,57 @@
import { noop } from "lodash";
import React from "react";
import styled from "styled-components";
import Button, { Category, Size } from "components/ads/Button";
import Toggle from "components/ads/Toggle";
import {
AllowToggle,
AllowToggleLabel,
AllowToggleWrapper,
ButtonWrapper,
FormBodyWrapper,
FormHeaderIndex,
FormHeaderLabel,
FormHeaderWrapper,
} from "./common";
import { memo } from "react";
export const StyledButton = styled(Button)`
width: 201px;
height: 38px;
`;
const NewsletterContainer = styled.div`
widht: 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>
<FormHeaderWrapper>
<FormHeaderIndex>3.</FormHeaderIndex>
<FormHeaderLabel>Stay in touch</FormHeaderLabel>
</FormHeaderWrapper>
<FormBodyWrapper>
<AllowToggleWrapper>
<AllowToggle>
<Toggle name="signupForNewsletter" onToggle={() => noop} value />
</AllowToggle>
<AllowToggleLabel>
Get updates about what we are cooking. We do not spam you.
</AllowToggleLabel>
</AllowToggleWrapper>
<ButtonWrapper>
<StyledButton
category={Category.primary}
size={Size.medium}
tag="button"
text="Make your first App"
/>
</ButtonWrapper>
</FormBodyWrapper>
</NewsletterContainer>
);
});

View File

@ -0,0 +1,179 @@
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,
WELCOME_FORM_NAME,
WELCOME_FORM_NAME_FIELD_NAME,
WELCOME_FORM_PASSWORD_FIELD_NAME,
WELCOME_FORM_ROLE_FIELD_NAME,
WELCOME_FORM_ROLE_NAME_FIELD_NAME,
WELCOME_FORM_VERIFY_PASSWORD_FIELD_NAME,
} from "constants/forms";
import { formValueSelector, InjectedFormProps, reduxForm } from "redux-form";
import { isEmail, isStrongPassword } from "utils/formhelpers";
import { AppState } from "reducers";
import { SUPER_USER_SUBMIT_PATH } from "constants/ApiConstants";
import { useState } from "react";
const PageWrapper = styled.div`
width: 100%;
display: flex;
justify-content: center;
`;
const SetupFormContainer = styled.div`
width: 566px;
padding-top: 120px;
`;
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;
email?: string;
password?: string;
verifyPassword?: string;
role?: string;
useCase?: string;
role_name?: string;
};
const validate = (values: DetailsFormValues) => {
const errors: DetailsFormValues = {};
if (!values.name) {
errors.name = "Please enter a valid Full Name";
}
if (!values.email || !isEmail(values.email)) {
errors.email = "Please enter a valid Email address";
}
if (!values.password || !isStrongPassword(values.password)) {
errors.password = "Please enter a strong password";
}
if (!values.verifyPassword || values.password != values.verifyPassword) {
errors.verifyPassword = "Please reenter the password";
}
if (!values.role) {
errors.role = "Please select a role";
}
if (values.role == "other" && !values.role_name) {
errors.role_name = "Please enter a role";
}
if (!values.useCase) {
errors.useCase = "Please select a use case";
}
return errors;
};
function SetupForm(props: InjectedFormProps & DetailsFormValues) {
const signupURL = `/api/v1/${SUPER_USER_SUBMIT_PATH}`;
const [showDetailsForm, setShowDetailsForm] = useState(true);
const formRef = useRef<HTMLFormElement>(null);
const onSubmit = () => {
const form: HTMLFormElement = formRef.current as HTMLFormElement;
const verifyPassword: HTMLInputElement = document.querySelector(
`[name="verifyPassword"]`,
) as HTMLInputElement;
const roleInput = document.createElement("input");
verifyPassword.removeAttribute("name");
roleInput.type = "text";
roleInput.name = "role";
roleInput.style.display = "none";
if (props.role != "other") {
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");
useCaseInput.type = "text";
useCaseInput.name = "useCase";
useCaseInput.value = props.useCase as string;
useCaseInput.style.display = "none";
form.appendChild(useCaseInput);
return true;
};
return (
<PageWrapper>
<SetupFormContainer>
<LogoContainer>
<AppsmithLogoImg alt="Appsmith logo" src={AppsmithLogo} />
</LogoContainer>
<form
action={signupURL}
id="super-user-form"
method="POST"
onSubmit={onSubmit}
ref={formRef}
>
<SetupStep active={showDetailsForm}>
<DetailsForm {...props} onNext={() => setShowDetailsForm(false)} />
</SetupStep>
<SetupStep active={!showDetailsForm}>
<DataCollectionForm />
<NewsletterForm />
</SetupStep>
</form>
<SpaceFiller />
</SetupFormContainer>
</PageWrapper>
);
}
const selector = formValueSelector(WELCOME_FORM_NAME);
export default connect((state: AppState) => {
return {
name: selector(state, WELCOME_FORM_NAME_FIELD_NAME),
email: selector(state, WELCOME_FORM_EMAIL_FIELD_NAME),
password: selector(state, WELCOME_FORM_PASSWORD_FIELD_NAME),
verify_password: selector(state, WELCOME_FORM_VERIFY_PASSWORD_FIELD_NAME),
role: selector(state, WELCOME_FORM_ROLE_FIELD_NAME),
role_name: selector(state, WELCOME_FORM_ROLE_NAME_FIELD_NAME),
useCase: selector(state, WELCOME_FORM_USECASE_FIELD_NAME),
};
}, null)(
reduxForm<DetailsFormValues>({
validate,
form: WELCOME_FORM_NAME,
touchOnBlur: true,
})(SetupForm),
);

View File

@ -0,0 +1,60 @@
import styled from "styled-components";
export const FormHeaderWrapper = styled.div`
position: relative;
`;
export const FormHeaderLabel = styled.h5`
width: 100%;
font-size: 20px;
margin: 8px 0 16px;
font-weight: 500;
`;
export const FormHeaderIndex = styled.h5`
font-size: 20px;
font-weight: 500;
position: absolute;
left: -33px;
top: -33px;
`;
export const FormBodyWrapper = styled.div`
padding: ${(prop) => prop.theme.spaces[10]}px 0px;
`;
export const FormHeaderSubtext = styled.p``;
export const ControlWrapper = styled.div`
margin: ${(prop) => prop.theme.spaces[6]}px 0px;
`;
export const Label = styled.label`
display: inline-block;
margin-bottom: 10px;
`;
export const ButtonWrapper = styled.div`
margin: ${(prop) => prop.theme.spaces[17] * 2}px 0px 0px;
`;
export const AllowToggleWrapper = styled.div`
display: flex;
`;
export const AllowToggle = styled.div`
flex-basis: 68px;
`;
export const AllowToggleLabel = styled.p`
margin-bottom: 0px;
margin-top: 2px;
`;
export const StyledLink = styled.a`
&,
&:hover {
color: ${(props) => props.theme.colors.link};
text-decoration: none;
}
`;

View File

@ -0,0 +1,50 @@
export type OptionType = {
label?: string;
value?: string;
};
export const roleOptions: OptionType[] = [
{
label: "Engineer",
value: "engineer",
},
{
label: "Product manager",
value: "product manager",
},
{
label: "Founder",
value: "founder",
},
{
label: "Operations",
value: "operations",
},
{
label: "Business Analyst",
value: "business analyst",
},
{
label: "Other",
value: "other",
},
];
export const useCaseOptions: OptionType[] = [
{
label: "Just Exploring",
value: "just exploring",
},
{
label: "Personal Project",
value: "personal project",
},
{
label: "Work Project",
value: "work project",
},
{
label: "Other",
value: "other",
},
];

View File

@ -0,0 +1,35 @@
import React from "react";
import LandingPage from "./Landing";
import SetupForm from "./SetupForm";
import requiresAuthHOC from "pages/UserAuth/requiresAuthHOC";
import { useState } from "react";
import styled from "styled-components";
import { useSelector } from "react-redux";
import { getCurrentUser } from "selectors/usersSelectors";
import { AUTH_LOGIN_URL } from "constants/routes";
import { Redirect } from "react-router";
const StyledSetupContainer = styled.div`
background-color: ${(props) => props.theme.colors.homepageBackground};
height: 100vh;
overflow: auto;
`;
function Setup() {
const user = useSelector(getCurrentUser);
const [showLandingPage, setShowLandingPage] = useState<boolean>(true);
if (!user?.emptyInstance) {
return <Redirect to={AUTH_LOGIN_URL} />;
}
return (
<StyledSetupContainer>
{showLandingPage ? (
<LandingPage onGetStarted={() => setShowLandingPage(false)} />
) : (
<SetupForm />
)}
</StyledSetupContainer>
);
}
export default requiresAuthHOC(Setup);

View File

@ -121,11 +121,22 @@ const usersReducer = createReducer(initialState, {
...state,
current: action.payload,
}),
[ReduxActionTypes.LOGOUT_USER_SUCCESS]: (state: UsersReduxState) => ({
[ReduxActionTypes.LOGOUT_USER_SUCCESS]: (
state: UsersReduxState,
action: ReduxAction<boolean>,
) => ({
...state,
current: undefined,
currentUser: DefaultCurrentUserDetails,
users: [DefaultCurrentUserDetails],
currentUser: {
...DefaultCurrentUserDetails,
emptyInstance: action.payload,
},
users: [
{
...DefaultCurrentUserDetails,
emptyInstance: action.payload,
},
],
}),
[ReduxActionTypes.FETCH_FEATURE_FLAGS_SUCCESS]: (state: UsersReduxState) => ({
...state,

View File

@ -15,7 +15,12 @@ import UserApi, {
UpdateUserRequest,
LeaveOrgRequest,
} from "api/UserApi";
import { APPLICATIONS_URL, AUTH_LOGIN_URL, BASE_URL } from "constants/routes";
import {
APPLICATIONS_URL,
AUTH_LOGIN_URL,
BASE_URL,
SETUP,
} from "constants/routes";
import history from "utils/history";
import { ApiResponse } from "api/ApiResponses";
import {
@ -109,17 +114,19 @@ export function* getCurrentUserSaga() {
// reset the flagsFetched flag
yield put(fetchFeatureFlagsSuccess());
}
if (window.location.pathname === BASE_URL) {
yield put({
type: ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS,
payload: response.data,
});
if (response.data.emptyInstance) {
history.replace(SETUP);
} else if (window.location.pathname === BASE_URL) {
if (response.data.isAnonymous) {
history.replace(AUTH_LOGIN_URL);
} else {
history.replace(APPLICATIONS_URL);
}
}
yield put({
type: ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS,
payload: response.data,
});
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.USER_ME_API,
);
@ -358,7 +365,8 @@ export function* logoutSaga(action: ReduxAction<{ redirectURL: string }>) {
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
AnalyticsUtil.reset();
yield put(logoutUserSuccess());
const currentUser = yield select(getCurrentUser);
yield put(logoutUserSuccess(!!currentUser?.emptyInstance));
localStorage.clear();
yield put(flushErrorsAndRedirect(redirectURL || AUTH_LOGIN_URL));
}