UX: Easily toggle badges in admin badge list (#20225)

This commit is contained in:
Keegan George 2023-02-09 11:36:27 -08:00 committed by GitHub
parent 58123e8089
commit 6338287e89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 222 additions and 10 deletions

View File

@ -1,7 +1,6 @@
import Controller, { inject as controller } from "@ember/controller"; import Controller, { inject as controller } from "@ember/controller";
import { observes } from "discourse-common/utils/decorators"; import { observes } from "discourse-common/utils/decorators";
import I18n from "I18n"; import I18n from "I18n";
import { bufferedProperty } from "discourse/mixins/buffered-content"; import { bufferedProperty } from "discourse/mixins/buffered-content";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import { next } from "@ember/runloop"; import { next } from "@ember/runloop";
@ -25,6 +24,14 @@ export default class AdminBadgesShowController extends Controller.extend(
@tracked savingStatus = ""; @tracked savingStatus = "";
@tracked selectedGraphicType = null; @tracked selectedGraphicType = null;
get badgeEnabledLabel() {
if (this.buffered.get("enabled")) {
return "admin.badges.enabled";
} else {
return "admin.badges.disabled";
}
}
get badgeTypes() { get badgeTypes() {
return this.adminBadges.badgeTypes; return this.adminBadges.badgeTypes;
} }
@ -238,4 +245,11 @@ export default class AdminBadgesShowController extends Controller.extend(
}, },
}); });
} }
@action
toggleBadge() {
this.model
.save({ enabled: !this.buffered.get("enabled") })
.catch(popupAjaxError);
}
} }

View File

@ -1,4 +1,12 @@
<DSection @class="current-badge content-body"> <DSection @class="current-badge content-body">
<div class="control-group current-badge__toggle-badge">
<DToggleSwitch
@state={{this.buffered.enabled}}
@label={{this.badgeEnabledLabel}}
{{on "click" this.toggleBadge}}
/>
</div>
<form class="form-horizontal"> <form class="form-horizontal">
<div class="control-group"> <div class="control-group">
<label for="name">{{i18n "admin.badges.name"}}</label> <label for="name">{{i18n "admin.badges.name"}}</label>
@ -253,13 +261,6 @@
{{i18n "admin.badges.show_posts"}} {{i18n "admin.badges.show_posts"}}
</label> </label>
</div> </div>
<div>
<label>
<Input @type="checkbox" @checked={{this.buffered.enabled}} />
{{i18n "admin.badges.enabled"}}
</label>
</div>
</div> </div>
<div class="buttons"> <div class="buttons">

View File

