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 Dropdown from "./dropdown";
import Image from "./image";
import Radio from "./radio";
import StylingPreview from "./styling-preview";
import Text from "./text";
@ -12,4 +13,5 @@ export default {
dropdown: Dropdown,
image: Image,
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 { dasherize } from "@ember/string";
import { htmlSafe } from "@ember/template";
import { or } from "truth-helpers";
import PluginOutlet from "discourse/components/plugin-outlet";
import fields from "./fields";
@ -42,7 +43,7 @@ export default class WizardFieldComponent extends Component {
<template>
<div class={{this.classes}}>
{{#if @field.label}}
{{#if (or @field.label @field.description)}}
<label for={{@field.id}}>
<span class="wizard-container__label">
{{@field.label}}

View File

@ -504,7 +504,8 @@ body.wizard {
&__description {
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;
a {
@ -630,6 +631,58 @@ body.wizard {
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 {
top: 2px;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -82,15 +82,11 @@ RSpec.describe Wizard::Builder do
count = defined?(::Chat) ? 4 : 3
expect(fields.length).to eq(count)
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.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.value).to eq(true)
if defined?(::Chat)
expect(chat_enabled_field.id).to eq("chat_enabled")
expect(chat_enabled_field.value).to eq(true)
end
expect(must_approve_users_field.value).to eq("yes")
end
end

View File

@ -61,13 +61,13 @@ RSpec.describe WizardSerializer do
expect(privacy_step).to_not be_nil
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" }
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" }
expect(must_approve_users_field["value"]).to eq(true)
expect(must_approve_users_field["value"]).to eq("yes")
end
end
end

View File

@ -6,6 +6,14 @@ module PageObjects
def click_jump_in
find(".jump-in").click
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

View File

@ -9,6 +9,34 @@ describe "Wizard", type: :system do
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
visit("/wizard/steps/ready")
wizard_page.click_jump_in