DEV: Update member access wizard step to use toggle group (#28013)

We want to change the design of the "member experience" step of the wizard from using checkbox switches to using radio toggle groups.
This commit is contained in:
Ted Johansson 2024-07-29 14:07:06 +08:00 committed by GitHub
parent 2a9dcade0a
commit 3126c50baa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 221 additions and 49 deletions

View File

@ -2,6 +2,7 @@ import Checkbox from "./checkbox";
import Checkboxes from "./checkboxes"; import Checkboxes from "./checkboxes";
import Dropdown from "./dropdown"; import Dropdown from "./dropdown";
import Image from "./image"; import Image from "./image";
import Radio from "./radio";
import StylingPreview from "./styling-preview"; import StylingPreview from "./styling-preview";
import Text from "./text"; import Text from "./text";
@ -12,4 +13,5 @@ export default {
dropdown: Dropdown, dropdown: Dropdown,
image: Image, image: Image,
text: Text, text: Text,
radio: Radio,
}; };

View File

@ -0,0 +1,72 @@
import Component from "@glimmer/component";
import { hash } from "@ember/helper";
import { on } from "@ember/modifier";
import { action, set } from "@ember/object";
import PluginOutlet from "discourse/components/plugin-outlet";
import concatClass from "discourse/helpers/concat-class";
import withEventValue from "discourse/helpers/with-event-value";
import icon from "discourse-common/helpers/d-icon";
export default class Radio extends Component {
constructor() {
super(...arguments);
this._setSelected();
}
get field() {
return this.args.field;
}
@action
selectionChanged(input) {
this.field.value = input;
this._setSelected();
}
_setSelected() {
for (let choice of this.field.choices) {
set(choice, "selected", this.field.value === choice.id);
}
}
<template>
<div class="wizard-container__radio-choices">
{{#each @field.choices as |c|}}
<div
class={{concatClass
"wizard-container__radio-choice"
(if c.selected "--selected")
}}
>
<label class="wizard-container__label">
<PluginOutlet
@name="wizard-radio"
@outletArgs={{hash disabled=c.disabled}}
>
<input
type="radio"
value={{c.id}}
class="wizard-container__radio"
disabled={{c.disabled}}
checked={{c.selected}}
{{on "change" (withEventValue this.selectionChanged)}}
/>
<span class="wizard-container__radio-label">
{{#if c.icon}}
{{icon c.icon}}
{{/if}}
<span>{{c.label}}</span>
</span>
</PluginOutlet>
<PluginOutlet
@name="below-wizard-radio"
@outletArgs={{hash disabled=c.disabled}}
/>
</label>
</div>
{{/each}}
</div>
</template>
}

View File

@ -3,6 +3,7 @@ import { assert } from "@ember/debug";
import { hash } from "@ember/helper"; import { hash } from "@ember/helper";
import { dasherize } from "@ember/string"; import { dasherize } from "@ember/string";
import { htmlSafe } from "@ember/template"; import { htmlSafe } from "@ember/template";
import { or } from "truth-helpers";
import PluginOutlet from "discourse/components/plugin-outlet"; import PluginOutlet from "discourse/components/plugin-outlet";
import fields from "./fields"; import fields from "./fields";
@ -42,7 +43,7 @@ export default class WizardFieldComponent extends Component {
<template> <template>
<div class={{this.classes}}> <div class={{this.classes}}>
{{#if @field.label}} {{#if (or @field.label @field.description)}}
<label for={{@field.id}}> <label for={{@field.id}}>
<span class="wizard-container__label"> <span class="wizard-container__label">
{{@field.label}} {{@field.label}}

View File

@ -504,7 +504,8 @@ body.wizard {
&__description { &__description {
color: var(--primary-high); color: var(--primary-high);
font-size: var(--font-down-1); font-size: var(--font-0);
font-weight: normal;
margin: 0.25em 0 0.5em 0; margin: 0.25em 0 0.5em 0;
a { a {
@ -630,6 +631,58 @@ body.wizard {
top: 2px; top: 2px;
} }
&__radio {
position: absolute;
visibility: hidden;
}
&__radio-choices {
align-items: stretch;
display: flex;
gap: 1em;
@include breakpoint(mobile-extra-large) {
flex-direction: column;
}
}
&__radio-choice {
flex-basis: 0;
flex-grow: 1;
display: flex;
&.--selected {
.wizard-container__label {
background-color: var(--tertiary-very-low);
border-color: var(--tertiary-high);
border-width: 2px;
margin: 0;
}
}
.wizard-container__label {
border-radius: 4px;
border: 1px solid var(--primary-low-mid);
flex-grow: 1;
margin: 1px 0;
}
label {
align-content: center;
cursor: pointer;
display: flex;
flex-direction: column;
flex-wrap: wrap;
font-weight: normal;
padding: 1em;
text-align: center;
.svg-icon {
margin-bottom: 0.5em;
width: 100%;
}
}
}
label .svg-icon { label .svg-icon {
top: 2px; top: 2px;
} }

View File

@ -5345,18 +5345,32 @@ en:
label: "Language" label: "Language"
privacy: privacy:
title: "Member experience" title: "Member access"
fields: fields:
login_required: login_required:
placeholder: "Private" label: "Visibility"
extra_description: "Only logged in users can access this community" description: "Is your community public or private?"
choices:
public:
label: "Public"
private:
label: "Private"
invite_only: invite_only:
placeholder: "Invite only" label: "Registration"
extra_description: "Users must be invited by trusted users or staff, otherwise users can sign up on their own" description: "How can members join this community?"
choices:
sign_up:
label: "Sign up"
invite_only:
label: "Invite only"
must_approve_users: must_approve_users:
placeholder: "Require approval" description: "Do you want to approve member accounts?"
extra_description: "Users must be approved by staff" choices:
"no":
label: "No, new members can join immediately"
"yes":
label: "Yes, new members must be approved by moderators"
chat_enabled: chat_enabled:
placeholder: "Enable chat" placeholder: "Enable chat"
extra_description: "Engage with your members in real time" extra_description: "Engage with your members in real time"

View File

@ -59,41 +59,38 @@ class Wizard
@wizard.append_step("privacy") do |step| @wizard.append_step("privacy") do |step|
step.emoji = "hugs" step.emoji = "hugs"
step.add_field( step.add_field(
id: "login_required", id: "login_required",
type: "checkbox", type: "radio",
icon: "unlock", value: SiteSetting.login_required ? "private" : "public",
value: SiteSetting.login_required, ) do |field|
) field.add_choice("public")
field.add_choice("private")
end
step.add_field( step.add_field(
id: "invite_only", id: "invite_only",
type: "checkbox", type: "radio",
icon: "user-plus", value: SiteSetting.invite_only ? "invite_only" : "sign_up",
value: SiteSetting.invite_only, ) do |field|
) field.add_choice("sign_up", icon: "user-plus")
field.add_choice("invite_only", icon: "paper-plane")
end
step.add_field( step.add_field(
id: "must_approve_users", id: "must_approve_users",
type: "checkbox", type: "radio",
icon: "user-shield", value: SiteSetting.must_approve_users ? "yes" : "no",
value: SiteSetting.must_approve_users, ) do |field|
) field.add_choice("no")
field.add_choice("yes")
if defined?(::Chat)
step.add_field(
id: "chat_enabled",
type: "checkbox",
icon: "d-chat",
value: SiteSetting.chat_enabled,
)
end end
step.on_update do |updater| step.on_update do |updater|
updater.update_setting(:login_required, updater.fields[:login_required]) updater.update_setting(:login_required, updater.fields[:login_required] == "private")
updater.update_setting(:invite_only, updater.fields[:invite_only]) updater.update_setting(:invite_only, updater.fields[:invite_only] == "invite_only")
updater.update_setting(:must_approve_users, updater.fields[:must_approve_users]) updater.update_setting(:must_approve_users, updater.fields[:must_approve_users] == "yes")
updater.update_setting(:chat_enabled, updater.fields[:chat_enabled]) if defined?(::Chat)
end end
end end

View File

@ -14,6 +14,7 @@ class Wizard
field = Field.new(attrs) field = Field.new(attrs)
field.step = self field.step = self
@fields << field @fields << field
yield field if block_given?
field field
end end

View File

@ -51,9 +51,9 @@ RSpec.describe Wizard::StepUpdater do
updater = updater =
wizard.create_updater( wizard.create_updater(
"privacy", "privacy",
login_required: false, login_required: "public",
invite_only: false, invite_only: "sign_up",
must_approve_users: false, must_approve_users: "no",
) )
updater.update updater.update
expect(updater.success?).to eq(true) expect(updater.success?).to eq(true)
@ -67,9 +67,9 @@ RSpec.describe Wizard::StepUpdater do
updater = updater =
wizard.create_updater( wizard.create_updater(
"privacy", "privacy",
login_required: true, login_required: "private",
invite_only: true, invite_only: "invite_only",
must_approve_users: true, must_approve_users: "yes",
) )
updater.update updater.update
expect(updater.success?).to eq(true) expect(updater.success?).to eq(true)

View File

@ -82,15 +82,11 @@ RSpec.describe Wizard::Builder do
count = defined?(::Chat) ? 4 : 3 count = defined?(::Chat) ? 4 : 3
expect(fields.length).to eq(count) expect(fields.length).to eq(count)
expect(login_required_field.id).to eq("login_required") expect(login_required_field.id).to eq("login_required")
expect(login_required_field.value).to eq(true) expect(login_required_field.value).to eq("private")
expect(invite_only_field.id).to eq("invite_only") expect(invite_only_field.id).to eq("invite_only")
expect(invite_only_field.value).to eq(false) expect(invite_only_field.value).to eq("sign_up")
expect(must_approve_users_field.id).to eq("must_approve_users") expect(must_approve_users_field.id).to eq("must_approve_users")
expect(must_approve_users_field.value).to eq(true) expect(must_approve_users_field.value).to eq("yes")
if defined?(::Chat)
expect(chat_enabled_field.id).to eq("chat_enabled")
expect(chat_enabled_field.value).to eq(true)
end
end end
end end

View File

@ -61,13 +61,13 @@ RSpec.describe WizardSerializer do
expect(privacy_step).to_not be_nil expect(privacy_step).to_not be_nil
login_required_field = privacy_step["fields"].find { |f| f["id"] == "login_required" } login_required_field = privacy_step["fields"].find { |f| f["id"] == "login_required" }
expect(login_required_field["value"]).to eq(true) expect(login_required_field["value"]).to eq("private")
invite_only_field = privacy_step["fields"].find { |f| f["id"] == "invite_only" } invite_only_field = privacy_step["fields"].find { |f| f["id"] == "invite_only" }
expect(invite_only_field["value"]).to eq(true) expect(invite_only_field["value"]).to eq("invite_only")
must_approve_users_field = privacy_step["fields"].find { |f| f["id"] == "must_approve_users" } must_approve_users_field = privacy_step["fields"].find { |f| f["id"] == "must_approve_users" }
expect(must_approve_users_field["value"]).to eq(true) expect(must_approve_users_field["value"]).to eq("yes")
end end
end end
end end

View File

@ -6,6 +6,14 @@ module PageObjects
def click_jump_in def click_jump_in
find(".jump-in").click find(".jump-in").click
end end
def go_to_next_step
find(".wizard-container__button.next").click
end
def select_access_option(label)
find(".wizard-container__radio-choice", text: label).click
end
end end
end end
end end

View File

@ -9,6 +9,34 @@ describe "Wizard", type: :system do
before { sign_in(admin) } before { sign_in(admin) }
it "lets user configure member access" do
visit("/wizard/steps/privacy")
expect(page).to have_css(
".wizard-container__radio-choice.--selected",
text: I18n.t("wizard.step.privacy.fields.login_required.choices.public.label"),
)
wizard_page.select_access_option("Private")
expect(page).to have_css(
".wizard-container__radio-choice.--selected",
text: I18n.t("wizard.step.privacy.fields.login_required.choices.private.label"),
)
wizard_page.go_to_next_step
expect(page).to have_current_path("/wizard/steps/ready")
expect(SiteSetting.login_required).to eq(true)
visit("/wizard/steps/privacy")
expect(page).to have_css(
".wizard-container__radio-choice.--selected",
text: I18n.t("wizard.step.privacy.fields.login_required.choices.private.label"),
)
end
it "redirects to latest when wizard is completed" do it "redirects to latest when wizard is completed" do
visit("/wizard/steps/ready") visit("/wizard/steps/ready")
wizard_page.click_jump_in wizard_page.click_jump_in