mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: ability to add description to tags (#15125)
Ability to add description to tags, which will be displayed on hover.
This commit is contained in:
parent
78723345c0
commit
9cabd3721b
@ -6,7 +6,7 @@ import bootbox from "bootbox";
|
|||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import { isEmpty } from "@ember/utils";
|
import { isEmpty } from "@ember/utils";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
tagName: "",
|
tagName: "",
|
||||||
@ -16,6 +16,10 @@ export default Component.extend({
|
|||||||
showEditControls: false,
|
showEditControls: false,
|
||||||
canAdminTag: reads("currentUser.staff"),
|
canAdminTag: reads("currentUser.staff"),
|
||||||
editSynonymsMode: and("canAdminTag", "showEditControls"),
|
editSynonymsMode: and("canAdminTag", "showEditControls"),
|
||||||
|
editing: false,
|
||||||
|
newTagName: null,
|
||||||
|
newTagDescription: null,
|
||||||
|
router: service(),
|
||||||
|
|
||||||
@discourseComputed("tagInfo.tag_group_names")
|
@discourseComputed("tagInfo.tag_group_names")
|
||||||
tagGroupsInfo(tagGroupNames) {
|
tagGroupsInfo(tagGroupNames) {
|
||||||
@ -41,6 +45,13 @@ export default Component.extend({
|
|||||||
return isEmpty(tagGroupNames) && isEmpty(categories) && isEmpty(synonyms);
|
return isEmpty(tagGroupNames) && isEmpty(categories) && isEmpty(synonyms);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@discourseComputed("newTagName")
|
||||||
|
updateDisabled(newTagName) {
|
||||||
|
const filterRegexp = new RegExp(this.site.tags_filter_regexp, "g");
|
||||||
|
newTagName = newTagName ? newTagName.replace(filterRegexp, "").trim() : "";
|
||||||
|
return newTagName.length === 0;
|
||||||
|
},
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.loadTagInfo();
|
this.loadTagInfo();
|
||||||
@ -69,8 +80,29 @@ export default Component.extend({
|
|||||||
this.toggleProperty("showEditControls");
|
this.toggleProperty("showEditControls");
|
||||||
},
|
},
|
||||||
|
|
||||||
renameTag() {
|
edit() {
|
||||||
showModal("rename-tag", { model: this.tag });
|
this.setProperties({
|
||||||
|
editing: true,
|
||||||
|
newTagName: this.tag.id,
|
||||||
|
newTagDescription: this.tagInfo.description,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelEditing() {
|
||||||
|
this.set("editing", false);
|
||||||
|
},
|
||||||
|
|
||||||
|
finishedEditing() {
|
||||||
|
this.tag
|
||||||
|
.update({ id: this.newTagName, description: this.newTagDescription })
|
||||||
|
.then((result) => {
|
||||||
|
this.set("editing", false);
|
||||||
|
this.tagInfo.set("description", this.newTagDescription);
|
||||||
|
if (result.payload) {
|
||||||
|
this.router.transitionTo("tag.show", result.payload.id);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError);
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteTag() {
|
deleteTag() {
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
import { action } from "@ember/object";
|
|
||||||
import BufferedContent from "discourse/mixins/buffered-content";
|
|
||||||
import Controller from "@ember/controller";
|
|
||||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
|
||||||
import { extractError } from "discourse/lib/ajax-error";
|
|
||||||
|
|
||||||
export default Controller.extend(ModalFunctionality, BufferedContent, {
|
|
||||||
newTag: null,
|
|
||||||
|
|
||||||
@discourseComputed("newTag", "model.id")
|
|
||||||
renameDisabled(newTag, currentTag) {
|
|
||||||
const filterRegexp = new RegExp(this.site.tags_filter_regexp, "g");
|
|
||||||
newTag = newTag ? newTag.replace(filterRegexp, "").trim() : "";
|
|
||||||
return newTag.length === 0 || newTag === currentTag;
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
performRename() {
|
|
||||||
this.model
|
|
||||||
.update({ id: this.newTag })
|
|
||||||
.then((result) => {
|
|
||||||
this.send("closeModal");
|
|
||||||
|
|
||||||
if (result.responseJson.tag) {
|
|
||||||
this.transitionToRoute("tag.show", result.responseJson.tag.id);
|
|
||||||
} else {
|
|
||||||
this.flash(extractError(result.responseJson.errors[0]), "error");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => this.flash(extractError(error), "error"));
|
|
||||||
},
|
|
||||||
});
|
|
@ -44,6 +44,7 @@ export function defaultRenderTag(tag, params) {
|
|||||||
href +
|
href +
|
||||||
" data-tag-name=" +
|
" data-tag-name=" +
|
||||||
tag +
|
tag +
|
||||||
|
(params.description ? ' title="' + params.description + '" ' : "") +
|
||||||
" class='" +
|
" class='" +
|
||||||
classes.join(" ") +
|
classes.join(" ") +
|
||||||
"'>" +
|
"'>" +
|
||||||
|
@ -55,7 +55,13 @@ export default function (topic, params) {
|
|||||||
if (tags) {
|
if (tags) {
|
||||||
for (let i = 0; i < tags.length; i++) {
|
for (let i = 0; i < tags.length; i++) {
|
||||||
buffer +=
|
buffer +=
|
||||||
renderTag(tags[i], { isPrivateMessage, tagsForUser, tagName }) + " ";
|
renderTag(tags[i], {
|
||||||
|
description:
|
||||||
|
topic.tags_descriptions && topic.tags_descriptions[tags[i]],
|
||||||
|
isPrivateMessage,
|
||||||
|
tagsForUser,
|
||||||
|
tagName,
|
||||||
|
}) + " ";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +1,41 @@
|
|||||||
<section class="tag-info">
|
<section class="tag-info">
|
||||||
{{#if tagInfo}}
|
{{#if tagInfo}}
|
||||||
<div class="tag-name">
|
<div class="tag-name">
|
||||||
|
{{#if editing}}
|
||||||
|
<div class="edit-tag-wrapper">
|
||||||
|
{{text-field id="edit-name" value=(readonly tagInfo.name) maxlength=siteSettings.max_tag_length input=(action (mut newTagName) value="target.value") autofocus="true"}}
|
||||||
|
{{text-field id="edit-description" value=(readonly tagInfo.description) placeholder=(i18n "tagging.description") maxlength=280 input=(action (mut newTagDescription) value="target.value") autofocus="true"}}
|
||||||
|
|
||||||
|
<div class="edit-controls">
|
||||||
|
{{#unless updateDisabled}}
|
||||||
|
{{d-button action=(action "finishedEditing") class="btn-primary submit-edit" icon="check" ariaLabel="tagging.save"}}
|
||||||
|
{{/unless}}
|
||||||
|
{{d-button action=(action "cancelEditing") class="btn-default cancel-edit" icon="times" ariaLabel="cancel"}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="tag-name-wrapper">
|
||||||
{{discourse-tag tagInfo.name tagName="div" size="large"}}
|
{{discourse-tag tagInfo.name tagName="div" size="large"}}
|
||||||
|
{{#if canAdminTag}}
|
||||||
|
<a href {{action "edit"}} id="edit-tag" title={{i18n "tagging.edit_tag"}}>{{d-icon "pencil-alt"}}</a>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{#if canAdminTag}}
|
||||||
|
<div class="tag-description-wrapper">
|
||||||
|
{{tagInfo.description}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if canAdminTag}}
|
{{#if canAdminTag}}
|
||||||
{{plugin-outlet name="tag-custom-settings" args=(hash tag=tagInfo) connectorTagName="" tagName="section"}}
|
{{plugin-outlet name="tag-custom-settings" args=(hash tag=tagInfo) connectorTagName="" tagName="section"}}
|
||||||
|
|
||||||
{{d-button class="btn-default" action=(action "renameTag") icon="pencil-alt" label="tagging.rename_tag" id="rename-tag"}}
|
<div class="tag-actions">
|
||||||
{{d-button class="btn-default" action=(action "toggleEditControls") icon="cog" label="tagging.edit_synonyms" id="edit-synonyms"}}
|
{{d-button class="btn-default" action=(action "toggleEditControls") icon="cog" label="tagging.edit_synonyms" id="edit-synonyms"}}
|
||||||
{{#if deleteAction}}
|
{{#if deleteAction}}
|
||||||
{{d-button class="btn-danger delete-tag" action=(action "deleteTag") icon="far-trash-alt" label="tagging.delete_tag" id="delete-tag"}}
|
{{d-button class="btn-danger delete-tag" action=(action "deleteTag") icon="far-trash-alt" label="tagging.delete_tag" id="delete-tag"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
<div class="tag-associations">
|
<div class="tag-associations">
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
{{#each sortedTags as |tag|}}
|
{{#each sortedTags as |tag|}}
|
||||||
<div class="tag-box">
|
<div class="tag-box">
|
||||||
{{discourse-tag tag.id isPrivateMessage=isPrivateMessage pmOnly=tag.pmOnly tagsForUser=tagsForUser}} {{#if tag.pmOnly}}{{d-icon "far-envelope"}}{{/if}}{{#if tag.totalCount}} <span class="tag-count">x {{tag.totalCount}}</span>{{/if}}
|
{{discourse-tag tag.id description=tag.description isPrivateMessage=isPrivateMessage pmOnly=tag.pmOnly tagsForUser=tagsForUser}} {{#if tag.pmOnly}}{{d-icon "far-envelope"}}{{/if}}{{#if tag.totalCount}} <span class="tag-count">x {{tag.totalCount}}</span>{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
{{#d-modal-body title="tagging.rename_tag"}}
|
|
||||||
<label class="control-label">
|
|
||||||
{{i18n "tagging.rename_instructions"}}
|
|
||||||
</label>
|
|
||||||
<div class="controls">
|
|
||||||
{{input
|
|
||||||
value=(readonly model.id)
|
|
||||||
maxlength=siteSettings.max_tag_length
|
|
||||||
input=(action (mut newTag) value="target.value")
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
{{/d-modal-body}}
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
{{d-button
|
|
||||||
class="btn-primary"
|
|
||||||
action=(action "performRename")
|
|
||||||
label="tagging.rename_tag"
|
|
||||||
disabled=renameDisabled
|
|
||||||
}}
|
|
||||||
</div>
|
|
@ -4,6 +4,7 @@ import {
|
|||||||
count,
|
count,
|
||||||
exists,
|
exists,
|
||||||
invisible,
|
invisible,
|
||||||
|
query,
|
||||||
queryAll,
|
queryAll,
|
||||||
updateCurrentUser,
|
updateCurrentUser,
|
||||||
} from "discourse/tests/helpers/qunit-helpers";
|
} from "discourse/tests/helpers/qunit-helpers";
|
||||||
@ -64,6 +65,7 @@ acceptance("Tags", function (needs) {
|
|||||||
bookmarked: false,
|
bookmarked: false,
|
||||||
liked: true,
|
liked: true,
|
||||||
tags: ["test"],
|
tags: ["test"],
|
||||||
|
tags_descriptions: { test: "test description" },
|
||||||
views: 42,
|
views: 42,
|
||||||
like_count: 42,
|
like_count: 42,
|
||||||
has_summary: false,
|
has_summary: false,
|
||||||
@ -355,6 +357,7 @@ acceptance("Tag info", function (needs) {
|
|||||||
tag_info: {
|
tag_info: {
|
||||||
id: 13,
|
id: 13,
|
||||||
name: "happy-monkey",
|
name: "happy-monkey",
|
||||||
|
description: "happy monkey description",
|
||||||
topic_count: 1,
|
topic_count: 1,
|
||||||
staff: false,
|
staff: false,
|
||||||
synonyms: [],
|
synonyms: [],
|
||||||
@ -429,6 +432,28 @@ acceptance("Tag info", function (needs) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("edit tag is showing input for name and description", async function (assert) {
|
||||||
|
updateCurrentUser({ moderator: false, admin: true });
|
||||||
|
|
||||||
|
await visit("/tag/happy-monkey");
|
||||||
|
assert.strictEqual(count("#show-tag-info"), 1);
|
||||||
|
|
||||||
|
await click("#show-tag-info");
|
||||||
|
assert.ok(exists(".tag-info .tag-name"), "show tag");
|
||||||
|
|
||||||
|
await click("#edit-tag");
|
||||||
|
assert.strictEqual(
|
||||||
|
query("#edit-name").value,
|
||||||
|
"happy-monkey",
|
||||||
|
"it displays original tag name"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
query("#edit-description").value,
|
||||||
|
"happy monkey description",
|
||||||
|
"it displays original tag description"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("can filter tags page by category", async function (assert) {
|
test("can filter tags page by category", async function (assert) {
|
||||||
await visit("/tag/planters");
|
await visit("/tag/planters");
|
||||||
|
|
||||||
@ -445,7 +470,7 @@ acceptance("Tag info", function (needs) {
|
|||||||
assert.strictEqual(count("#show-tag-info"), 1);
|
assert.strictEqual(count("#show-tag-info"), 1);
|
||||||
|
|
||||||
await click("#show-tag-info");
|
await click("#show-tag-info");
|
||||||
assert.ok(exists("#rename-tag"), "can rename tag");
|
assert.ok(exists("#edit-tag"), "can rename tag");
|
||||||
assert.ok(exists("#edit-synonyms"), "can edit synonyms");
|
assert.ok(exists("#edit-synonyms"), "can edit synonyms");
|
||||||
assert.ok(exists("#delete-tag"), "can delete tag");
|
assert.ok(exists("#delete-tag"), "can delete tag");
|
||||||
|
|
||||||
|
@ -6405,6 +6405,7 @@ export default {
|
|||||||
bookmarked: false,
|
bookmarked: false,
|
||||||
liked: false,
|
liked: false,
|
||||||
tags: ["test", "test-tag"],
|
tags: ["test", "test-tag"],
|
||||||
|
tags_description: { test: "test description", "test-tag": "test tag description" },
|
||||||
views: 6,
|
views: 6,
|
||||||
like_count: 0,
|
like_count: 0,
|
||||||
has_summary: false,
|
has_summary: false,
|
||||||
|
@ -540,6 +540,10 @@ export default {
|
|||||||
pinned_globally: false,
|
pinned_globally: false,
|
||||||
posters: [],
|
posters: [],
|
||||||
tags: ["dev", "slow"],
|
tags: ["dev", "slow"],
|
||||||
|
tags_descriptions: {
|
||||||
|
"dev": "dev description",
|
||||||
|
"slow": "slow description",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 14727,
|
id: 14727,
|
||||||
|
@ -99,7 +99,11 @@ export default MultiSelectComponent.extend(TagsMixin, {
|
|||||||
return results
|
return results
|
||||||
.filter((r) => !makeArray(context.tags).includes(r.id))
|
.filter((r) => !makeArray(context.tags).includes(r.id))
|
||||||
.map((result) => {
|
.map((result) => {
|
||||||
return { id: result.text, name: result.text, count: result.count };
|
return {
|
||||||
|
id: result.text,
|
||||||
|
name: result.description,
|
||||||
|
count: result.count,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -338,9 +338,32 @@ section.tag-info {
|
|||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-tag-wrapper {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tag-name-wrapper,
|
||||||
|
.tag-description-wrapper {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.tag-name-wrapper a {
|
||||||
|
color: var(--primary-high);
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-name-wrapper a {
|
||||||
|
font-size: var(--font-up-3);
|
||||||
|
}
|
||||||
|
|
||||||
.tag-name .discourse-tag {
|
.tag-name .discourse-tag {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 0.75em;
|
}
|
||||||
|
|
||||||
|
.tag-description-wrapper {
|
||||||
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.synonyms-list,
|
.synonyms-list,
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
@import "reviewables";
|
@import "reviewables";
|
||||||
@import "ring";
|
@import "ring";
|
||||||
@import "search";
|
@import "search";
|
||||||
|
@import "tagging";
|
||||||
@import "topic-list";
|
@import "topic-list";
|
||||||
@import "topic-post";
|
@import "topic-post";
|
||||||
@import "topic";
|
@import "topic";
|
||||||
|
9
app/assets/stylesheets/mobile/tagging.scss
Normal file
9
app/assets/stylesheets/mobile/tagging.scss
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.edit-tag-wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
.edit-controls {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tag-info .tag-actions {
|
||||||
|
display: flex;
|
||||||
|
}
|
@ -135,11 +135,14 @@ class TagsController < ::ApplicationController
|
|||||||
tag = Tag.find_by_name(params[:tag_id])
|
tag = Tag.find_by_name(params[:tag_id])
|
||||||
raise Discourse::NotFound if tag.nil?
|
raise Discourse::NotFound if tag.nil?
|
||||||
|
|
||||||
|
if (params[:tag][:id].present?)
|
||||||
new_tag_name = DiscourseTagging.clean_tag(params[:tag][:id])
|
new_tag_name = DiscourseTagging.clean_tag(params[:tag][:id])
|
||||||
tag.name = new_tag_name
|
tag.name = new_tag_name
|
||||||
|
end
|
||||||
|
tag.description = params[:tag][:description] if params[:tag]&.has_key?(:description)
|
||||||
if tag.save
|
if tag.save
|
||||||
StaffActionLogger.new(current_user).log_custom('renamed_tag', previous_value: params[:tag_id], new_value: new_tag_name)
|
StaffActionLogger.new(current_user).log_custom('renamed_tag', previous_value: params[:tag_id], new_value: new_tag_name)
|
||||||
render json: { tag: { id: new_tag_name } }
|
render json: { tag: { id: tag.name, description: tag.description } }
|
||||||
else
|
else
|
||||||
render_json_error tag.errors.full_messages
|
render_json_error tag.errors.full_messages
|
||||||
end
|
end
|
||||||
@ -353,6 +356,8 @@ class TagsController < ::ApplicationController
|
|||||||
{
|
{
|
||||||
id: t.name,
|
id: t.name,
|
||||||
text: t.name,
|
text: t.name,
|
||||||
|
name: t.name,
|
||||||
|
description: t.description,
|
||||||
count: t.topic_count,
|
count: t.topic_count,
|
||||||
pm_count: show_pm_tags ? t.pm_topic_count : 0,
|
pm_count: show_pm_tags ? t.pm_topic_count : 0,
|
||||||
target_tag: t.target_tag_id ? target_tags.find { |x| x.id == t.target_tag_id }&.name : nil
|
target_tag: t.target_tag_id ? target_tags.find { |x| x.id == t.target_tag_id }&.name : nil
|
||||||
|
@ -15,6 +15,7 @@ class Tag < ActiveRecord::Base
|
|||||||
|
|
||||||
validate :target_tag_validator, if: Proc.new { |t| t.new_record? || t.will_save_change_to_target_tag_id? }
|
validate :target_tag_validator, if: Proc.new { |t| t.new_record? || t.will_save_change_to_target_tag_id? }
|
||||||
validate :name_validator
|
validate :name_validator
|
||||||
|
validates :description, length: { maximum: 280 }
|
||||||
|
|
||||||
scope :where_name, ->(name) do
|
scope :where_name, ->(name) do
|
||||||
name = Array(name).map(&:downcase)
|
name = Array(name).map(&:downcase)
|
||||||
@ -215,6 +216,7 @@ end
|
|||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# pm_topic_count :integer default(0), not null
|
# pm_topic_count :integer default(0), not null
|
||||||
# target_tag_id :integer
|
# target_tag_id :integer
|
||||||
|
# description :string
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
module TopicTagsMixin
|
module TopicTagsMixin
|
||||||
def self.included(klass)
|
def self.included(klass)
|
||||||
klass.attributes :tags
|
klass.attributes :tags
|
||||||
|
klass.attributes :tags_descriptions
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_tags?
|
def include_tags?
|
||||||
@ -10,16 +11,26 @@ module TopicTagsMixin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def tags
|
def tags
|
||||||
# Calling method `pluck` or `order` along with `includes` causing N+1 queries
|
all_tags.map(&:name)
|
||||||
tags = (SiteSetting.tags_sort_alphabetically ? topic.tags.sort_by(&:name) : topic.tags.sort_by(&:topic_count).reverse).map(&:name)
|
|
||||||
if scope.is_staff?
|
|
||||||
tags
|
|
||||||
else
|
|
||||||
tags - scope.hidden_tag_names
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def tags_descriptions
|
||||||
|
all_tags.each.with_object({}) { |tag, acc| acc[tag.name] = tag.description }.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
def topic
|
def topic
|
||||||
object.is_a?(Topic) ? object : object.topic
|
object.is_a?(Topic) ? object : object.topic
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def all_tags
|
||||||
|
return @tags if defined?(@tags)
|
||||||
|
# Calling method `pluck` or `order` along with `includes` causing N+1 queries
|
||||||
|
tags = (SiteSetting.tags_sort_alphabetically ? topic.tags.sort_by(&:name) : topic.tags.sort_by(&:topic_count).reverse)
|
||||||
|
if !scope.is_staff?
|
||||||
|
tags = tags.reject { |tag| scope.hidden_tag_names.include?(tag[:name]) }
|
||||||
|
end
|
||||||
|
@tags = tags
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TagSerializer < ApplicationSerializer
|
class TagSerializer < ApplicationSerializer
|
||||||
attributes :id, :name, :topic_count, :staff
|
attributes :id, :name, :topic_count, :staff, :description
|
||||||
|
|
||||||
def staff
|
def staff
|
||||||
DiscourseTagging.staff_tag_names.include?(name)
|
DiscourseTagging.staff_tag_names.include?(name)
|
||||||
|
@ -3796,6 +3796,7 @@ en:
|
|||||||
category_restricted: "This tag is restricted to categories you don't have permission to access."
|
category_restricted: "This tag is restricted to categories you don't have permission to access."
|
||||||
synonyms: "Synonyms"
|
synonyms: "Synonyms"
|
||||||
synonyms_description: "When the following tags are used, they will be replaced with <b>%{base_tag_name}</b>."
|
synonyms_description: "When the following tags are used, they will be replaced with <b>%{base_tag_name}</b>."
|
||||||
|
save: "Save name and description of the tag"
|
||||||
tag_groups_info:
|
tag_groups_info:
|
||||||
one: 'This tag belongs to the group "%{tag_groups}".'
|
one: 'This tag belongs to the group "%{tag_groups}".'
|
||||||
other: "This tag belongs to these groups: %{tag_groups}."
|
other: "This tag belongs to these groups: %{tag_groups}."
|
||||||
@ -3819,8 +3820,8 @@ en:
|
|||||||
delete_confirm_synonyms:
|
delete_confirm_synonyms:
|
||||||
one: "Its synonym will also be deleted."
|
one: "Its synonym will also be deleted."
|
||||||
other: "Its %{count} synonyms will also be deleted."
|
other: "Its %{count} synonyms will also be deleted."
|
||||||
rename_tag: "Rename Tag"
|
edit_tag: "Edit tag name and description"
|
||||||
rename_instructions: "Choose a new name for the tag:"
|
description: "Description"
|
||||||
sort_by: "Sort by:"
|
sort_by: "Sort by:"
|
||||||
sort_by_count: "count"
|
sort_by_count: "count"
|
||||||
sort_by_name: "name"
|
sort_by_name: "name"
|
||||||
|
7
db/migrate/20211116225901_add_description_to_tags.rb
Normal file
7
db/migrate/20211116225901_add_description_to_tags.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddDescriptionToTags < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :tags, :description, :string
|
||||||
|
end
|
||||||
|
end
|
@ -256,7 +256,7 @@ module DiscourseTagging
|
|||||||
end
|
end
|
||||||
|
|
||||||
sql << <<~SQL
|
sql << <<~SQL
|
||||||
SELECT #{distinct_clause} t.id, t.name, t.topic_count, t.pm_topic_count,
|
SELECT #{distinct_clause} t.id, t.name, t.topic_count, t.pm_topic_count, t.description,
|
||||||
tgr.tgm_id as tgm_id, tgr.tag_group_id as tag_group_id, tgr.parent_tag_id as parent_tag_id,
|
tgr.tgm_id as tgm_id, tgr.tag_group_id as tag_group_id, tgr.parent_tag_id as parent_tag_id,
|
||||||
tgr.one_per_topic as one_per_topic, t.target_tag_id
|
tgr.one_per_topic as one_per_topic, t.target_tag_id
|
||||||
FROM tags t
|
FROM tags t
|
||||||
|
@ -236,9 +236,9 @@ describe TopicViewSerializer do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'tags order' do
|
describe 'tags order' do
|
||||||
fab!(:tag1) { Fabricate(:tag, name: 'ctag', topic_count: 5) }
|
fab!(:tag1) { Fabricate(:tag, name: 'ctag', description: "c description", topic_count: 5) }
|
||||||
fab!(:tag2) { Fabricate(:tag, name: 'btag', topic_count: 9) }
|
fab!(:tag2) { Fabricate(:tag, name: 'btag', description: "b description", topic_count: 9) }
|
||||||
fab!(:tag3) { Fabricate(:tag, name: 'atag', topic_count: 3) }
|
fab!(:tag3) { Fabricate(:tag, name: 'atag', description: "a description", topic_count: 3) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
topic.tags << tag1
|
topic.tags << tag1
|
||||||
@ -249,6 +249,7 @@ describe TopicViewSerializer do
|
|||||||
it 'tags are automatically sorted by tag popularity' do
|
it 'tags are automatically sorted by tag popularity' do
|
||||||
json = serialize_topic(topic, user)
|
json = serialize_topic(topic, user)
|
||||||
expect(json[:tags]).to eq(%w(btag ctag atag))
|
expect(json[:tags]).to eq(%w(btag ctag atag))
|
||||||
|
expect(json[:tags_descriptions]).to eq({ btag: "b description", ctag: "c description", atag: "a description" })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'tags can be sorted alphabetically' do
|
it 'tags can be sorted alphabetically' do
|
||||||
|
@ -50,6 +50,7 @@ RSpec.describe WebHookTopicViewSerializer do
|
|||||||
created_by
|
created_by
|
||||||
last_poster
|
last_poster
|
||||||
tags
|
tags
|
||||||
|
tags_descriptions
|
||||||
thumbnails
|
thumbnails
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user