@ -0,0 +1,20 @@
<div class="d-toggle-switch">
<label class="d-toggle-switch--label">
{{! template-lint-disable no-unnecessary-concat }}
<button
class="d-toggle-switch__checkbox"
type="button"
role="switch"
aria-checked="{{@state}}"
...attributes
></button>
<span class="d-toggle-switch__checkbox-slider">
{{#if @state}}
{{d-icon "check"}}
{{/if}}
</span>
</label>
<span class="d-toggle-switch__checkbox-label">
{{this.computedLabel}}
</span>
</div>

View File

@ -0,0 +1,15 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import I18n from "I18n";
export default class DiscourseToggleSwitch extends Component {
@tracked iconEnabled = true;
@tracked showIcon = this.iconEnabled && this.icon;
get computedLabel() {
if (this.args.label) {
return I18n.t(this.args.label);
}
return this.args.translatedLabel;
}
}

View File

@ -0,0 +1,66 @@
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { hbs } from "ember-cli-htmlbars";
import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import I18n from "I18n";
module("Integration | Component | d-toggle-switch", function (hooks) {
setupRenderingTest(hooks);
test("it renders a toggle button in a disabled state", async function (assert) {
this.set("state", false);
await render(hbs`<DToggleSwitch @state={{this.state}}/>`);
assert.ok(exists(".d-toggle-switch"), "it renders a toggle switch");
assert.strictEqual(
query(".d-toggle-switch__checkbox").getAttribute("aria-checked"),
"false"
);
});
test("it renders a toggle button in a enabled state", async function (assert) {
this.set("state", true);
await render(hbs`<DToggleSwitch @state={{this.state}}/>`);
assert.ok(exists(".d-toggle-switch"), "it renders a toggle switch");
assert.strictEqual(
query(".d-toggle-switch__checkbox").getAttribute("aria-checked"),
"true"
);
});
test("it renders a checkmark icon when enabled", async function (assert) {
this.set("state", true);
await render(hbs`<DToggleSwitch @state={{this.state}}/>`);
assert.ok(exists(".d-toggle-switch__checkbox-slider .d-icon-check"));
});
test("it renders a label for the button", async function (assert) {
I18n.translations[I18n.locale].js.test = { fooLabel: "foo" };
this.set("state", true);
await render(
hbs`<DToggleSwitch @state={{this.state}}/ @label={{this.label}} @translatedLabel={{this.translatedLabel}} />`
);
this.set("label", "test.fooLabel");
assert.strictEqual(
query(".d-toggle-switch__checkbox-label").innerText,
I18n.t("test.fooLabel")
);
this.setProperties({
label: null,
translatedLabel: "bar",
});
assert.strictEqual(
query(".d-toggle-switch__checkbox-label").innerText,
"bar"
);
});
});

View File

@ -7,6 +7,7 @@
@import "conditional-loading-section"; @import "conditional-loading-section";
@import "convert-to-public-topic-modal"; @import "convert-to-public-topic-modal";
@import "d-tooltip"; @import "d-tooltip";
@import "d-toggle-switch";
@import "date-input"; @import "date-input";
@import "date-picker"; @import "date-picker";
@import "date-time-input-range"; @import "date-time-input-range";

View File

@ -0,0 +1,82 @@
.d-toggle-switch {
--toggle-switch-width: 45px;
--toggle-switch-height: 24px;
&:focus {
.d-toggle-switch__checkbox-slider {
outline: 2px solid var(--tertiary);
}
}
&:hover {
.d-toggle-switch__checkbox-slider {
background-color: var(--primary-high);
}
.d-toggle-switch__checkbox[aria-checked="true"]
+ .d-toggle-switch__checkbox-slider {
background-color: var(--tertiary-hover);
}
}
display: flex;
align-items: center;
&__label {
position: relative;
display: inline-block;
cursor: pointer;
}
&__checkbox {
position: absolute;
visibility: hidden;
}
&__checkbox[aria-checked="true"] + .d-toggle-switch__checkbox-slider {
background-color: var(--tertiary);
}
&__checkbox[aria-checked="true"] + .d-toggle-switch__checkbox-slider::before {
left: calc(var(--toggle-switch-width) - 22px);
}
&__checkbox-slider {
display: inline-block;
cursor: pointer;
background: var(--primary-low-mid);
border-radius: 16px;
width: var(--toggle-switch-width);
height: var(--toggle-switch-height);
margin-right: 0.5em;
position: relative;
vertical-align: middle;
transition: background 0.25s;
.d-icon {
font-size: var(--font-down-1);
color: var(--secondary);
left: 7px;
top: 7px;
position: absolute;
}
}
&__checkbox-slider::before,
&__checkbox-slider::after {
content: "";
display: block;
position: absolute;
cursor: pointer;
}
&__checkbox-slider::before {
background: var(--secondary);
border-radius: 50%;
width: calc(var(--toggle-switch-width) / 2.5);
height: calc(var(--toggle-switch-width) / 2.5);
top: 3.5px;
left: 4px;
transition: left 0.25s;
}
}

View File

@ -15,7 +15,7 @@ class Admin::BadgesController < Admin::AdminController
.includes(:badge_grouping) .includes(:badge_grouping)
.includes(:badge_type, :image_upload) .includes(:badge_type, :image_upload)
.references(:badge_grouping) .references(:badge_grouping)
.order("badge_groupings.position, badge_type_id, badges.name") .order("enabled DESC", "badge_groupings.position, badge_type_id, badges.name")
.to_a, .to_a,
protected_system_fields: Badge.protected_system_fields, protected_system_fields: Badge.protected_system_fields,
triggers: Badge.trigger_hash, triggers: Badge.trigger_hash,

View File

@ -6013,7 +6013,8 @@ en:
allow_title: Allow badge to be used as a title allow_title: Allow badge to be used as a title
multiple_grant: Can be granted multiple times multiple_grant: Can be granted multiple times
listable: Show badge on the public badges page listable: Show badge on the public badges page
enabled: Enable badge enabled: enabled
disabled: disabled
icon: Icon icon: Icon
image: Image image: Image
graphic: Graphic graphic: Graphic

View File

@ -217,6 +217,8 @@ export function createData(store) {
{ disabled: true, text: "disabled" }, { disabled: true, text: "disabled" },
], ],
toggleSwitchState: true,
navItems: ["latest", "categories", "top"].map((name) => { navItems: ["latest", "categories", "top"].map((name) => {
let item = NavItem.fromText(name); let item = NavItem.fromText(name);

View File

@ -152,4 +152,14 @@
@translatedLabel={{bs.text}} @translatedLabel={{bs.text}}
/> />
{{/each}} {{/each}}
</StyleguideExample>
<StyleguideExample @title="DToggleSwitch">
<DToggleSwitch
@state={{this.dummy.toggleSwitchState}}
{{on
"click"
(fn (mut this.dummy.toggleSwitchState) (not this.dummy.toggleSwitchState))
}}
/>
</StyleguideExample> </StyleguideExample>