UX: Add image uploader widget for uploading badge images (#12377)

UX: Add image uploader widget for uploading badge images (#12377)

Currently the process of adding a custom image to badge is quite clunky; you have to upload your image to a topic, and then copy the image URL and pasting it in a text field. Besides being clucky, if the topic or post that contains the image is deleted, the image will be garbage-collected in a few days and the badge will lose the image because the application is not that the image is referenced by a badge.

This commit improves that by adding a proper image uploader widget for badge images.

diff --git a/app/assets/javascripts/admin/addon/controllers/admin-badges-show.js b/app/assets/javascripts/admin/addon/controllers/admin-badges-show.js
index 19f1d6a..5e1ed5b 100644
--- a/app/assets/javascripts/admin/addon/controllers/admin-badges-show.js
+++ b/app/assets/javascripts/admin/addon/controllers/admin-badges-show.js
@@ -5,19 +5,27 @@ import bootbox from "bootbox";
 import { bufferedProperty } from "discourse/mixins/buffered-content";
 import { popupAjaxError } from "discourse/lib/ajax-error";
 import { propertyNotEqual } from "discourse/lib/computed";
-import { reads } from "@ember/object/computed";
+import { equal, reads } from "@ember/object/computed";
 import { run } from "@ember/runloop";
+import { action } from "@ember/object";
+import getURL from "discourse-common/lib/get-url";
+
+const IMAGE = "image";
+const ICON = "icon";
 
 export default Controller.extend(bufferedProperty("model"), {
   adminBadges: controller(),
   saving: false,
   savingStatus: "",
+  selectedGraphicType: null,
   badgeTypes: reads("adminBadges.badgeTypes"),
   badgeGroupings: reads("adminBadges.badgeGroupings"),
   badgeTriggers: reads("adminBadges.badgeTriggers"),
   protectedSystemFields: reads("adminBadges.protectedSystemFields"),
   readOnly: reads("buffered.system"),
   showDisplayName: propertyNotEqual("name", "displayName"),
+  iconSelectorSelected: equal("selectedGraphicType", ICON),
+  imageUploaderSelected: equal("selectedGraphicType", IMAGE),
 
   init() {
     this._super(...arguments);
@@ -67,6 +75,41 @@ export default Controller.extend(bufferedProperty("model"), {
     this.set("savingStatus", "");
   },
 
+  showIconSelector() {
+    this.set("selectedGraphicType", ICON);
+  },
+
+  showImageUploader() {
+    this.set("selectedGraphicType", IMAGE);
+  },
+
+  @action
+  changeGraphicType(newType) {
+    if (newType === IMAGE) {
+      this.showImageUploader();
+    } else if (newType === ICON) {
+      this.showIconSelector();
+    } else {
+      throw new Error(`Unknown badge graphic type "${newType}"`);
+    }
+  },
+
+  @action
+  setImage(upload) {
+    this.buffered.setProperties({
+      image_upload_id: upload.id,
+      image_url: getURL(upload.url),
+    });
+  },
+
+  @action
+  removeImage() {
+    this.buffered.setProperties({
+      image_upload_id: null,
+      image_url: null,
+    });
+  },
+
   actions: {
     save() {
       if (!this.saving) {
@@ -82,7 +125,7 @@ export default Controller.extend(bufferedProperty("model"), {
           "description",
           "long_description",
           "icon",
-          "image",
+          "image_upload_id",
           "query",
           "badge_grouping_id",
           "trigger",
diff --git a/app/assets/javascripts/admin/addon/routes/admin-badges-show.js b/app/assets/javascripts/admin/addon/routes/admin-badges-show.js
index a370cd3..7053be2 100644
--- a/app/assets/javascripts/admin/addon/routes/admin-badges-show.js
+++ b/app/assets/javascripts/admin/addon/routes/admin-badges-show.js
@@ -23,6 +23,15 @@ export default Route.extend({
     );
   },
 
+  setupController(controller, model) {
+    this._super(...arguments);
+    if (model.image_url) {
+      controller.showImageUploader();
+    } else if (model.icon) {
+      controller.showIconSelector();
+    }
+  },
+
   actions: {
     saveError(e) {
       let msg = I18n.t("generic_error");
diff --git a/app/assets/javascripts/admin/addon/templates/badges-show.hbs b/app/assets/javascripts/admin/addon/templates/badges-show.hbs
index 429f0cb..e71ed63 100644
--- a/app/assets/javascripts/admin/addon/templates/badges-show.hbs
+++ b/app/assets/javascripts/admin/addon/templates/badges-show.hbs
@@ -15,23 +15,48 @@
     </div>
 
     <div>
-      <label for="icon">{{i18n "admin.badges.icon"}}</label>
-      {{icon-picker
-        name="icon"
-        value=buffered.icon
-        options=(hash
-          maximum=1
-        )
-        onChange=(action (mut buffered.icon))
-      }}
-
-      <p class="help">{{i18n "admin.badges.icon_help"}}</p>
-    </div>
-
-    <div>
-      <label for="image">{{i18n "admin.badges.image"}}</label>
-      {{input type="text" name="image" value=buffered.image}}
-      <p class="help">{{i18n "admin.badges.image_help"}}</p>
+      <label for="graphic">{{i18n "admin.badges.graphic"}}</label>
+      <div class="radios">
+        <label class="radio-label" for="badge-icon">
+          {{radio-button
+            name="badge-icon"
+            id="badge-icon"
+            value="icon"
+            selection=selectedGraphicType
+            onChange=(action "changeGraphicType")
+          }}
+          <span>{{i18n "admin.badges.select_an_icon"}}</span>
+        </label>
+
+        <label class="radio-label" for="badge-image">
+          {{radio-button
+            name="badge-image"
+            id="badge-image"
+            value="image"
+            selection=selectedGraphicType
+            onChange=(action "changeGraphicType")
+          }}
+          <span>{{i18n "admin.badges.upload_an_image"}}</span>
+        </label>
+      </div>
+      {{#if imageUploaderSelected}}
+        {{image-uploader
+          imageUrl=buffered.image_url
+          onUploadDone=(action "setImage")
+          onUploadDeleted=(action "removeImage")
+          type="badge_image"
+          class="no-repeat contain-image"}}
+        <div class="control-instructions">
+          <p class="help">{{i18n "admin.badges.image_help"}}</p>
+        </div>
+      {{else if iconSelectorSelected}}
+        {{icon-picker
+          name="icon"
+          value=buffered.icon
+          options=(hash maximum=1)
+          onChange=(action (mut buffered.icon))
+        }}
+      {{/if}}
     </div>
 
     <div>
diff --git a/app/assets/javascripts/discourse/app/models/badge.js b/app/assets/javascripts/discourse/app/models/badge.js
index 99648d7..f949312 100644
--- a/app/assets/javascripts/discourse/app/models/badge.js
+++ b/app/assets/javascripts/discourse/app/models/badge.js
@@ -5,10 +5,11 @@ import RestModel from "discourse/models/rest";
 import { ajax } from "discourse/lib/ajax";
 import discourseComputed from "discourse-common/utils/decorators";
 import getURL from "discourse-common/lib/get-url";
-import { none } from "@ember/object/computed";
+import { alias, none } from "@ember/object/computed";
 
 const Badge = RestModel.extend({
   newBadge: none("id"),
+  image: alias("image_url"),
 
   @discourseComputed
   url() {
diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-badges-show-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-badges-show-test.js
new file mode 100644
index 0000000..3da3a7a
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/acceptance/admin-badges-show-test.js
@@ -0,0 +1,101 @@
+import {
+  acceptance,
+  exists,
+  query,
+} from "discourse/tests/helpers/qunit-helpers";
+import { click, visit } from "@ember/test-helpers";
+import { test } from "qunit";
+
+acceptance("Admin - Badges - Show", function (needs) {
+  needs.user();
+  test("new badge page", async function (assert) {
+    await visit("/admin/badges/new");
+    assert.ok(
+      !query("input#badge-icon").checked,
+      "radio button for selecting an icon is off initially"
+    );
+    assert.ok(
+      !query("input#badge-image").checked,
+      "radio button for uploading an image is off initially"
+    );
+    assert.ok(!exists(".icon-picker"), "icon picker is not visible");
+    assert.ok(!exists(".image-uploader"), "image uploader is not visible");
+
+    await click("input#badge-icon");
+    assert.ok(
+      exists(".icon-picker"),
+      "icon picker is visible after clicking the select icon radio button"
+    );
+    assert.ok(!exists(".image-uploader"), "image uploader remains hidden");
+
+    await click("input#badge-image");
+    assert.ok(
+      !exists(".icon-picker"),
+      "icon picker is hidden after clicking the upload image radio button"
+    );
+    assert.ok(
+      exists(".image-uploader"),

[... diff too long, it was truncated ...]

GitHub sha: a23d0f99

This commit appears in #12377 which was approved by eviltrout. It was merged by OsamaSayegh.

This commit has been mentioned on Discourse Meta. There might be relevant details there: