FEATURE: allows multiple custom emoji groups (#9308)

FEATURE: allows multiple custom emoji groups (#9308)

Note: DBHelper would fail with a sql syntax error on columns like “group”.

Co-authored-by: Jarek Radosz jradosz@gmail.com

diff --git a/app/assets/javascripts/admin/controllers/admin-emojis.js b/app/assets/javascripts/admin/controllers/admin-emojis.js
index 6a1d295..1a49670 100644
--- a/app/assets/javascripts/admin/controllers/admin-emojis.js
+++ b/app/assets/javascripts/admin/controllers/admin-emojis.js
@@ -1,37 +1,74 @@
 import { sort } from "@ember/object/computed";
-import EmberObject from "@ember/object";
+import EmberObject, { action, computed } from "@ember/object";
 import Controller from "@ember/controller";
 import { ajax } from "discourse/lib/ajax";
+
+const ALL_FILTER = "all";
+
 export default Controller.extend({
-  sortedEmojis: sort("model", "emojiSorting"),
+  filter: null,
+  sorting: null,
 
   init() {
     this._super(...arguments);
 
-    this.emojiSorting = ["name"];
+    this.setProperties({
+      filter: ALL_FILTER,
+      sorting: ["group", "name"]
+    });
   },
 
-  actions: {
-    emojiUploaded(emoji) {
-      emoji.url += "?t=" + new Date().getTime();
-      this.model.pushObject(EmberObject.create(emoji));
-    },
-
-    destroy(emoji) {
-      return bootbox.confirm(
-        I18n.t("admin.emoji.delete_confirm", { name: emoji.get("name") }),
-        I18n.t("no_value"),
-        I18n.t("yes_value"),
-        destroy => {
-          if (destroy) {
-            return ajax("/admin/customize/emojis/" + emoji.get("name"), {
-              type: "DELETE"
-            }).then(() => {
-              this.model.removeObject(emoji);
-            });
-          }
-        }
-      );
+  sortedEmojis: sort("filteredEmojis.[]", "sorting"),
+
+  emojiGroups: computed("model", {
+    get() {
+      return this.model.mapBy("group").uniq();
+    }
+  }),
+
+  sortingGroups: computed("emojiGroups.[]", {
+    get() {
+      return [ALL_FILTER].concat(this.emojiGroups);
+    }
+  }),
+
+  filteredEmojis: computed("model.[]", "filter", {
+    get() {
+      if (!this.filter || this.filter === ALL_FILTER) {
+        return this.model;
+      } else {
+        return this.model.filterBy("group", this.filter);
+      }
     }
+  }),
+
+  @action
+  filterGroups(value) {
+    this.set("filter", value);
+  },
+
+  @action
+  emojiUploaded(emoji, group) {
+    emoji.url += "?t=" + new Date().getTime();
+    emoji.group = group;
+    this.model.pushObject(EmberObject.create(emoji));
+  },
+
+  @action
+  destroyEmoji(emoji) {
+    return bootbox.confirm(
+      I18n.t("admin.emoji.delete_confirm", { name: emoji.get("name") }),
+      I18n.t("no_value"),
+      I18n.t("yes_value"),
+      destroy => {
+        if (destroy) {
+          return ajax("/admin/customize/emojis/" + emoji.get("name"), {
+            type: "DELETE"
+          }).then(() => {
+            this.model.removeObject(emoji);
+          });
+        }
+      }
+    );
   }
 });
diff --git a/app/assets/javascripts/admin/templates/emojis.hbs b/app/assets/javascripts/admin/templates/emojis.hbs
index 5f9de81..f2547b9 100644
--- a/app/assets/javascripts/admin/templates/emojis.hbs
+++ b/app/assets/javascripts/admin/templates/emojis.hbs
@@ -1,35 +1,49 @@
-<div class='emoji'>
-  <h2>{{i18n 'admin.emoji.title'}}</h2>
+<div class='admin-emojis'>
+  <h1>{{i18n 'admin.emoji.title'}}</h1>
 
-  <p class="desc">{{i18n 'admin.emoji.help'}}</p>
+  <p class="desc">{{i18n "admin.emoji.help"}}</p>
 
-  <p>{{emoji-uploader done=(action "emojiUploaded")}}</p>
+  {{emoji-uploader
+    emojiGroups=emojiGroups
+    done=(action "emojiUploaded")
+  }}
+
+  <hr>
 
   {{#if sortedEmojis}}
-    <div>
-      <table id="custom_emoji">
-        <thead>
+    <table id="custom_emoji">
+      <thead>
+        <tr>
+          <th>{{i18n "admin.emoji.image"}}</th>
+          <th>{{i18n "admin.emoji.name"}}</th>
+          <th>
+            {{combo-box
+              value=filter
+              content=sortingGroups
+              nameProperty=null
+              valueProperty=null
+              onChange=(action "filterGroups")
+            }}
+          </th>
+          <th></th>
+        </tr>
+      </thead>
+      <tbody>
+        {{#each sortedEmojis as |e|}}
           <tr>
-            <th>{{i18n "admin.emoji.image"}}</th>
-            <th>{{i18n "admin.emoji.name"}}</th>
-            <th></th>
+            <th><img class="emoji emoji-custom" src={{e.url}} title={{e.name}}></th>
+            <th>:{{e.name}}:</th>
+            <th>{{e.group}}</th>
+            <th>
+              {{d-button
+                action=(action "destroyEmoji" e)
+                class="btn-danger"
+                icon="far-trash-alt"
+              }}
+            </th>
           </tr>
-        </thead>
-        <tbody>
-          {{#each sortedEmojis as |e|}}
-            <tr>
-              <th><img class="emoji emoji-custom" src={{e.url}} title={{e.name}}></th>
-              <th>:{{e.name}}:</th>
-              <th>
-                {{d-button
-                  action=(action "destroy" e)
-                  class="btn-danger pull-right"
-                  icon="far-trash-alt"}}
-              </th>
-            </tr>
-          {{/each}}
-        </tbody>
-      </table>
-    </div>
+        {{/each}}
+      </tbody>
+    </table>
   {{/if}}
 </div>
diff --git a/app/assets/javascripts/discourse/components/emoji-picker.js b/app/assets/javascripts/discourse/components/emoji-picker.js
index 2696ca1..ef56c41 100644
--- a/app/assets/javascripts/discourse/components/emoji-picker.js
+++ b/app/assets/javascripts/discourse/components/emoji-picker.js
@@ -14,9 +14,26 @@ import ENV, { INPUT_DELAY } from "discourse-common/config/environment";
 const { run } = Ember;
 
 const PER_ROW = 11;
-const customEmojis = _.keys(extendedEmojiList()).map(code => {
-  return { code, src: emojiUrlFor(code) };
-});
+function customEmojis() {
+  const list = extendedEmojiList();
+  const emojis = Object.keys(list)
+    .map(code => {
+      const { group } = list[code];
+      return {
+        code,
+        src: emojiUrlFor(code),
+        group,
+        key: `emoji_picker.${group || "default"}`
+      };
+    })
+    .reduce((acc, curr) => {
+      if (!acc[curr.group]) acc[curr.group] = [];
+      acc[curr.group].push(curr);
+      return acc;
+    }, {});
+
+  return Object.values(emojis);
+}
 
 export default Component.extend({
   automaticPositioning: true,
@@ -35,7 +52,9 @@ export default Component.extend({
   },
 
   show() {
-    const template = findRawTemplate("emoji-picker")({ customEmojis });
+    const template = findRawTemplate("emoji-picker")({
+      customEmojis: customEmojis()
+    });
     this.$picker.html(template);
 
     this.$filter = this.$picker.find(".filter");
@@ -579,7 +598,7 @@ export default Component.extend({
       this.$picker.width() -
       this.$picker.find(".categories-column").width() -
       this.$picker.find(".diversity-picker").width() -
-      32;
+      60;
     this.$picker.find(".info").css("max-width", infoMaxWidth);
   },
 
diff --git a/app/assets/javascripts/discourse/components/emoji-uploader.js b/app/assets/javascripts/discourse/components/emoji-uploader.js
index 77e2d5e..9693811 100644
--- a/app/assets/javascripts/discourse/components/emoji-uploader.js
+++ b/app/assets/javascripts/discourse/components/emoji-uploader.js
@@ -1,23 +1,53 @@
 import { notEmpty, not } from "@ember/object/computed";
+import { action } from "@ember/object";
 import Component from "@ember/component";
 import discourseComputed from "discourse-common/utils/decorators";
 import UploadMixin from "discourse/mixins/upload";
 
+const DEFAULT_GROUP = "default";
+
 export default Component.extend(UploadMixin, {
   type: "emoji",
   uploadUrl: "/admin/customize/emojis",
   hasName: notEmpty("name"),
+  hasGroup: notEmpty("group"),
   addDisabled: not("hasName"),
+  group: "default",
+  emojiGroups: null,
+  newEmojiGroups: null,
+  tagName: null,
+
+  didReceiveAttrs() {
+    this._super(...arguments);
+
+    this.set("newEmojiGroups", this.emojiGroups);
+  },
 
   uploadOptions() {
-    return {
-      sequentialUploads: true
-    };

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

GitHub sha: 0996c3b7

This commit appears in #9308 which was approved by eviltrout. It was merged by jjaffeux.