mirror of
				https://github.com/discourse/discourse.git
				synced 2025-02-25 18:55:32 -06:00 
			
		
		
		
	UX: Easily toggle badges in admin badge list (#20225)
This commit is contained in:
		| @@ -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); | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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"> | ||||||
|   | |||||||
| @@ -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> | ||||||
| @@ -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; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -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" | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -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"; | ||||||
|   | |||||||
| @@ -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; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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> | ||||||
		Reference in New Issue
	
	Block a user