FEATURE: Watched words improvements (#7899)

FEATURE: Watched words improvements (#7899)

This commit contains 3 features:

  • FEATURE: Allow downloading watched words This introduces a button that allows admins to download watched words per action in a .txt file.

  • FEATURE: Allow clearing watched words in bulk This adds a “Clear All” button that clears all deleted words per action (e.g. block, flag etc.)

  • FEATURE: List all blocked words contained in the post when it’s blocked When a post is rejected because it contains one or more blocked words, the error message now lists all the blocked words contained in the post.


This also changes the format of the file for importing watched words from .csv to .txt so it becomes inconsistent with the extension of the file when watched words are exported.

diff --git a/app/assets/javascripts/admin/components/watched-word-uploader.js.es6 b/app/assets/javascripts/admin/components/watched-word-uploader.js.es6
index 5f047a2..b170633 100644
--- a/app/assets/javascripts/admin/components/watched-word-uploader.js.es6
+++ b/app/assets/javascripts/admin/components/watched-word-uploader.js.es6
@@ -2,13 +2,13 @@ import computed from "ember-addons/ember-computed-decorators";
 import UploadMixin from "discourse/mixins/upload";
 
 export default Ember.Component.extend(UploadMixin, {
-  type: "csv",
+  type: "txt",
   classNames: "watched-words-uploader",
   uploadUrl: "/admin/logs/watched_words/upload",
   addDisabled: Ember.computed.alias("uploading"),
 
   validateUploadedFilesOptions() {
-    return { csvOnly: true };
+    return { skipValidation: true };
   },
 
   @computed("actionKey")
diff --git a/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 b/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6
index 2e38279..2e12605 100644
--- a/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6
@@ -1,5 +1,7 @@
 import computed from "ember-addons/ember-computed-decorators";
 import WatchedWord from "admin/models/watched-word";
+import { ajax } from "discourse/lib/ajax";
+import { fmt } from "discourse/lib/computed";
 
 export default Ember.Controller.extend({
   actionNameKey: null,
@@ -8,6 +10,10 @@ export default Ember.Controller.extend({
     "adminWatchedWords.filtered",
     "adminWatchedWords.showWords"
   ),
+  downloadLink: fmt(
+    "actionNameKey",
+    "/admin/logs/watched_words/action/%@/download"
+  ),
 
   findAction(actionName) {
     return (this.get("adminWatchedWords.model") || []).findBy(
@@ -17,13 +23,13 @@ export default Ember.Controller.extend({
   },
 
   @computed("actionNameKey", "adminWatchedWords.model")
-  filteredContent(actionNameKey) {
-    if (!actionNameKey) {
-      return [];
-    }
+  currentAction(actionName) {
+    return this.findAction(actionName);
+  },
 
-    const a = this.findAction(actionNameKey);
-    return a ? a.words : [];
+  @computed("currentAction.words.[]", "adminWatchedWords.model")
+  filteredContent(words) {
+    return words || [];
   },
 
   @computed("actionNameKey")
@@ -31,10 +37,9 @@ export default Ember.Controller.extend({
     return I18n.t("admin.watched_words.action_descriptions." + actionNameKey);
   },
 
-  @computed("actionNameKey", "adminWatchedWords.model")
-  wordCount(actionNameKey) {
-    const a = this.findAction(actionNameKey);
-    return a ? a.words.length : 0;
+  @computed("currentAction.count")
+  wordCount(count) {
+    return count || 0;
   },
 
   actions: {
@@ -62,10 +67,9 @@ export default Ember.Controller.extend({
     },
 
     recordRemoved(arg) {
-      const a = this.findAction(this.actionNameKey);
-      if (a) {
-        a.words.removeObject(arg);
-        a.decrementProperty("count");
+      if (this.currentAction) {
+        this.currentAction.words.removeObject(arg);
+        this.currentAction.decrementProperty("count");
       }
     },
 
@@ -73,6 +77,30 @@ export default Ember.Controller.extend({
       WatchedWord.findAll().then(data => {
         this.set("adminWatchedWords.model", data);
       });
+    },
+
+    clearAll() {
+      const actionKey = this.actionNameKey;
+      bootbox.confirm(
+        I18n.t(`admin.watched_words.clear_all_confirm_${actionKey}`),
+        I18n.t("no_value"),
+        I18n.t("yes_value"),
+        result => {
+          if (result) {
+            ajax(`/admin/logs/watched_words/action/${actionKey}.json`, {
+              method: "DELETE"
+            }).then(() => {
+              const action = this.findAction(actionKey);
+              if (action) {
+                action.setProperties({
+                  words: [],
+                  count: 0
+                });
+              }
+            });
+          }
+        }
+      );
     }
   }
 });
diff --git a/app/assets/javascripts/admin/templates/components/watched-word-uploader.hbs b/app/assets/javascripts/admin/templates/components/watched-word-uploader.hbs
index 4042bc5..d2422da 100644
--- a/app/assets/javascripts/admin/templates/components/watched-word-uploader.hbs
+++ b/app/assets/javascripts/admin/templates/components/watched-word-uploader.hbs
@@ -1,7 +1,6 @@
 <label class="btn btn-default {{if addDisabled 'disabled'}}">
   {{d-icon "upload"}}
   {{i18n 'admin.watched_words.form.upload'}}
-  <input class="hidden-upload-field" disabled={{addDisabled}} type="file" accept="text/plain,text/csv" />
+  <input class="hidden-upload-field" disabled={{addDisabled}} type="file" accept="text/plain" />
 </label>
-<br/>
 <span class="instructions">{{i18n 'admin.watched_words.one_word_per_line'}}</span>
diff --git a/app/assets/javascripts/admin/templates/watched-words-action.hbs b/app/assets/javascripts/admin/templates/watched-words-action.hbs
index ad04b20..9e81157 100644
--- a/app/assets/javascripts/admin/templates/watched-words-action.hbs
+++ b/app/assets/javascripts/admin/templates/watched-words-action.hbs
@@ -3,14 +3,24 @@
 <p class="about">{{actionDescription}}</p>
 
 <div class="watched-word-controls">
-{{watched-word-form
-  actionKey=actionNameKey
-  action=(action "recordAdded")
-  filteredContent=filteredContent
-  regularExpressions=adminWatchedWords.regularExpressions}}
+  {{watched-word-form
+    actionKey=actionNameKey
+    action=(action "recordAdded")
+    filteredContent=filteredContent
+    regularExpressions=adminWatchedWords.regularExpressions}}
 
-{{watched-word-uploader uploading=uploading actionKey=actionNameKey done=(action "uploadComplete")}}
+  <div class="download-upload-controls">
+    <div class="download">
+      {{d-button
+        class="btn-default download-link"
+        href=downloadLink
+        icon="download"
+        label="admin.watched_words.download"}}
+    </div>
+    {{watched-word-uploader uploading=uploading actionKey=actionNameKey done=(action "uploadComplete")}}
+  </div>
 </div>
+
 <div>
   <label class="show-words-checkbox">
     {{input type="checkbox" checked=adminWatchedWords.showWords disabled=adminWatchedWords.disableShowWords}}
@@ -26,3 +36,11 @@
     {{i18n 'admin.watched_words.word_count' count=wordCount}}
   {{/if}}
 </div>
+
+<div class="clear-all-row">
+  {{d-button
+    class="btn-danger clear-all"
+    label="admin.watched_words.clear_all"
+    icon="trash-alt"
+    action=(action "clearAll")}}
+</div>
diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6
index 18af8ae..66b8282 100644
--- a/app/assets/javascripts/discourse/lib/url.js.es6
+++ b/app/assets/javascripts/discourse/lib/url.js.es6
@@ -23,7 +23,8 @@ const SERVER_SIDE_ONLY = [
   /\.rss$/,
   /\.json$/,
   /^\/admin\/upgrade$/,
-  /^\/logs($|\/)/
+  /^\/logs($|\/)/,
+  /^\/admin\/logs\/watched_words\/action\/[^\/]+\/download$/
 ];
 
 export function rewritePath(path) {
diff --git a/app/assets/stylesheets/common/admin/staff_logs.scss b/app/assets/stylesheets/common/admin/staff_logs.scss
index bbfd6ae..349480b 100644
--- a/app/assets/stylesheets/common/admin/staff_logs.scss
+++ b/app/assets/stylesheets/common/admin/staff_logs.scss
@@ -362,17 +362,33 @@ table.screened-ip-addresses {
   display: inline-block;
   width: 250px;
   margin-bottom: 1em;
-  float: left;
+  vertical-align: top;
+}
+
+.admin-watched-words {
+  .clear-all-row {
+    display: flex;
+    margin-top: 10px;
+    justify-content: flex-end;
+  }
 }
 
 .watched-word-controls {
   display: flex;
   flex-wrap: wrap;
   margin-bottom: 1em;
+  justify-content: space-between;
+  .download-upload-controls {
+    display: flex;
+  }
+  .download {
+    justify-content: flex-end;
+  }
 }
 
 .watched-words-list {
   margin-top: 20px;
+  display: inline-block;
 }
 
 .watched-word {
@@ -395,13 +411,17 @@ table.screened-ip-addresses {
 }
 
 .watched-words-uploader {
-  margin-left: auto;

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

GitHub sha: f14c6d81