FEATURE: add setting to show tags by group (#138)

* FEATURE: add setting to show tags by group
This commit is contained in:
Jean 2023-06-09 11:02:17 -04:00 committed by GitHub
parent e95388ea0b
commit d4ab4080db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 435 additions and 7 deletions

View File

@ -48,6 +48,7 @@ export default Controller.extend({
categories: readOnly("model.categories"), categories: readOnly("model.categories"),
topics: alias("model.topics.topic_list.topics"), topics: alias("model.topics.topic_list.topics"),
tags: readOnly("model.tags"), tags: readOnly("model.tags"),
tagGroups: readOnly("model.tag_groups"),
topicCount: alias("model.topic_count"), topicCount: alias("model.topic_count"),
emptyResults: equal("topicCount", 0), emptyResults: equal("topicCount", 0),
@ -142,6 +143,39 @@ export default Controller.extend({
return tags; 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") @discourseComputed("tagSort")
tagSortNumericIcon(tagSort) { tagSortNumericIcon(tagSort) {
if (tagSort.type === "numeric" && tagSort.direction === "asc") { if (tagSort.type === "numeric" && tagSort.direction === "asc") {
@ -192,9 +226,17 @@ export default Controller.extend({
return !!filterTags; return !!filterTags;
}, },
@discourseComputed() @discourseComputed("siteSettings.tagging_enabled", "shouldShowTagsByGroup")
shouldShowTags() { shouldShowTags(tagging_enabled, shouldShowTagsByGroup) {
return this.siteSettings.tagging_enabled; 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() @discourseComputed()

View File

@ -139,6 +139,68 @@
</ul> </ul>
</div> </div>
{{/if}} {{/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}} {{/if}}
</div> </div>

View File

@ -4,5 +4,7 @@ en:
docs_categories: "A list of category slugs to include in docs" docs_categories: "A list of category slugs to include in docs"
docs_tags: "A list of tags 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" 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_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" docs_add_search_menu_tip: "Adds the tip \"in:docs\" to the search menu random tips"

View File

@ -6,6 +6,13 @@ plugins:
type: category_list type: category_list
default: "" default: ""
client: true client: true
show_tags_by_group:
default: false
client: true
docs_tag_groups:
type: tag_group_list
default: ""
client: true
docs_tags: docs_tags:
type: tag_list type: tag_list
default: "" default: ""

View File

@ -21,7 +21,8 @@ module Docs
opts = { no_definitions: true, limit: false } opts = { no_definitions: true, limit: false }
tq = TopicQuery.new(@user, opts) tq = TopicQuery.new(@user, opts)
results = tq.list_docs_topics 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.references(:categories)
results = results =
results.where("topics.category_id IN (?)", Query.categories).or( 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 topic_tags ttx ON ttx.topic_id = topics.id
INNER JOIN tags t2 ON t2.id = ttx.tag_id INNER JOIN tags t2 ON t2.id = ttx.tag_id
SQL 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 = categories =
results results
@ -145,7 +163,33 @@ module Docs
topic_list["load_more_url"] = nil topic_list["load_more_url"] = nil
end 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 end
def create_tags_object(tags) def create_tags_object(tags)

View File

@ -8,6 +8,14 @@ describe Docs::DocsController do
fab!(:topic2) { Fabricate(:topic, title: "I love pineapple today", category: category) } fab!(:topic2) { Fabricate(:topic, title: "I love pineapple today", category: category) }
fab!(:tag) { Fabricate(:tag, topics: [topic], name: "test") } 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 before do
SiteSetting.tagging_enabled = true SiteSetting.tagging_enabled = true
SiteSetting.docs_enabled = true SiteSetting.docs_enabled = true
@ -97,6 +105,57 @@ describe Docs::DocsController do
expect(tags.size).to eq(3) expect(tags.size).to eq(3)
expect(topics.size).to eq(1) expect(topics.size).to eq(1)
end 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 end
context "when filtering by category" do context "when filtering by category" do

View File

@ -6,6 +6,7 @@ import {
} from "discourse/tests/helpers/qunit-helpers"; } from "discourse/tests/helpers/qunit-helpers";
import { test } from "qunit"; import { test } from "qunit";
import docsFixtures from "../fixtures/docs"; import docsFixtures from "../fixtures/docs";
import docsShowTagGroupsFixtures from "../fixtures/docs-show-tag-groups";
import { click, visit } from "@ember/test-helpers"; import { click, visit } from "@ember/test-helpers";
let DOCS_URL_PATH = "docs"; 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) { acceptance("Docs - empty state", function (needs) {
needs.user(); needs.user();
needs.site({ docs_path: DOCS_URL_PATH }); needs.site({ docs_path: DOCS_URL_PATH });

View File

@ -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,
};