FEATURE: add setting to show tags by group (#138)
* FEATURE: add setting to show tags by group
This commit is contained in:
parent
e95388ea0b
commit
d4ab4080db
|
@ -48,6 +48,7 @@ export default Controller.extend({
|
|||
categories: readOnly("model.categories"),
|
||||
topics: alias("model.topics.topic_list.topics"),
|
||||
tags: readOnly("model.tags"),
|
||||
tagGroups: readOnly("model.tag_groups"),
|
||||
topicCount: alias("model.topic_count"),
|
||||
emptyResults: equal("topicCount", 0),
|
||||
|
||||
|
@ -142,6 +143,39 @@ export default Controller.extend({
|
|||
return tags;
|
||||
},
|
||||
|
||||
@discourseComputed("tagGroups", "tagSort", "tagFilter")
|
||||
sortedTagGroups(tagGroups, tagSort, filter) {
|
||||
let { type, direction } = tagSort;
|
||||
let sortedTagGroups = [...tagGroups];
|
||||
|
||||
if (type === "numeric") {
|
||||
sortedTagGroups.forEach((group) => {
|
||||
group.totalCount = group.tags.reduce(
|
||||
(acc, curr) => acc + curr.count,
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
sortedTagGroups.sort((a, b) => b.totalCount - a.totalCount);
|
||||
} else {
|
||||
sortedTagGroups.sort((a, b) =>
|
||||
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
if (direction === "desc") {
|
||||
sortedTagGroups.reverse();
|
||||
}
|
||||
|
||||
if (this.showTagFilter) {
|
||||
return sortedTagGroups.filter((tag) =>
|
||||
tag.id.toLowerCase().includes(filter.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
return sortedTagGroups;
|
||||
},
|
||||
|
||||
@discourseComputed("tagSort")
|
||||
tagSortNumericIcon(tagSort) {
|
||||
if (tagSort.type === "numeric" && tagSort.direction === "asc") {
|
||||
|
@ -192,9 +226,17 @@ export default Controller.extend({
|
|||
return !!filterTags;
|
||||
},
|
||||
|
||||
@discourseComputed()
|
||||
shouldShowTags() {
|
||||
return this.siteSettings.tagging_enabled;
|
||||
@discourseComputed("siteSettings.tagging_enabled", "shouldShowTagsByGroup")
|
||||
shouldShowTags(tagging_enabled, shouldShowTagsByGroup) {
|
||||
return tagging_enabled && !shouldShowTagsByGroup;
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"siteSettings.show_tags_by_group",
|
||||
"siteSettings.docs_tag_groups"
|
||||
)
|
||||
shouldShowTagsByGroup(show_tags_by_group, docs_tag_groups) {
|
||||
return show_tags_by_group && Boolean(docs_tag_groups);
|
||||
},
|
||||
|
||||
@discourseComputed()
|
||||
|
|
|
@ -139,6 +139,68 @@
|
|||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (and tagGroups shouldShowTagsByGroup)}}
|
||||
<div class="docs-items docs-tags">
|
||||
<section class="item-controls">
|
||||
<h3>{{i18n "docs.tags"}}</h3>
|
||||
<div class="item-controls-buttons">
|
||||
<DButton
|
||||
class={{if
|
||||
(eq tagSort.type "alpha")
|
||||
"tags-alphabet active"
|
||||
"tags-alphabet"
|
||||
}}
|
||||
@icon={{this.tagSortAlphaIcon}}
|
||||
@action={{toggleTagSort}}
|
||||
@actionParam="alpha"
|
||||
/>
|
||||
<DButton
|
||||
class={{if
|
||||
(eq tagSort.type "numeric")
|
||||
"tags-amount active"
|
||||
"tags-amount"
|
||||
}}
|
||||
@icon={{this.tagSortNumericIcon}}
|
||||
@action={{toggleTagSort}}
|
||||
@actionParam="numeric"
|
||||
/>
|
||||
<PluginOutlet @name="tags-controls-buttons-bottom" />
|
||||
</div>
|
||||
</section>
|
||||
{{#if showTagFilter}}
|
||||
<Input
|
||||
@value={{tagFilter}}
|
||||
class="filter"
|
||||
placeholder={{i18n "docs.tags_filter_placeholder"}}
|
||||
/>
|
||||
{{/if}}
|
||||
<PluginOutlet
|
||||
@name="before-docs-tag-list"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash
|
||||
tags=tags
|
||||
updateSelectedTags=updateSelectedTags
|
||||
}}
|
||||
/>
|
||||
<ul>
|
||||
{{#each sortedTagGroups as |tagGroup|}}
|
||||
<li class="docs-filter-tag-group-{{tagGroup.id}}">
|
||||
{{tagGroup.name}}
|
||||
<ul>
|
||||
{{#each tagGroup.tags as |tag|}}
|
||||
<li class="docs-filter-tag-{{id}}">
|
||||
<DocsTag
|
||||
@tag={{tag}}
|
||||
@selectTag={{action "updateSelectedTags" tag}}
|
||||
/>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -4,5 +4,7 @@ en:
|
|||
docs_categories: "A list of category slugs to include in docs"
|
||||
docs_tags: "A list of tags to include in docs"
|
||||
docs_add_solved_filter: "Adds a filter for solved topics -- requires Discourse Solved to be installed and enabled"
|
||||
show_tags_by_group: "Organize tags using Tag Groups. Create groups to categorize related tags."
|
||||
docs_tag_groups: "The Group Tags used to show tags by group."
|
||||
docs_add_to_top_menu: "Adds a link to the top menu to navigate to the Docs view"
|
||||
docs_add_search_menu_tip: "Adds the tip \"in:docs\" to the search menu random tips"
|
||||
|
|
|
@ -6,6 +6,13 @@ plugins:
|
|||
type: category_list
|
||||
default: ""
|
||||
client: true
|
||||
show_tags_by_group:
|
||||
default: false
|
||||
client: true
|
||||
docs_tag_groups:
|
||||
type: tag_group_list
|
||||
default: ""
|
||||
client: true
|
||||
docs_tags:
|
||||
type: tag_list
|
||||
default: ""
|
||||
|
|
|
@ -21,7 +21,8 @@ module Docs
|
|||
opts = { no_definitions: true, limit: false }
|
||||
tq = TopicQuery.new(@user, opts)
|
||||
results = tq.list_docs_topics
|
||||
results = results.left_outer_joins(:tags)
|
||||
results =
|
||||
results.left_outer_joins(SiteSetting.show_tags_by_group ? { tags: :tag_groups } : :tags)
|
||||
results = results.references(:categories)
|
||||
results =
|
||||
results.where("topics.category_id IN (?)", Query.categories).or(
|
||||
|
@ -99,8 +100,25 @@ module Docs
|
|||
INNER JOIN topic_tags ttx ON ttx.topic_id = topics.id
|
||||
INNER JOIN tags t2 ON t2.id = ttx.tag_id
|
||||
SQL
|
||||
tags = count_query.group("t2.name").reorder("").count
|
||||
tags = create_tags_object(tags)
|
||||
|
||||
if SiteSetting.show_tags_by_group
|
||||
enabled_tag_groups = SiteSetting.docs_tag_groups.split("|")
|
||||
subquery = TagGroup.where(name: enabled_tag_groups).select(:id)
|
||||
results = results.joins(tags: :tag_groups).where(tag_groups: { id: subquery })
|
||||
|
||||
tags =
|
||||
count_query
|
||||
.joins(tags: :tag_groups)
|
||||
.where(tag_groups: { id: subquery })
|
||||
.group("tag_groups.id", "tag_groups.name", "tags.name")
|
||||
.reorder("")
|
||||
.count
|
||||
|
||||
tags = create_group_tags_object(tags)
|
||||
else
|
||||
tags = count_query.group("t2.name").reorder("").count
|
||||
tags = create_tags_object(tags)
|
||||
end
|
||||
|
||||
categories =
|
||||
results
|
||||
|
@ -145,7 +163,33 @@ module Docs
|
|||
topic_list["load_more_url"] = nil
|
||||
end
|
||||
|
||||
{ tags: tags, categories: categories, topics: topic_list, topic_count: results_length }
|
||||
tags_key = SiteSetting.show_tags_by_group ? :tag_groups : :tags
|
||||
{
|
||||
tags_key => tags,
|
||||
:categories => categories,
|
||||
:topics => topic_list,
|
||||
:topic_count => results_length,
|
||||
}
|
||||
end
|
||||
|
||||
def create_group_tags_object(tags)
|
||||
tags_hash = ActiveSupport::OrderedHash.new
|
||||
allowed_tags = DiscourseTagging.filter_allowed_tags(Guardian.new(@user)).map(&:name)
|
||||
|
||||
tags.each do |group_tags_data, count|
|
||||
group_tag_id, group_tag_name, tag_name = group_tags_data
|
||||
active = @filters[:tags]&.include?(tag_name)
|
||||
|
||||
tags_hash[group_tag_id] ||= { id: group_tag_id, name: group_tag_name, tags: [] }
|
||||
tags_hash[group_tag_id][:tags] << { id: tag_name, count: count, active: active }
|
||||
end
|
||||
|
||||
tags_hash
|
||||
.transform_values do |group|
|
||||
group[:tags] = group[:tags].filter { |tag| allowed_tags.include?(tag[:id]) }
|
||||
group
|
||||
end
|
||||
.values
|
||||
end
|
||||
|
||||
def create_tags_object(tags)
|
||||
|
|
|
@ -8,6 +8,14 @@ describe Docs::DocsController do
|
|||
fab!(:topic2) { Fabricate(:topic, title: "I love pineapple today", category: category) }
|
||||
fab!(:tag) { Fabricate(:tag, topics: [topic], name: "test") }
|
||||
|
||||
def get_tag_attributes(tag)
|
||||
{ "id" => tag.name, "count" => 1 }
|
||||
end
|
||||
|
||||
def get_tags_from_response(response_tags)
|
||||
response_tags.map { |tag| tag.except("active") }
|
||||
end
|
||||
|
||||
before do
|
||||
SiteSetting.tagging_enabled = true
|
||||
SiteSetting.docs_enabled = true
|
||||
|
@ -97,6 +105,57 @@ describe Docs::DocsController do
|
|||
expect(tags.size).to eq(3)
|
||||
expect(topics.size).to eq(1)
|
||||
end
|
||||
|
||||
context "when show_tags_by_group is enabled" do
|
||||
fab!(:tag4) { Fabricate(:tag, topics: [topic], name: "test4") }
|
||||
|
||||
fab!(:tag_group_1) { Fabricate(:tag_group, name: "test-test2", tag_names: %w[test test2]) }
|
||||
fab!(:tag_group_2) do
|
||||
Fabricate(:tag_group, name: "test3-test4", tag_names: %w[test3 test4])
|
||||
end
|
||||
fab!(:non_docs_tag_group) do
|
||||
Fabricate(:tag_group, name: "non-docs-group", tag_names: %w[test3])
|
||||
end
|
||||
fab!(:empty_tag_group) { Fabricate(:tag_group, name: "empty-group") }
|
||||
|
||||
let(:docs_json_path) { "/#{GlobalSetting.docs_path}.json" }
|
||||
let(:parsed_body) { response.parsed_body }
|
||||
let(:tag_groups) { parsed_body["tag_groups"] }
|
||||
let(:tag_ids) { tag_groups.map { |group| group["id"] } }
|
||||
|
||||
before do
|
||||
SiteSetting.show_tags_by_group = true
|
||||
SiteSetting.docs_tag_groups = "test-test2|test3-test4"
|
||||
get docs_json_path
|
||||
end
|
||||
|
||||
it "should add groups to the tags attribute" do
|
||||
get docs_json_path
|
||||
expect(get_tags_from_response(tag_groups[0]["tags"])).to contain_exactly(
|
||||
*[tag, tag2].map { |t| get_tag_attributes(t) },
|
||||
)
|
||||
expect(get_tags_from_response(tag_groups[1]["tags"])).to contain_exactly(
|
||||
*[tag3, tag4].map { |t| get_tag_attributes(t) },
|
||||
)
|
||||
end
|
||||
|
||||
it "only displays tag groups that are enabled" do
|
||||
SiteSetting.docs_tag_groups = "test3-test4"
|
||||
get docs_json_path
|
||||
expect(tag_groups.size).to eq(1)
|
||||
expect(get_tags_from_response(tag_groups[0]["tags"])).to contain_exactly(
|
||||
*[tag3, tag4].map { |t| get_tag_attributes(t) },
|
||||
)
|
||||
end
|
||||
|
||||
it "does not return tag groups without tags" do
|
||||
expect(tag_ids).not_to include(empty_tag_group.id)
|
||||
end
|
||||
|
||||
it "does not return non-docs tag groups" do
|
||||
expect(tag_ids).not_to include(non_docs_tag_group.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when filtering by category" do
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { test } from "qunit";
|
||||
import docsFixtures from "../fixtures/docs";
|
||||
import docsShowTagGroupsFixtures from "../fixtures/docs-show-tag-groups";
|
||||
import { click, visit } from "@ember/test-helpers";
|
||||
|
||||
let DOCS_URL_PATH = "docs";
|
||||
|
@ -73,6 +74,82 @@ acceptance("Docs", function (needs) {
|
|||
});
|
||||
});
|
||||
|
||||
acceptance("Docs - with tag groups enabled", function (needs) {
|
||||
needs.user();
|
||||
needs.site({ docs_path: DOCS_URL_PATH });
|
||||
needs.settings({
|
||||
docs_enabled: true,
|
||||
navigation_menu: "legacy",
|
||||
});
|
||||
|
||||
function getRootElementText(selector) {
|
||||
return Array.from(query(selector).childNodes)
|
||||
.filter((node) => node.nodeType === Node.TEXT_NODE)
|
||||
.map((node) => node.textContent.trim())
|
||||
.join("");
|
||||
}
|
||||
|
||||
function assertTagGroup(assert, tagGroup) {
|
||||
let groupTagSelector = `.docs-filter-tag-group-${tagGroup.id}`;
|
||||
assert.equal(
|
||||
getRootElementText(groupTagSelector),
|
||||
tagGroup.expectedTagGroupName
|
||||
);
|
||||
assert.equal(
|
||||
query(`${groupTagSelector} .docs-tag .docs-item-id`).innerText,
|
||||
tagGroup.expectedTagName
|
||||
);
|
||||
assert.equal(
|
||||
query(`${groupTagSelector} .docs-tag .docs-item-count`).innerText,
|
||||
tagGroup.expectedCount
|
||||
);
|
||||
}
|
||||
|
||||
needs.pretender((server, helper) => {
|
||||
server.get("/" + DOCS_URL_PATH + ".json", () => {
|
||||
return helper.response(docsShowTagGroupsFixtures);
|
||||
});
|
||||
});
|
||||
|
||||
test("Show tag groups", async function (assert) {
|
||||
this.siteSettings.tagging_enabled = true;
|
||||
this.siteSettings.show_tags_by_group = true;
|
||||
this.siteSettings.docs_tag_groups =
|
||||
"my-tag-group-1|my-tag-group-2|my-tag-group-3";
|
||||
|
||||
await visit("/");
|
||||
await click("#toggle-hamburger-menu");
|
||||
await click(".docs-link");
|
||||
|
||||
assert.equal(query(".docs-category .docs-item-id").innerText, "bug");
|
||||
assert.equal(query(".docs-category .docs-item-count").innerText, "119");
|
||||
|
||||
const expectedTagGroups = [
|
||||
{
|
||||
id: "1",
|
||||
expectedTagGroupName: "my-tag-group-1",
|
||||
expectedTagName: "something 1",
|
||||
expectedCount: "50",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
expectedTagGroupName: "my-tag-group-2",
|
||||
expectedTagName: "something 2",
|
||||
expectedCount: "10",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
expectedTagGroupName: "my-tag-group-3",
|
||||
expectedTagName: "something 3",
|
||||
expectedCount: "1",
|
||||
},
|
||||
];
|
||||
|
||||
for (let tagGroup of expectedTagGroups) {
|
||||
assertTagGroup(assert, tagGroup);
|
||||
}
|
||||
});
|
||||
});
|
||||
acceptance("Docs - empty state", function (needs) {
|
||||
needs.user();
|
||||
needs.site({ docs_path: DOCS_URL_PATH });
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
export default {
|
||||
tag_groups: [
|
||||
{
|
||||
id: 1,
|
||||
name: "my-tag-group-1",
|
||||
tags: [
|
||||
{
|
||||
id: "something 1",
|
||||
count: 50,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
id: "something 2",
|
||||
count: 10,
|
||||
active: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "my-tag-group-2",
|
||||
tags: [
|
||||
{
|
||||
id: "something 2",
|
||||
count: 10,
|
||||
active: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "my-tag-group-3",
|
||||
tags: [
|
||||
{
|
||||
id: "something 3",
|
||||
count: 1,
|
||||
active: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "my-tag-group-4",
|
||||
tags: [
|
||||
{
|
||||
id: "something 4",
|
||||
count: 1,
|
||||
active: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
categories: [
|
||||
{
|
||||
id: 1,
|
||||
count: 119,
|
||||
active: false,
|
||||
},
|
||||
],
|
||||
topics: {
|
||||
users: [
|
||||
{
|
||||
id: 2,
|
||||
username: "cvx",
|
||||
name: "Jarek",
|
||||
avatar_template: "/letter_avatar/cvx/{size}/2.png",
|
||||
},
|
||||
],
|
||||
primary_groups: [],
|
||||
topic_list: {
|
||||
can_create_topic: true,
|
||||
draft: null,
|
||||
draft_key: "new_topic",
|
||||
draft_sequence: 94,
|
||||
per_page: 30,
|
||||
top_tags: ["something"],
|
||||
topics: [
|
||||
{
|
||||
id: 54881,
|
||||
title: "Importing from Software X",
|
||||
fancy_title: "Importing from Software X",
|
||||
slug: "importing-from-software-x",
|
||||
posts_count: 112,
|
||||
reply_count: 72,
|
||||
highest_post_number: 122,
|
||||
image_url: null,
|
||||
created_at: "2016-12-28T14:59:29.396Z",
|
||||
last_posted_at: "2020-11-14T16:21:35.720Z",
|
||||
bumped: true,
|
||||
bumped_at: "2020-11-14T16:21:35.720Z",
|
||||
archetype: "regular",
|
||||
unseen: false,
|
||||
pinned: false,
|
||||
unpinned: null,
|
||||
visible: true,
|
||||
closed: false,
|
||||
archived: false,
|
||||
bookmarked: null,
|
||||
liked: null,
|
||||
tags: ["something"],
|
||||
views: 15222,
|
||||
like_count: 167,
|
||||
has_summary: true,
|
||||
last_poster_username: "cvx",
|
||||
category_id: 1,
|
||||
pinned_globally: false,
|
||||
featured_link: null,
|
||||
has_accepted_answer: false,
|
||||
posters: [
|
||||
{
|
||||
extras: null,
|
||||
description: "Original Poster",
|
||||
user_id: 2,
|
||||
primary_group_id: null,
|
||||
},
|
||||
{
|
||||
extras: null,
|
||||
description: "Frequent Poster",
|
||||
user_id: 2,
|
||||
primary_group_id: null,
|
||||
},
|
||||
{
|
||||
extras: "latest",
|
||||
description: "Most Recent Poster",
|
||||
user_id: 2,
|
||||
primary_group_id: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
load_more_url: "/docs.json?page=1",
|
||||
},
|
||||
search_count: null,
|
||||
};
|
Loading…
Reference in New Issue