mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 11:44:49 +08:00
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:
parent
2a9dcade0a
commit
3126c50baa
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
}
|
|
@ -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}}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user