Add username to onboarding (#12697)

* Add username to onboarding

* Add margin to profile form error alert

* Update user controller for onboarding

Co-authored-by: Michael Kohl <me@citizen428.net>
This commit is contained in:
Jacob Herrington 2021-02-24 10:48:31 -06:00 committed by GitHub
parent 5bed8f56d4
commit dd3121e91c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 108 additions and 95 deletions

View file

@ -10,7 +10,7 @@ class UsersController < ApplicationController
before_action :set_suggested_users, only: %i[index]
before_action :initialize_stripe, only: %i[edit]
ALLOWED_USER_PARAMS = %i[last_onboarding_page].freeze
ALLOWED_USER_PARAMS = %i[last_onboarding_page username].freeze
INDEX_ATTRIBUTES_FOR_SERIALIZATION = %i[id name username summary profile_image].freeze
private_constant :INDEX_ATTRIBUTES_FOR_SERIALIZATION
@ -165,19 +165,20 @@ class UsersController < ApplicationController
end
def onboarding_update
if params[:user]
sanitize_user_params
current_user.assign_attributes(params[:user].permit(ALLOWED_USER_PARAMS))
current_user.profile_updated_at = Time.current
end
if current_user.save && params[:profile]
update_result = Profiles::Update.call(current_user, { profile: profile_params })
end
current_user.saw_onboarding = true
authorize User
render_update_response(update_result&.success?)
user_params = { saw_onboarding: true }
if params[:user]
if params.dig(:user, :username).blank?
return render_update_response(false, "Username cannot be blank")
end
sanitize_user_params
user_params.merge!(params[:user].permit(ALLOWED_USER_PARAMS))
end
update_result = Profiles::Update.call(current_user, { user: user_params, profile: profile_params })
render_update_response(update_result.success?, update_result.errors_as_sentence)
end
def onboarding_checkbox_update
@ -313,11 +314,11 @@ class UsersController < ApplicationController
recent_suggestions.presence || default_suggested_users
end
def render_update_response(success)
outcome = success ? "updated successfully" : "update failed"
def render_update_response(success, errors = nil)
status = success ? 200 : 422
respond_to do |format|
format.json { render json: { outcome: outcome } }
format.json { render json: { errors: errors }, status: status }
end
end
@ -376,7 +377,7 @@ class UsersController < ApplicationController
end
def profile_params
params[:profile].permit(Profile.attributes)
params[:profile] ? params[:profile].permit(Profile.attributes) : nil
end
def password_params

View file

@ -133,7 +133,7 @@ describe('<Onboarding />', () => {
termsCheckbox.click();
// click to next step
const nextButton = await findByText(/continue/i);
let nextButton = await findByText(/continue/i);
fetch.mockResponse(fakeEmptyResponse);
nextButton.click();
@ -148,10 +148,10 @@ describe('<Onboarding />', () => {
// we should be on the Profile Form step
await findByTestId('onboarding-profile-form');
// click on skip for now
skipButton = getByText(/Skip for now/i);
// click on continue without adjusting form fields
nextButton = getByText(/Continue/i);
fetch.mockResponse(fakeEmptyResponse);
skipButton.click();
nextButton.click();
// we should be on the Follow Users step
await findByTestId('onboarding-follow-users');

View file

@ -1,7 +1,7 @@
import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import { userData, getContentOfToken, updateOnboarding } from '../utilities';
import { userData, updateOnboarding } from '../utilities';
import { Navigation } from './Navigation';
import { ColorPicker } from './ProfileForm/ColorPicker';
@ -22,8 +22,8 @@ export class ProfileForm extends Component {
this.user = userData();
this.state = {
groups: [],
formValues: {},
canSkip: true,
formValues: { username: this.user.username },
canSkip: false,
last_onboarding_page: 'v2: personal info form',
};
}
@ -47,26 +47,34 @@ export class ProfileForm extends Component {
}
}
onSubmit() {
const csrfToken = getContentOfToken('csrf-token');
async onSubmit() {
const { formValues, last_onboarding_page } = this.state;
fetch('/onboarding_update', {
method: 'PATCH',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
user: { last_onboarding_page },
profile: { ...formValues },
}),
credentials: 'same-origin',
}).then((response) => {
if (response.ok) {
const { next } = this.props;
next();
const { username, ...newFormValues } = formValues;
try {
const response = await request('/onboarding_update', {
method: 'PATCH',
body: {
user: { last_onboarding_page, username },
profile: { ...newFormValues },
},
});
if (!response.ok) {
throw response;
}
});
const { next } = this.props;
next();
} catch (error) {
Honeybadger.notify(error.statusText);
let errorMessage = 'Unable to continue, please try again.';
if (error.status === 422) {
// parse validation error messages from UsersController#onboarding_update
const errorData = await error.json();
errorMessage = errorData.errors;
this.setState({ error: true, errorMessage });
} else {
this.setState({ error: true, errorMessage });
}
}
}
handleFieldChange(e) {
@ -153,14 +161,6 @@ export class ProfileForm extends Component {
const { profile_image_90, username, name } = this.user;
const { canSkip, groups = [], error, errorMessage } = this.state;
if (error) {
return (
<div role="alert" class="crayons-notice crayons-notice--danger">
An error occurred: {errorMessage}
</div>
);
}
const sections = groups.map((group) => {
return (
<div key={group.id} class="onboarding-profile-sub-section">
@ -195,6 +195,11 @@ export class ProfileForm extends Component {
slidesCount={slidesCount}
currentSlideIndex={currentSlideIndex}
/>
{error && (
<div role="alert" class="crayons-notice crayons-notice--danger m-2">
An error occurred: {errorMessage}
</div>
)}
<div className="onboarding-content about">
<header className="onboarding-content-header">
<h1 id="title" className="title">
@ -219,9 +224,19 @@ export class ProfileForm extends Component {
/>
</figure>
<h3>{name}</h3>
<p>{username}</p>
</div>
<div>{sections}</div>
<div className="onboarding-profile-sub-section">
<TextInput
field={{
attribute_name: 'username',
label: 'Username',
default_value: username,
required: true,
}}
onFieldChange={this.handleFieldChange}
/>
</div>
{sections}
</div>
</div>
</div>

View file

@ -25,8 +25,15 @@ import PropTypes from 'prop-types';
import { FormField } from '@crayons';
export function TextInput(props) {
const { onFieldChange } = props;
const { attribute_name, placeholder_text, description, label } = props.field;
const { onFieldChange, field } = props;
const {
attribute_name,
placeholder_text,
default_value,
description,
label,
required,
} = field;
return (
<FormField>
@ -36,9 +43,11 @@ export function TextInput(props) {
<input
class="crayons-textfield"
placeholder={placeholder_text}
defaultValue={default_value}
name={attribute_name}
id={attribute_name}
onChange={onFieldChange}
required={required ? 'required' : ''}
/>
{description && <p class="crayons-field__description">{description}</p>}
</FormField>

View file

@ -1,5 +1,5 @@
import { h } from 'preact';
import { render, fireEvent } from '@testing-library/preact';
import { render } from '@testing-library/preact';
import fetch from 'jest-fetch-mock';
import '@testing-library/jest-dom';
import { axe } from 'jest-axe';
@ -130,14 +130,17 @@ describe('ProfileForm', () => {
const { findByLabelText } = renderProfileForm();
const field1 = await findByLabelText(/Education/i);
const field2 = await findByLabelText(/Name/i);
const field2 = await findByLabelText(/^Name/i);
const field3 = await findByLabelText(/Website URL/i);
const field4 = await findByLabelText(/Username/i);
expect(field1).toBeInTheDocument();
expect(field2).toBeInTheDocument();
expect(field2.getAttribute('placeholder')).toEqual('John Doe');
expect(field3).toBeInTheDocument();
expect(field3.getAttribute('placeholder')).toEqual('https://yoursite.com');
expect(field4).toBeInTheDocument();
expect(field4.getAttribute('value')).toEqual('username');
});
it('should render a stepper', () => {
@ -152,44 +155,9 @@ describe('ProfileForm', () => {
expect(queryByTestId('back-button')).toBeDefined();
});
it('should update the text on the forward button', async () => {
const {
getByLabelText,
getByText,
queryByText,
findByLabelText,
findByText,
} = renderProfileForm();
it('should not be skippable', async () => {
const { getByText } = renderProfileForm();
// input the bio
const field2 = await findByLabelText(/Name/i);
expect(field2.value).toEqual('');
getByText(/skip for now/i);
expect(queryByText(/continue/i)).toBeNull();
fireEvent.keyDown(field2, {
key: 'Enter',
keyCode: 13,
which: 13,
target: { value: 'Hong Kong Fuey' },
});
expect(field2.value).toEqual('Hong Kong Fuey');
// input the location too (since we're using firevent and it doesn't call the focus events
// that will trigger the continue )
let field3 = getByLabelText(/Website URL/i);
expect(field3.value).toEqual('');
fireEvent.keyDown(field3, {
key: 'Enter',
keyCode: 13,
which: 13,
target: { value: 'www.website.com' },
});
field3 = await findByLabelText(/Website URL/i);
expect(field3.value).toEqual('www.website.com');
findByText(/continue/i);
expect(getByText(/continue/i)).toBeInTheDocument();
});
});

View file

@ -13,12 +13,32 @@ RSpec.describe "UsersOnboarding", type: :request do
end
it "updates the user's last_onboarding_page attribute" do
params = { user: { last_onboarding_page: "v2: personal info form" } }
params = { user: { last_onboarding_page: "v2: personal info form", username: "test" } }
expect do
patch "/onboarding_update.json", params: params
end.to change(user, :last_onboarding_page)
end
it "updates the user's username attribute" do
params = { user: { username: "WilhuffTarkin" } }
expect do
patch "/onboarding_update.json", params: params
end.to change(user, :username).to("wilhufftarkin")
end
it "returns a 422 error if the username is blank" do
params = { user: { username: "" } }
patch "/onboarding_update.json", params: params
expect(response).to have_http_status(:unprocessable_entity)
end
it "updates the user's profile" do
params = { profile: { employer_name: "Galatic Empire" } }
expect do
patch "/onboarding_update.json", params: params
end.to change(user.profile, :employer_name).to("Galatic Empire")
end
it "does not update the user's last_onboarding_page if it is empty" do
params = { user: { last_onboarding_page: "" } }
expect do