FEATURE: Allow watched words to be created as a group (#26632)

At the moment, there is no way to create a group of related watched words together.  If a user needed a set of words to be created together, they'll have to create them individually one at a time.

This change attempts to allow related watched words to be created as a group. The idea here is to have a list of words be tied together via a common `WatchedWordGroup` record.  Given a list of words, a `WatchedWordGroup` record is created and assigned to each `WatchedWord` record. The existing WatchedWord creation behaviour remains largely unchanged.

Co-authored-by: Selase Krakani <skrakani@gmail.com>
Co-authored-by: Martin Brennan <martin@discourse.org>
This commit is contained in:
Vinoth Kannan
2024-04-29 15:50:55 +05:30
committed by GitHub
parent 0c8f531909
commit 143f06f2c6
18 changed files with 327 additions and 88 deletions

View File

@@ -1,14 +1,13 @@
<div class="watched-word-input">
<label for="watched-word">{{i18n "admin.watched_words.form.label"}}</label>
<TextField
@id="watched-word"
@value={{this.word}}
@disabled={{this.formSubmitted}}
@autocorrect="off"
@autocapitalize="off"
@placeholderKey={{this.placeholderKey}}
@title={{i18n this.placeholderKey}}
class="watched-word-input-field"
<WatchedWords
@id="watched-words"
@value={{this.words}}
@onChange={{action (mut this.words)}}
@options={{hash
filterPlaceholder=this.placeholderKey
disabled=this.formSubmitted
}}
/>
</div>

View File

@@ -1,11 +1,11 @@
import Component from "@ember/component";
import { action } from "@ember/object";
import { equal, not } from "@ember/object/computed";
import { schedule } from "@ember/runloop";
import { empty, equal } from "@ember/object/computed";
import { service } from "@ember/service";
import { isEmpty } from "@ember/utils";
import { classNames, tagName } from "@ember-decorators/component";
import { observes } from "@ember-decorators/object";
import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "discourse-i18n";
import WatchedWord from "admin/models/watched-word";
@@ -18,10 +18,11 @@ export default class WatchedWordForm extends Component {
formSubmitted = false;
actionKey = null;
showMessage = false;
selectedTags = null;
isCaseSensitive = false;
selectedTags = [];
words = [];
@not("word") submitDisabled;
@empty("words") submitDisabled;
@equal("actionKey", "replace") canReplace;
@@ -29,11 +30,6 @@ export default class WatchedWordForm extends Component {
@equal("actionKey", "link") canLink;
didInsertElement() {
super.didInsertElement(...arguments);
this.set("selectedTags", []);
}
@discourseComputed("siteSettings.watched_words_regular_expressions")
placeholderKey(watchedWordsRegularExpressions) {
if (watchedWordsRegularExpressions) {
@@ -43,29 +39,38 @@ export default class WatchedWordForm extends Component {
}
}
@observes("word")
@observes("words.[]")
removeMessage() {
if (this.showMessage && !isEmpty(this.word)) {
if (this.showMessage && !isEmpty(this.words)) {
this.set("showMessage", false);
}
}
@discourseComputed("word")
isUniqueWord(word) {
const words = this.filteredContent || [];
const filtered = words.filter(
(content) => content.action === this.actionKey
);
return filtered.every((content) => {
if (content.case_sensitive === true) {
return content.word !== word;
}
return content.word.toLowerCase() !== word.toLowerCase();
@observes("actionKey")
actionChanged() {
this.setProperties({
showMessage: false,
});
}
focusInput() {
schedule("afterRender", () => this.element.querySelector("input").focus());
@discourseComputed("words.[]")
isUniqueWord(words) {
const existingWords = this.filteredContent || [];
const filtered = existingWords.filter(
(content) => content.action === this.actionKey
);
const duplicate = filtered.find((content) => {
if (content.case_sensitive === true) {
return words.includes(content.word);
} else {
return words
.map((w) => w.toLowerCase())
.includes(content.word.toLowerCase());
}
});
return !duplicate;
}
@action
@@ -90,7 +95,7 @@ export default class WatchedWordForm extends Component {
this.set("formSubmitted", true);
const watchedWord = WatchedWord.create({
word: this.word,
words: this.words,
replacement:
this.canReplace || this.canTag || this.canLink
? this.replacement
@@ -103,30 +108,23 @@ export default class WatchedWordForm extends Component {
.save()
.then((result) => {
this.setProperties({
word: "",
words: [],
replacement: "",
formSubmitted: false,
selectedTags: [],
showMessage: true,
message: I18n.t("admin.watched_words.form.success"),
isCaseSensitive: false,
});
this.action(WatchedWord.create(result));
this.focusInput();
if (result.words) {
result.words.forEach((word) => {
this.action(WatchedWord.create(word));
});
} else {
this.action(result);
}
})
.catch((e) => {
this.set("formSubmitted", false);
const message = e.jqXHR.responseJSON?.errors
? I18n.t("generic_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
})
: I18n.t("generic_error");
this.dialog.alert({
message,
didConfirm: () => this.focusInput(),
didCancel: () => this.focusInput(),
});
});
.catch(popupAjaxError)
.finally(this.set("formSubmitted", false));
}
}
}

View File

@@ -34,7 +34,7 @@ export default class WatchedWord extends EmberObject {
{
type: this.id ? "PUT" : "POST",
data: {
word: this.word,
words: this.words,
replacement: this.replacement,
action_key: this.action,
case_sensitive: this.isCaseSensitive,