Onboarding: custom newsletter opt-in settings (#20114)
* Settings can process markdown into html * Update Settings with new onboarding settings * Async render onboarding newsletter step * Tweak onboarding design * Fix broken spec * Better rendered component test * Tweaks to match design * Try to tweak design * Try having a default state * Tweak placeholder content * Better await componentDidMount * Continue to tweak the design * ContentRenderer#process should always return Result * Try more clarity in the partial * Rubocop
This commit is contained in:
parent
7110c69bb5
commit
6a48b486ca
15 changed files with 260 additions and 60 deletions
|
|
@ -73,3 +73,32 @@
|
|||
right: var(--su-6);
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-main {
|
||||
.email-preferences-wrapper {
|
||||
h1 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: var(--lh-tight);
|
||||
}
|
||||
p {
|
||||
line-height: var(--lh-base);
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding_newsletter_opt_in_head {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.onboarding_newsletter_opt_in_subhead {
|
||||
color: var(--base-60);
|
||||
}
|
||||
|
||||
.email_newsletter {
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 0 6px 6px 0;
|
||||
border-left-width: thick;
|
||||
border-left-color: var(--accent-brand);
|
||||
background-color: var(--card-secondary-bg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,17 @@ class OnboardingsController < ApplicationController
|
|||
notifications_updated_response(success, current_user.notification_setting.errors_as_sentence)
|
||||
end
|
||||
|
||||
def newsletter
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
rendered_content = render_to_string(partial: "onboardings/newsletter",
|
||||
formats: [:html],
|
||||
layout: false)
|
||||
render json: { content: rendered_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unset_username?
|
||||
|
|
|
|||
|
|
@ -8,15 +8,19 @@ export class EmailPreferencesForm extends Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
|
||||
this.state = {
|
||||
email_newsletter: false,
|
||||
content: '<p>Loading...</p>'
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetch('/onboarding/newsletter')
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
this.setState({ content: json['content'] });
|
||||
});
|
||||
|
||||
updateOnboarding('v2: email preferences form');
|
||||
}
|
||||
|
||||
|
|
@ -40,15 +44,7 @@ export class EmailPreferencesForm extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
const { name } = event.target;
|
||||
this.setState((currentState) => ({
|
||||
[name]: !currentState[name],
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { email_newsletter } = this.state;
|
||||
const { prev, slidesCount, currentSlideIndex } = this.props;
|
||||
return (
|
||||
<div
|
||||
|
|
@ -61,36 +57,12 @@ export class EmailPreferencesForm extends Component {
|
|||
aria-labelledby="title"
|
||||
aria-describedby="subtitle"
|
||||
>
|
||||
<div className="onboarding-content terms-and-conditions-wrapper">
|
||||
<header className="onboarding-content-header">
|
||||
<h1 id="title" className="title">
|
||||
Almost there!
|
||||
</h1>
|
||||
<h2 id="subtitle" className="subtitle">
|
||||
Review your email preferences before we continue.
|
||||
</h2>
|
||||
</header>
|
||||
<div
|
||||
className="onboarding-content email-preferences-wrapper"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: this.state.content }}
|
||||
/>
|
||||
|
||||
<form>
|
||||
<fieldset>
|
||||
<legend>Email preferences</legend>
|
||||
<ul>
|
||||
<li className="checkbox-item">
|
||||
<label htmlFor="email_newsletter">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="email_newsletter"
|
||||
name="email_newsletter"
|
||||
checked={email_newsletter}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
I want to receive weekly newsletter emails.
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<Navigation
|
||||
prev={prev}
|
||||
next={this.onSubmit}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,26 @@ describe('EmailPreferencesForm', () => {
|
|||
username: 'username',
|
||||
});
|
||||
|
||||
const fakeResponse = JSON.stringify({
|
||||
content: `
|
||||
<h1>Almost there!</h1>
|
||||
<form>
|
||||
<fieldset>
|
||||
<ul>
|
||||
<li class="checkbox-item">
|
||||
<label for="email_newsletter"><input type="checkbox" id="email_newsletter" name="email_newsletter">I want to receive weekly newsletter emails.</label>
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fetch.resetMocks();
|
||||
fetch.mockResponseOnce(fakeResponse);
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
document.head.innerHTML =
|
||||
'<meta name="csrf-token" content="some-csrf-token" />';
|
||||
|
|
@ -46,20 +66,16 @@ describe('EmailPreferencesForm', () => {
|
|||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should load the appropriate text', () => {
|
||||
const { queryByText } = renderEmailPreferencesForm();
|
||||
|
||||
expect(queryByText(/almost there!/i)).toExist();
|
||||
expect(
|
||||
queryByText(/review your email preferences before we continue./i),
|
||||
).toExist();
|
||||
expect(queryByText('Email preferences')).toExist();
|
||||
it('should load the appropriate text', async () => {
|
||||
const { findByLabelText } = renderEmailPreferencesForm();
|
||||
await findByLabelText(/receive weekly newsletter/i);
|
||||
expect(document.body.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should show the checkbox unchecked', () => {
|
||||
const { queryByLabelText } = renderEmailPreferencesForm();
|
||||
|
||||
expect(queryByLabelText(/receive weekly newsletter/i).checked).toBe(false);
|
||||
it('should show the checkbox unchecked', async () => {
|
||||
const { findByLabelText } = renderEmailPreferencesForm();
|
||||
const checkbox = await findByLabelText(/receive weekly newsletter/i);
|
||||
expect(checkbox.checked).toBe(false);
|
||||
});
|
||||
|
||||
it('should render a stepper', () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EmailPreferencesForm should load the appropriate text 1`] = `
|
||||
"<div><div data-testid=\\"onboarding-email-preferences-form\\" class=\\"onboarding-main crayons-modal crayons-modal--large\\"><div class=\\"crayons-modal__box\\" role=\\"dialog\\" aria-labelledby=\\"title\\" aria-describedby=\\"subtitle\\"><div class=\\"onboarding-content email-preferences-wrapper\\">
|
||||
<h1>Almost there!</h1>
|
||||
<form>
|
||||
<fieldset>
|
||||
<ul>
|
||||
<li class=\\"checkbox-item\\">
|
||||
<label for=\\"email_newsletter\\"><input type=\\"checkbox\\" id=\\"email_newsletter\\" name=\\"email_newsletter\\">I want to receive weekly newsletter emails.</label>
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div><nav class=\\"onboarding-navigation\\"><div class=\\"navigation-content\\"><div class=\\"back-button-container \\"><button data-testid=\\"back-button\\" class=\\"back-button\\" type=\\"button\\" aria-label=\\"Back to previous onboarding step\\"><svg width=\\"24\\" height=\\"24\\" fill=\\"none\\" class=\\"crayons-icon\\" xmlns=\\"http://www.w3.org/2000/svg\\"><path d=\\"M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414L7.828 11z\\"></path></svg></button></div><div data-testid=\\"stepper\\" class=\\"stepper\\"><span class=\\"dot active\\"></span><span class=\\"dot active\\"></span><span class=\\"dot active\\"></span><span class=\\"dot active\\"></span><span class=\\"dot active\\"></span></div><button class=\\"next-button\\" type=\\"button\\">Finish</button></div></nav></div></div></div>"
|
||||
`;
|
||||
|
|
@ -89,6 +89,18 @@ module Constants
|
|||
description: "",
|
||||
placeholder: I18n.t("lib.constants.settings.general.meta_keywords.description")
|
||||
},
|
||||
onboarding_newsletter_content: {
|
||||
description: I18n.t("lib.constants.settings.general.onboarding_newsletter_content.description"),
|
||||
placeholder: I18n.t("lib.constants.settings.general.onboarding_newsletter_content.placeholder")
|
||||
},
|
||||
onboarding_newsletter_opt_in_head: {
|
||||
description: I18n.t("lib.constants.settings.general.onboarding_newsletter_opt_in_head.description"),
|
||||
placeholder: I18n.t("lib.constants.settings.general.onboarding_newsletter_opt_in_head.placeholder")
|
||||
},
|
||||
onboarding_newsletter_opt_in_subhead: {
|
||||
description: I18n.t("lib.constants.settings.general.onboarding_newsletter_opt_in_subhead.description"),
|
||||
placeholder: I18n.t("lib.constants.settings.general.onboarding_newsletter_opt_in_subhead.placeholder")
|
||||
},
|
||||
payment_pointer: {
|
||||
description: I18n.t("lib.constants.settings.general.payment.description"),
|
||||
placeholder: "$pay.somethinglikethis.co/value"
|
||||
|
|
|
|||
|
|
@ -73,7 +73,9 @@ module Settings
|
|||
if result.nil? # we don't want to accidentally do this for "false"
|
||||
result ||= default.is_a?(Proc) ? default.call : default
|
||||
end
|
||||
result = __send__(:convert_string_to_value_type, type, result, separator: separator)
|
||||
|
||||
read_as_type = type == :markdown ? :string : type
|
||||
result = __send__(:convert_string_to_value_type, read_as_type, result, separator: separator)
|
||||
|
||||
result
|
||||
end
|
||||
|
|
@ -83,10 +85,17 @@ module Settings
|
|||
var_name = key
|
||||
|
||||
record = find_by(var: var_name) || new(var: var_name)
|
||||
value = __send__(:convert_string_to_value_type, type, value, separator: separator)
|
||||
|
||||
record.value = value
|
||||
record.save!
|
||||
if type == :markdown
|
||||
processed = __send__(:convert_string_to_value_type, type, value)
|
||||
record.value = value
|
||||
record.save!
|
||||
__send__(:"#{key}_processed_html=", processed)
|
||||
else
|
||||
value = __send__(:convert_string_to_value_type, type, value, separator: separator)
|
||||
record.value = value
|
||||
record.save!
|
||||
end
|
||||
|
||||
value
|
||||
end
|
||||
|
|
@ -127,6 +136,8 @@ module Settings
|
|||
value.to_f
|
||||
when :big_decimal
|
||||
value.to_d
|
||||
when :markdown
|
||||
ContentRenderer.new(value).process.processed_html
|
||||
else
|
||||
value
|
||||
end
|
||||
|
|
|
|||
|
|
@ -123,9 +123,21 @@ module Settings
|
|||
existing_published_article_id: true, allow_nil: true
|
||||
}
|
||||
|
||||
# Onboarding newsletter
|
||||
setting :onboarding_newsletter_content, type: :markdown
|
||||
setting :onboarding_newsletter_content_processed_html
|
||||
setting :onboarding_newsletter_opt_in_head
|
||||
setting :onboarding_newsletter_opt_in_subhead
|
||||
|
||||
setting :default_content_language, type: :string, default: "en",
|
||||
validates: { inclusion: Languages::Detection.codes }
|
||||
|
||||
def self.custom_newsletter_configured?
|
||||
onboarding_newsletter_content_processed_html.present? &&
|
||||
onboarding_newsletter_opt_in_head.present? &&
|
||||
onboarding_newsletter_opt_in_subhead.present?
|
||||
end
|
||||
|
||||
def self.social_media_services
|
||||
SOCIAL_MEDIA_SERVICES.index_with do |name|
|
||||
social_media_handles[name]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# renders markdown for Articles, Billboards, Comments
|
||||
# renders markdown for Articles, Billboards, Comments, Onboarding newsletter content
|
||||
class ContentRenderer
|
||||
Result = Struct.new(:front_matter, :reading_time, :processed_html, keyword_init: true)
|
||||
|
||||
|
|
@ -9,10 +9,10 @@ class ContentRenderer
|
|||
end
|
||||
|
||||
# @param input [String] body_markdown to process
|
||||
# @param source [Article, Comment, Billboard]
|
||||
# @param source [optional, possibly Article, Comment, Billboard]
|
||||
# @param user [User, NilClass] article's or comment's user, nil for Billboard
|
||||
# @param fixer [Object] fixes the input markdown
|
||||
def initialize(input, source:, user: nil, fixer: MarkdownProcessor::Fixer::FixAll)
|
||||
def initialize(input, source: nil, user: nil, fixer: MarkdownProcessor::Fixer::FixAll)
|
||||
@input = input || ""
|
||||
@source = source
|
||||
@user = user
|
||||
|
|
|
|||
|
|
@ -14,6 +14,30 @@
|
|||
value: Settings::General.suggested_tags.join(","),
|
||||
placeholder: Constants::Settings::General.details[:suggested_tags][:placeholder] %>
|
||||
</div>
|
||||
|
||||
<div class="crayons-field">
|
||||
<%= admin_config_label :newsletter_step_heading %>
|
||||
<%= admin_config_description Constants::Settings::General.details[:onboarding_newsletter_content][:description] %>
|
||||
<%= f.text_area :onboarding_newsletter_content,
|
||||
class: "crayons-textfield",
|
||||
value: Settings::General.onboarding_newsletter_content,
|
||||
placeholder: Constants::Settings::General.details[:onboarding_newsletter_content][:placeholder] %>
|
||||
</div>
|
||||
|
||||
<div class="crayons-field">
|
||||
<%= admin_config_label Constants::Settings::General.details[:onboarding_newsletter_opt_in_head][:description] %>
|
||||
<%= f.text_field :onboarding_newsletter_opt_in_head,
|
||||
class: "crayons-textfield",
|
||||
value: Settings::General.onboarding_newsletter_opt_in_head,
|
||||
placeholder: Constants::Settings::General.details[:onboarding_newsletter_opt_in_head][:placeholder] %>
|
||||
|
||||
<%= admin_config_label Constants::Settings::General.details[:onboarding_newsletter_opt_in_subhead][:description] %>
|
||||
<%= f.text_field :onboarding_newsletter_opt_in_subhead,
|
||||
class: "crayons-textfield",
|
||||
value: Settings::General.onboarding_newsletter_opt_in_subhead,
|
||||
placeholder: Constants::Settings::General.details[:onboarding_newsletter_opt_in_subhead][:placeholder] %>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
<%= render "update_setting_button", f: f %>
|
||||
</div>
|
||||
|
|
|
|||
63
app/views/onboardings/_newsletter.html.erb
Normal file
63
app/views/onboardings/_newsletter.html.erb
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<% if feature_flag_enabled?(:onboarding_newsletter_content) %>
|
||||
<% if Settings::General.custom_newsletter_configured? %>
|
||||
<%= Settings::General.onboarding_newsletter_content_processed_html.html_safe %>
|
||||
|
||||
<form>
|
||||
<fieldset>
|
||||
<div class="flex email_newsletter mt-5 pt-4 ">
|
||||
<div class="flex h-5 ml-3">
|
||||
<input type="checkbox" id="email_newsletter" name="email_newsletter" class="w-4 h-4 mt-1 crayons-checkbox">
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<label for="email_newsletter" class="font-medium text-gray-900 dark:text-gray-300">
|
||||
<p class="onboarding_newsletter_opt_in_head" style="font-weight: 600"><%= Settings::General.onboarding_newsletter_opt_in_head %></p>
|
||||
<p class="onboarding_newsletter_opt_in_subhead mt-1 pb-4"><%= Settings::General.onboarding_newsletter_opt_in_subhead %></p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
<% else %>
|
||||
<%# feature flag is "on" but feature is not configured or only partly configured %>
|
||||
<h1>Almost there!</h1>
|
||||
<p>Review your email preferences before we continue.</p>
|
||||
|
||||
<form>
|
||||
<fieldset>
|
||||
<div class="flex email_newsletter mt-5 pt-3 ">
|
||||
<div class="flex h-5 ml-3">
|
||||
<input type="checkbox" id="email_newsletter" name="email_newsletter" class="w-4 h-4 mt-2 crayons-checkbox">
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
<label for="email_newsletter" class="font-medium text-gray-900 dark:text-gray-300">
|
||||
<p class="mt-1 pb-4">I want to receive weekly newsletter emails</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%# feature flag is "off", original behavior %>
|
||||
<header class="onboarding-content-header">
|
||||
<h1 id="title" class="title">
|
||||
Almost there!
|
||||
</h1>
|
||||
<h2 id="subtitle" class="subtitle">
|
||||
Review your email preferences before we continue.
|
||||
</h2>
|
||||
</header>
|
||||
<form>
|
||||
<fieldset>
|
||||
<legend>Email preferences</legend>
|
||||
<ul>
|
||||
<li class="checkbox-item">
|
||||
<label for="email_newsletter"><input type="checkbox" id="email_newsletter" name="email_newsletter">I want to receive weekly newsletter emails.</label>
|
||||
</li>
|
||||
<li class="checkbox-item">
|
||||
<label for="email_digest_periodic"><input type="checkbox" id="email_digest_periodic" name="email_digest_periodic">I want to receive a periodic digest of top posts from my tags.</label>
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
</form>
|
||||
<% end %>
|
||||
|
|
@ -144,6 +144,15 @@ en:
|
|||
description: The Mascot account
|
||||
meta_keywords:
|
||||
description: 'List of valid keywords: comma separated, letters only e.g. engineering, development'
|
||||
onboarding_newsletter_content:
|
||||
description: "Use this field to set the title and subtitle for your newsletter offering step. Keep it concise."
|
||||
placeholder: "You can configure image, title and subtitle via markdown"
|
||||
onboarding_newsletter_opt_in_head:
|
||||
description: "Newsletter checkbox heading"
|
||||
placeholder: "E.g., Yes, subscribe me to the weekly newsletter"
|
||||
onboarding_newsletter_opt_in_subhead:
|
||||
description: "Newsletter checkbox subheading"
|
||||
placeholder: "E.g., Rest assured, we respect your privacy. Your email will only be used for our newsletter and won't be shared with others."
|
||||
payment:
|
||||
description: 'Used for site-wide web monetization. See: https://github.com/forem/forem/pull/6345'
|
||||
periodic:
|
||||
|
|
|
|||
|
|
@ -144,6 +144,15 @@ fr:
|
|||
description: The Mascot account
|
||||
meta_keywords:
|
||||
description: 'List of valid keywords: comma separated, letters only e.g. engineering, development'
|
||||
onboarding_newsletter_content:
|
||||
description: "Use this field to set the title and subtitle for your newsletter offering step. Keep it concise."
|
||||
placeholder: "You can configure image, title and subtitle via markdown"
|
||||
onboarding_newsletter_opt_in_head:
|
||||
description: "Newsletter checkbox heading"
|
||||
placeholder: "E.g., Yes, subscribe me to the weekly newsletter"
|
||||
onboarding_newsletter_opt_in_subhead:
|
||||
description: "Newsletter checkbox subheading"
|
||||
placeholder: "E.g., Rest assured, we respect your privacy. Your email will only be used for our newsletter and won't be shared with others."
|
||||
payment:
|
||||
description: 'Used for site-wide web monetization. See: https://github.com/forem/forem/pull/6345'
|
||||
periodic:
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ Rails.application.routes.draw do
|
|||
patch :notifications, defaults: { format: :json }
|
||||
get :tags, defaults: { format: :json }
|
||||
get :users_and_organizations, defaults: { format: :json }
|
||||
get :newsletter, defaults: { format: :json }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ RSpec.describe Settings::Base do
|
|||
setting :float_item, type: :float, default: 7
|
||||
setting :big_decimal_item, type: :big_decimal, default: 9
|
||||
setting :default_value_with_block, type: :integer, default: -> { 1 + 1 }
|
||||
setting :some_markdown, type: :markdown
|
||||
setting :some_markdown_processed_html
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -49,7 +51,7 @@ RSpec.describe Settings::Base do
|
|||
|
||||
describe ".keys" do
|
||||
it "returns all the defined settings", :aggregate_failures do
|
||||
expect(TestSetting.keys.size).to eq 10
|
||||
expect(TestSetting.keys.size).to eq 12
|
||||
expect(TestSetting.keys).to include("host")
|
||||
expect(TestSetting.keys).to include("default_value_with_block")
|
||||
end
|
||||
|
|
@ -132,6 +134,19 @@ RSpec.describe Settings::Base do
|
|||
TestSetting.big_decimal_item = 5
|
||||
expect(TestSetting.big_decimal_item).to be_an_instance_of(BigDecimal)
|
||||
end
|
||||
|
||||
it "can be coerced from markdown to parsed_html" do
|
||||
some_markdown = <<~HEREDOC
|
||||
Hi, Hello!
|
||||
|
||||
This is **markdown**.
|
||||
HEREDOC
|
||||
TestSetting.some_markdown = some_markdown
|
||||
|
||||
processed_html = "<p>Hi, Hello!</p>\n\n<p>This is <strong>markdown</strong>.</p>\n\n"
|
||||
expect(TestSetting.some_markdown_processed_html).to eq(processed_html)
|
||||
expect(TestSetting.some_markdown).to eq(some_markdown)
|
||||
end
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue