Creator Onboarding: Creator Setup View (#14728)
Co-authored-by: Julianna Tetreault <32834804+juliannatetreault@users.noreply.github.com> Co-authored-by: Yhëhtozr <conlang2012@outlook.com> Co-authored-by: yheuhtozr <84892012+yheuhtozr@users.noreply.github.com> Co-authored-by: Nick Taylor <nick@dev.to>
This commit is contained in:
parent
a92b746783
commit
4a9f442354
17 changed files with 349 additions and 11 deletions
|
|
@ -41,7 +41,10 @@ function fetchBaseData() {
|
|||
// Assigning User
|
||||
if (checkUserLoggedIn()) {
|
||||
document.body.dataset.user = json.user;
|
||||
document.body.dataset.creator = json.creator;
|
||||
document.body.dataset.creatorOnboarding = json.creator_onboarding;
|
||||
browserStoreCache('set', json.user);
|
||||
|
||||
setTimeout(() => {
|
||||
if (typeof ga === 'function') {
|
||||
ga('set', 'userId', JSON.parse(json.user).id);
|
||||
|
|
@ -50,6 +53,8 @@ function fetchBaseData() {
|
|||
} else {
|
||||
// Ensure user data is not exposed if no one is logged in
|
||||
delete document.body.dataset.user;
|
||||
delete document.body.dataset.creator;
|
||||
delete document.body.dataset.creatorOnboarding;
|
||||
browserStoreCache('remove');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
app/controllers/admin/creator_settings_controller.rb
Normal file
38
app/controllers/admin/creator_settings_controller.rb
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
module Admin
|
||||
class CreatorSettingsController < Admin::ApplicationController
|
||||
ALLOWED_PARAMS = %i[community_name logo_svg primary_brand_color_hex invite_only_mode public].freeze
|
||||
|
||||
def new; end
|
||||
|
||||
def create
|
||||
extra_authorization
|
||||
ActiveRecord::Base.transaction do
|
||||
::Settings::Community.community_name = settings_params[:community_name]
|
||||
::Settings::General.logo_svg = settings_params[:logo_svg]
|
||||
::Settings::UserExperience.primary_brand_color_hex = settings_params[:primary_brand_color_hex]
|
||||
::Settings::Authentication.invite_only_mode = settings_params[:invite_only]
|
||||
::Settings::UserExperience.public = settings_params[:public]
|
||||
end
|
||||
# For this feature to work as expected for the time being, we must set the COC and TOS to true.
|
||||
# However, this is not a viable solution, as Forem Creators are required to see and check the
|
||||
# COC and TOS. Bypassing them in this manner will not do and we will need to rethink this solution.
|
||||
# TODO: Replace the current solution of setting the COC and TOS to true with a better, more
|
||||
# long-term solution for Forem Creators.
|
||||
current_user.update!(saw_onboarding: true, checked_code_of_conduct: true, checked_terms_and_conditions: true)
|
||||
redirect_to root_path
|
||||
rescue StandardError => e
|
||||
flash.now[:error] = e.message
|
||||
render new_admin_creator_setting_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extra_authorization
|
||||
not_authorized unless current_user.has_role?(:creator)
|
||||
end
|
||||
|
||||
def settings_params
|
||||
params.permit(ALLOWED_PARAMS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -18,7 +18,9 @@ class AsyncInfoController < ApplicationController
|
|||
broadcast: broadcast_data,
|
||||
param: request_forgery_protection_token,
|
||||
token: form_authenticity_token,
|
||||
user: user_data
|
||||
user: user_data,
|
||||
creator: user_is_a_creator,
|
||||
creator_onboarding: use_creator_onboarding
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -62,6 +64,14 @@ class AsyncInfoController < ApplicationController
|
|||
end.to_json
|
||||
end
|
||||
|
||||
def user_is_a_creator
|
||||
@user.creator?
|
||||
end
|
||||
|
||||
def use_creator_onboarding
|
||||
FeatureFlag.enabled?(:creator_onboarding) && user_is_a_creator
|
||||
end
|
||||
|
||||
def user_cache_key
|
||||
"user-info-#{current_user&.id}__
|
||||
#{current_user&.last_sign_in_at}__
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ HTMLDocument.prototype.ready = new Promise((resolve) => {
|
|||
return null;
|
||||
});
|
||||
|
||||
// If localStorage.getItem('shouldRedirectToOnboarding') is not set, i.e. null, that means we should redirect.
|
||||
function redirectableLocation() {
|
||||
return (
|
||||
window.location.pathname !== '/onboarding' &&
|
||||
window.location.pathname !== '/signout_confirm' &&
|
||||
window.location.pathname !== '/privacy'
|
||||
window.location.pathname !== '/privacy' &&
|
||||
window.location.pathname !== '/admin/creator_settings/new'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -25,6 +27,14 @@ function onboardingSkippable(currentUser) {
|
|||
);
|
||||
}
|
||||
|
||||
function onboardCreator(currentUser) {
|
||||
return (
|
||||
document.body.dataset.creator === 'true' &&
|
||||
document.body.dataset.creatorOnboarding === 'true' &&
|
||||
!currentUser.saw_onboarding
|
||||
);
|
||||
}
|
||||
|
||||
document.ready.then(
|
||||
getUserDataAndCsrfToken()
|
||||
.then(({ currentUser, csrfToken }) => {
|
||||
|
|
@ -32,7 +42,9 @@ document.ready.then(
|
|||
window.csrfToken = csrfToken;
|
||||
getUnopenedChannels();
|
||||
|
||||
if (redirectableLocation() && !onboardingSkippable(currentUser)) {
|
||||
if (redirectableLocation() && onboardCreator(currentUser)) {
|
||||
window.location = `${window.location.origin}/admin/creator_settings/new?referrer=${window.location}`;
|
||||
} else if (redirectableLocation() && !onboardingSkippable(currentUser)) {
|
||||
window.location = `${window.location.origin}/onboarding?referrer=${window.location}`;
|
||||
}
|
||||
})
|
||||
|
|
@ -46,6 +58,12 @@ window.InstantClick.on('change', () => {
|
|||
getUserDataAndCsrfToken()
|
||||
.then(({ currentUser }) => {
|
||||
if (
|
||||
redirectableLocation() &&
|
||||
localStorage.getItem('shouldRedirectToOnboarding') === null &&
|
||||
onboardCreator(currentUser)
|
||||
) {
|
||||
window.location = `${window.location.origin}/admin/creator_settings/new?referrer=${window.location}`;
|
||||
} else if (
|
||||
redirectableLocation() &&
|
||||
localStorage.getItem('shouldRedirectToOnboarding') === null &&
|
||||
!onboardingSkippable(currentUser)
|
||||
|
|
|
|||
9
app/models/creator_setting.rb
Normal file
9
app/models/creator_setting.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
class CreatorSetting < ApplicationRecord
|
||||
# This class exists to take advantage of Rolify for limiting authorization
|
||||
# on internal reports.
|
||||
# NOTE: It is not backed by a database table and should not be expected to
|
||||
# function like a traditional Rails model
|
||||
self.abstract_class = true
|
||||
|
||||
resourcify
|
||||
end
|
||||
73
app/views/admin/creator_settings/_form.html.erb
Normal file
73
app/views/admin/creator_settings/_form.html.erb
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<div class="crayons-field mt-6 align-left">
|
||||
<%= label_tag :community_name, class: "crayons-field__label" do %>
|
||||
Community name
|
||||
<span class="crayons-field__required crayons-tooltip" data-tooltip="This will set the primary name for your Forem" aria-describedby="community-name-subtitle"></span>
|
||||
<p id="community-name-subtitle" class="crayons-field__description">Used as the primary name for your Forem.</p>
|
||||
<% end %>
|
||||
<%= text_field_tag :community_name, "", placeholder: "Climbing Life", class: "crayons-textfield fs-italic", required: true %>
|
||||
</div>
|
||||
|
||||
<div class="crayons-field mt-6 align-left">
|
||||
<%= label_tag :logo_svg, class: "crayons-field__label" do %>
|
||||
Logo
|
||||
<span class="crayons-field__required crayons-tooltip" data-tooltip="This will set the logo for your Forem" aria-describedby="logo-subtitle"></span>
|
||||
<p id="logo-subtitle" class="crayons-field__description">Ideally SVG, but PNG will work, too. Min size: 300x300px.</p>
|
||||
<% end %>
|
||||
<span class="block border-none">
|
||||
<%= file_field_tag :logo_svg, class: "crayons-btn crayons-btn--secondary", role: "button", required: true %>
|
||||
</span>
|
||||
|
||||
<% if ::Settings::General.logo_svg.present? %>
|
||||
<div class="site-logo">
|
||||
<%= ::Settings::General.logo_svg.html_safe %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="crayons-field mt-6 align-left">
|
||||
<%= label_tag :primary_brand_color_hex, class: "crayons-field__label" do %>
|
||||
Brand color
|
||||
<span class="crayons-field__required crayons-tooltip" data-tooltip="This will set the accent color for buttons, links, etc. on your Forem" aria-describedby="color-selector-subtitle"></span>
|
||||
<p id="color-selector-subtitle" class="crayons-field__description">This will be the "accent" color used for buttons, links, etc.</p>
|
||||
<% end %>
|
||||
|
||||
<div class="flex items-center w-100 m:w-50 crayons-field">
|
||||
<div class="flex items-center">
|
||||
<%= text_field_tag :primary_brand_color_hex,
|
||||
::Settings::UserExperience.primary_brand_color_hex,
|
||||
pattern: "^#+([a-fA-F0-9]{6})$",
|
||||
placeholder: ::Settings::UserExperience.primary_brand_color_hex,
|
||||
class: "crayons-textfield js-color-field" %>
|
||||
<%= color_field_tag :primary_brand_color_hex,
|
||||
::Settings::UserExperience.primary_brand_color_hex,
|
||||
pattern: "^#+([a-fA-F0-9]{6})$",
|
||||
placeholder: ::Settings::UserExperience.primary_brand_color_hex,
|
||||
class: "crayons-color-selector js-color-field ml-2",
|
||||
required: true %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="crayons-field mt-6 align-left">
|
||||
<fieldset aria-describedby="section-description">
|
||||
<legend class="fs-l">Who can join this community?</legend>
|
||||
<div>
|
||||
<%= radio_button_tag :invite_only_mode, "0", class: "crayons-field crayons-field--radio", required: true %>
|
||||
<label for="invite_only_mode_0">Everyone</label>
|
||||
<br>
|
||||
<%= radio_button_tag :invite_only_mode, "1", class: "crayons-field crayons-field--radio", required: true %>
|
||||
<label for="invite_only_mode_1">Only people who are invited</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="crayons-field mt-6 align-left">
|
||||
<fieldset aria-describedby="section-description">
|
||||
<legend class="fs-l">Who can view content in this community?</legend>
|
||||
<div>
|
||||
<%= radio_button_tag :public, "0", class: "crayons-field crayons-field--radio", required: true %>
|
||||
<label for="public_0">Everyone</label>
|
||||
<br>
|
||||
<%= radio_button_tag :public, "1", class: "crayons-field crayons-field--radio", required: true %>
|
||||
<label for="public_1">Members only</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
48
app/views/admin/creator_settings/new.html.erb
Normal file
48
app/views/admin/creator_settings/new.html.erb
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<% if FeatureFlag.enabled?(:creator_onboarding) && User.with_role(:creator).any? %>
|
||||
<style>
|
||||
<%= Rails.application.assets["setup-mode.css"].to_s.html_safe %>
|
||||
#page-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
<main id="main-content" class="flex flex-1 justify-center flex-col crayons-layout crayons-layout--limited-xs">
|
||||
<div aria-live="assertive">
|
||||
<% if flash[:error] %>
|
||||
<div class="crayons-notice crayons-notice--danger mb-6" role="alert">
|
||||
<%= flash[:error] %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= form_tag(admin_creator_settings_path, method: "post") do %>
|
||||
<% if defined?(resource) && resource&.errors&.any? %>
|
||||
<div class="crayons-card crayons-card--secondary crayons-notice crayons-notice--danger" role="alert" data-testid="signup-errors">
|
||||
<div class="crayons-card__header">
|
||||
<h1 class="crayons-card__headline">
|
||||
Whoops, we found <%= pluralize(resource.errors.size, "problem") %>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="crayons-card__body">
|
||||
<ul>
|
||||
<% resource.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="align-center">
|
||||
<p class="pb-4 fs-3xl fw-bold">Lovely! Let's set up your Forem.</p>
|
||||
<p class="color-base-70 fs-xl">No stress, you can always change it later.</p>
|
||||
</div>
|
||||
<br>
|
||||
<%= render "form" %>
|
||||
<div class="crayons-field mt-6 align-left">
|
||||
<%= submit_tag "Finish", class: "crayons-btn btn--primary" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= inline_svg_tag("forem-background.svg", aria_hidden: true, class: "forem-background absolute bottom-0 right-0") %>
|
||||
|
|
@ -105,7 +105,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="crayons-card p-6">
|
||||
<div>
|
||||
<h2 class="d-inline">Destructive Actions</h2>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div class="small-pic">
|
||||
<%= optimized_image_tag(episode.image_url || episode.podcast.image_url,
|
||||
optimizer_options: { crop: "imagga_scale", width: 240, height: 240 },
|
||||
image_options: { alt: episode.title, loading: "lazy"}) %>
|
||||
image_options: { alt: episode.title, loading: "lazy" }) %>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h3><span class="tag-identifier"><%= t("views.podcasts.tag") %></span><%= episode.title %></h3>
|
||||
|
|
|
|||
|
|
@ -86,4 +86,4 @@ en:
|
|||
description: All videos on %{community}
|
||||
heading: "%{community} on Video"
|
||||
beta_html: "<strong>Video is beta:</strong> %{contact}"
|
||||
upload: "🎥 Upload Video File 🙌"
|
||||
upload: "🎥 Upload Video File 🙌"
|
||||
|
|
@ -86,4 +86,4 @@ fr:
|
|||
description: All videos on %{community}
|
||||
heading: "%{community} en Vidéo"
|
||||
beta_html: "<strong>Vidéo est en bêta:</strong> %{contact}"
|
||||
upload: "🎥 Télécharger un fichier vidéo 🙌"
|
||||
upload: "🎥 Télécharger un fichier vidéo 🙌"
|
||||
|
|
@ -13,7 +13,9 @@ namespace :admin do
|
|||
resources :invitations, only: %i[index new create destroy]
|
||||
resources :organization_memberships, only: %i[update destroy create]
|
||||
resources :permissions, only: %i[index]
|
||||
resources :reactions, only: [:update]
|
||||
resources :reactions, only: %i[update]
|
||||
resources :creator_settings, only: %i[create new]
|
||||
|
||||
namespace :settings do
|
||||
resources :authentications, only: [:create]
|
||||
resources :campaigns, only: [:create]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
describe('Creator Setup Page', () => {
|
||||
const { baseUrl } = Cypress.config();
|
||||
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.fixture('users/creatorUser.json').as('creator');
|
||||
cy.get('@creator').then((creator) => {
|
||||
cy.loginCreator(creator);
|
||||
});
|
||||
|
||||
cy.visit(`${baseUrl}admin/creator_settings/new?referrer=${baseUrl}`);
|
||||
});
|
||||
|
||||
it('should submit the creator settings form', () => {
|
||||
// should display a welcome message
|
||||
cy.findByText("Lovely! Let's set up your Forem.").should('be.visible');
|
||||
cy.findByText('No stress, you can always change it later.').should(
|
||||
'be.visible',
|
||||
);
|
||||
|
||||
// should contain a community name and update the field properly
|
||||
cy.findByRole('textbox', { name: /community name/i })
|
||||
.as('communityName')
|
||||
.invoke('attr', 'placeholder')
|
||||
.should('eq', 'Climbing Life');
|
||||
cy.get('@communityName').type('Climbing Life');
|
||||
|
||||
// should contain a logo upload field and upload a logo upon click
|
||||
cy.findByText(/^Logo/).should('be.visible');
|
||||
cy.findByRole('button', { name: /logo/i }).attachFile(
|
||||
'/images/admin-image.png',
|
||||
);
|
||||
|
||||
// should contain a brand color field
|
||||
cy.findByText(/^Brand color/).should('be.visible');
|
||||
cy.findByText(/^Brand color/).invoke('attr', 'value', '#ff0000');
|
||||
|
||||
// should contain a 'Who can join this community?' radio selector field and allow selection upon click
|
||||
cy.findByRole('group', { name: /^Who can join this community/i }).should(
|
||||
'be.visible',
|
||||
);
|
||||
cy.findAllByRole('radio', { name: /everyone/i }).check();
|
||||
cy.findAllByRole('radio').should('be.checked');
|
||||
|
||||
// should contain a 'Who can view content in this community?' radio selector field and allow selection upon click
|
||||
cy.findByRole('group', {
|
||||
name: /^Who can view content in this community/i,
|
||||
}).should('be.visible');
|
||||
cy.findAllByRole('radio', { name: /members only/i }).check();
|
||||
cy.findAllByRole('radio').should('be.checked');
|
||||
|
||||
// should redirect the creator to the home page when the form is completely filled out and 'Finish' is clicked
|
||||
cy.findByRole('button', { name: 'Finish' }).click();
|
||||
cy.url().should('equal', baseUrl);
|
||||
});
|
||||
|
||||
it('should not submit the creator settings form if any of the fields are not filled out', () => {
|
||||
// TODO: Circle back around to testing this once the styling for the form is complete
|
||||
cy.findByRole('textbox', { name: /community name/i }).should(
|
||||
'have.attr',
|
||||
'required',
|
||||
);
|
||||
cy.findByRole('button', { name: /logo/i }).should('have.attr', 'required');
|
||||
// should not redirect the creator to the home page when the form is not completely filled out and 'Finish' is clicked
|
||||
cy.findByRole('button', { name: 'Finish' }).click();
|
||||
cy.url().should(
|
||||
'equal',
|
||||
`${baseUrl}admin/creator_settings/new?referrer=${baseUrl}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -96,6 +96,9 @@ describe('Creator Signup Page', () => {
|
|||
.click();
|
||||
|
||||
const { baseUrl } = Cypress.config();
|
||||
cy.url().should('equal', `${baseUrl}onboarding?referrer=${baseUrl}`);
|
||||
cy.url().should(
|
||||
'equal',
|
||||
`${baseUrl}admin/creator_settings/new?referrer=${baseUrl}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ Cypress.Commands.add('loginCreator', ({ name, username, email, password }) => {
|
|||
return cy.request(
|
||||
'POST',
|
||||
'/users',
|
||||
`utf8=%E2%9C%93&user%5Bname%5D=${encodedName}&user%5Busername%5D=${encodedUsername}&user%5Bemail%5D=${encodedEmail}%40forem.local&user%5Bpassword%5D=${encodedPassword}&commit=Create+my+account`,
|
||||
`utf8=%E2%9C%93&user%5Bname%5D=${encodedName}&user%5Busername%5D=${encodedUsername}&user%5Bemail%5D=${encodedEmail}%40forem.local&user%5Bpassword%5D=${encodedPassword}&commit=Create+my+account&user%5Bforem_owner_secret%5D=secret`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
62
spec/requests/admin/creator_settings_spec.rb
Normal file
62
spec/requests/admin/creator_settings_spec.rb
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "/creator_settings/new", type: :request do
|
||||
let!(:creator) { create(:user, :creator) }
|
||||
let!(:non_admin_user) { create(:user) }
|
||||
let(:params) do
|
||||
{ community_name: "Climbing Life",
|
||||
logo_svg: "https://dummyimage.com/300x300.png",
|
||||
primary_brand_color_hex: "000000",
|
||||
public: true,
|
||||
invite_only: false }
|
||||
end
|
||||
|
||||
before do
|
||||
allow(FeatureFlag).to receive(:enabled?).with(:creator_onboarding).and_return(true)
|
||||
allow(Settings::General).to receive(:waiting_on_first_user).and_return(false)
|
||||
end
|
||||
|
||||
describe "GET /admin/creator_settings/new" do
|
||||
before do
|
||||
sign_in creator
|
||||
get new_admin_creator_setting_path
|
||||
end
|
||||
|
||||
context "when the user is a creator" do
|
||||
it "allows the request" do
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it "renders the correct page" do
|
||||
expect(response.body).to include("Lovely! Let's set up your Forem.")
|
||||
end
|
||||
end
|
||||
|
||||
context "when the user is a not a creator" do
|
||||
before do
|
||||
sign_in non_admin_user
|
||||
end
|
||||
|
||||
it "blocks the request" do
|
||||
expect do
|
||||
get new_admin_creator_setting_path
|
||||
end.to raise_error(Pundit::NotAuthorizedError)
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /admin/creator_settings/new" do
|
||||
before do
|
||||
sign_in creator
|
||||
get new_admin_creator_setting_path
|
||||
end
|
||||
|
||||
it "allows a creator to successfully fill out the creator setup form", :aggregate_failures do
|
||||
post admin_creator_settings_path, params: params
|
||||
expect(creator.saw_onboarding).to eq(true)
|
||||
expect(creator.checked_code_of_conduct).to eq(true)
|
||||
expect(creator.checked_terms_and_conditions).to eq(true)
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -26,7 +26,7 @@ RSpec.describe "AsyncInfo", type: :request do
|
|||
sign_in create(:user)
|
||||
|
||||
get "/async_info/base_data"
|
||||
expect(response.parsed_body.keys).to match_array(%w[broadcast param token user])
|
||||
expect(response.parsed_body.keys).to match_array(%w[broadcast creator creator_onboarding param token user])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue