FEATURE: Introduce 'Subcategories with featured topics' view (#16083)

This categories view is designed for sites which make heavy use of subcategories, and use top-level categories mainly for grouping
This commit is contained in:
David Taylor 2022-03-04 21:11:59 +00:00 committed by GitHub
parent 07e80b52ef
commit eb2e3b510d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 252 additions and 68 deletions

View File

@ -32,67 +32,73 @@ CategoryList.reopenClass({
}
});
result.category_list.categories.forEach((c) => {
if (c.parent_category_id) {
c.parentCategory = list.findBy("id", c.parent_category_id);
}
result.category_list.categories.forEach((c) =>
categories.pushObject(this._buildCategoryResult(c, list, statPeriod))
);
if (c.subcategory_ids) {
c.subcategories = c.subcategory_ids.map((scid) =>
list.findBy("id", parseInt(scid, 10))
);
}
if (c.topics) {
c.topics = c.topics.map((t) => Topic.create(t));
}
switch (statPeriod) {
case "week":
case "month":
const stat = c[`topics_${statPeriod}`];
if (stat > 0) {
const unit = I18n.t(`categories.topic_stat_unit.${statPeriod}`);
c.stat = I18n.t("categories.topic_stat", {
count: stat, // only used to correctly pluralize the string
number: `<span class="value">${number(stat)}</span>`,
unit: `<span class="unit">${unit}</span>`,
});
c.statTitle = I18n.t(
`categories.topic_stat_sentence_${statPeriod}`,
{
count: stat,
}
);
c.pickAll = false;
break;
}
default:
c.stat = `<span class="value">${number(c.topics_all_time)}</span>`;
c.statTitle = I18n.t("categories.topic_sentence", {
count: c.topics_all_time,
});
c.pickAll = true;
break;
}
if (Site.currentProp("mobileView")) {
c.statTotal = I18n.t("categories.topic_stat_all_time", {
count: c.topics_all_time,
number: `<span class="value">${number(c.topics_all_time)}</span>`,
});
}
const record = Site.current().updateCategory(c);
record.setupGroupsAndPermissions();
categories.pushObject(record);
});
return categories;
},
_buildCategoryResult(c, list, statPeriod) {
if (c.parent_category_id) {
c.parentCategory = list.findBy("id", c.parent_category_id);
}
if (c.subcategory_list) {
c.subcategories = c.subcategory_list.map((subCategory) =>
this._buildCategoryResult(subCategory, list, statPeriod)
);
} else if (c.subcategory_ids) {
c.subcategories = c.subcategory_ids.map((scid) =>
list.findBy("id", parseInt(scid, 10))
);
}
if (c.topics) {
c.topics = c.topics.map((t) => Topic.create(t));
}
switch (statPeriod) {
case "week":
case "month":
const stat = c[`topics_${statPeriod}`];
if (stat > 0) {
const unit = I18n.t(`categories.topic_stat_unit.${statPeriod}`);
c.stat = I18n.t("categories.topic_stat", {
count: stat, // only used to correctly pluralize the string
number: `<span class="value">${number(stat)}</span>`,
unit: `<span class="unit">${unit}</span>`,
});
c.statTitle = I18n.t(`categories.topic_stat_sentence_${statPeriod}`, {
count: stat,
});
c.pickAll = false;
break;
}
default:
c.stat = `<span class="value">${number(c.topics_all_time)}</span>`;
c.statTitle = I18n.t("categories.topic_sentence", {
count: c.topics_all_time,
});
c.pickAll = true;
break;
}
if (Site.currentProp("mobileView")) {
c.statTotal = I18n.t("categories.topic_stat_all_time", {
count: c.topics_all_time,
number: `<span class="value">${number(c.topics_all_time)}</span>`,
});
}
const record = Site.current().updateCategory(c);
record.setupGroupsAndPermissions();
return record;
},
listForParent(store, category) {
return ajax(
`/categories.json?parent_category_id=${category.get("id")}`

View File

@ -0,0 +1,22 @@
{{#each categories as |category|}}
<table class="category-list subcategory-list with-topics">
<thead>
<tr>
<th class="category">
{{category-title-link category=category}}
<span class="stat" title={{category.statTitle}}>{{html-safe category.stat}}</span>
</th>
<th class="topics">{{i18n "categories.topics"}}</th>
<th class="latest">{{i18n "categories.latest"}}</th>
</tr>
</thead>
<tbody aria-labelledby="categories-only-category">
{{#each category.subcategories as |subCategory|}}
{{parent-category-row category=subCategory showTopics=true}}
{{else}}
{{!-- No subcategories... so just show the parent to avoid confusion --}}
{{parent-category-row category=category showTopics=true}}
{{/each}}
</tbody>
</table>
{{/each}}

View File

@ -0,0 +1,76 @@
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
import { visit } from "@ember/test-helpers";
import { test } from "qunit";
acceptance("Categories - 'categories_only'", function (needs) {
needs.settings({
desktop_category_page_style: "categories_only",
});
test("basic functionality", async function (assert) {
await visit("/categories");
assert.ok(
exists("table.category-list tr[data-category-id=1]"),
"shows the topic list"
);
});
});
acceptance("Categories - 'categories_and_latest_topics'", function (needs) {
needs.settings({
desktop_category_page_style: "categories_and_latest_topics",
});
test("basic functionality", async function (assert) {
await visit("/categories");
assert.ok(
exists("table.category-list tr[data-category-id=1]"),
"shows a category"
);
assert.ok(
exists("div.latest-topic-list div[data-topic-id=8]"),
"shows the topic list"
);
});
});
acceptance("Categories - 'categories_with_featured_topics'", function (needs) {
needs.settings({
desktop_category_page_style: "categories_with_featured_topics",
});
test("basic functionality", async function (assert) {
await visit("/categories");
assert.ok(
exists("table.category-list.with-topics tr[data-category-id=1]"),
"shows a category"
);
assert.ok(
exists("table.category-list.with-topics div[data-topic-id=11994]"),
"shows a featured topic"
);
});
});
acceptance(
"Categories - 'subcategories_with_featured_topics'",
function (needs) {
needs.settings({
desktop_category_page_style: "subcategories_with_featured_topics",
});
test("basic functionality", async function (assert) {
await visit("/categories");
assert.ok(
exists("table.subcategory-list.with-topics thead h3 .category-name"),
"shows heading for top-level category"
);
assert.ok(
exists(
"table.subcategory-list.with-topics tr[data-category-id=26] h3 .category-name"
),
"shows table row for subcategories"
);
assert.ok(
exists("table.category-list.with-topics div[data-topic-id=11994]"),
"shows a featured topic"
);
});
}
);

View File

@ -23,6 +23,24 @@
}
}
.navigation-categories .category-list.subcategory-list {
margin-bottom: 1em;
}
.subcategory-list {
th.category {
h3 {
display: inline;
}
.category-text-title {
display: inline-flex;
}
.stat {
margin-left: 0.5em;
}
}
}
.category-boxes,
.category-boxes-with-topics {
display: flex;

View File

@ -23,11 +23,14 @@ class CategoriesController < ApplicationController
parent_category = Category.find_by_slug(params[:parent_category_id]) || Category.find_by(id: params[:parent_category_id].to_i)
include_subcategories = SiteSetting.desktop_category_page_style == "subcategories_with_featured_topics" ||
params[:include_subcategories] == "true"
category_options = {
is_homepage: current_homepage == "categories",
parent_category_id: params[:parent_category_id],
include_topics: include_topics(parent_category),
include_subcategories: params[:include_subcategories] == "true"
include_subcategories: include_subcategories
}
@category_list = CategoryList.new(guardian, category_options)
@ -377,6 +380,7 @@ class CategoriesController < ApplicationController
params[:include_topics] ||
(parent_category && parent_category.subcategory_list_includes_topics?) ||
style == "categories_with_featured_topics" ||
style == "subcategories_with_featured_topics" ||
style == "categories_boxes_with_topics" ||
style == "categories_with_top_topics"
end

View File

@ -113,13 +113,6 @@ class CategoryList
notification_levels = CategoryUser.notification_levels_for(@guardian.user)
default_notification_level = CategoryUser.default_notification_level
allowed_topic_create = Set.new(Category.topic_create_allowed(@guardian).pluck(:id))
@categories.each do |category|
category.notification_level = notification_levels[category.id] || default_notification_level
category.permission = CategoryGroup.permission_types[:full] if allowed_topic_create.include?(category.id)
category.has_children = category.subcategories.present?
end
if @options[:parent_category_id].blank?
subcategory_ids = {}
subcategory_list = {}
@ -144,8 +137,15 @@ class CategoryList
@categories.delete_if { |c| to_delete.include?(c) }
end
allowed_topic_create = Set.new(Category.topic_create_allowed(@guardian).pluck(:id))
categories_with_descendants.each do |category|
category.notification_level = notification_levels[category.id] || default_notification_level
category.permission = CategoryGroup.permission_types[:full] if allowed_topic_create.include?(category.id)
category.has_children = category.subcategories.present?
end
if @topics_by_category_id
@categories.each do |c|
categories_with_descendants.each do |c|
topics_in_cat = @topics_by_category_id[c.id]
if topics_in_cat.present?
c.displayable_topics = []
@ -178,7 +178,7 @@ class CategoryList
# Put unpinned topics at the end of the list
def sort_unpinned
if @guardian.current_user && @all_topics.present?
@categories.each do |c|
categories_with_descendants.each do |c|
next if c.displayable_topics.blank? || c.displayable_topics.size <= c.num_featured_topics
unpinned = []
c.displayable_topics.each do |t|
@ -198,10 +198,22 @@ class CategoryList
end
def trim_results
@categories.each do |c|
categories_with_descendants.each do |c|
next if c.displayable_topics.blank?
c.displayable_topics = c.displayable_topics[0, c.num_featured_topics]
end
end
def categories_with_descendants(categories = @categories)
return @categories_with_children if @categories_with_children && (categories == @categories)
return nil if categories.nil?
result = categories.flat_map do |c|
[c, *categories_with_descendants(c.subcategory_list)]
end
@categories_with_children = result if categories == @categories
result
end
end

View File

@ -16,6 +16,7 @@ class CategoryPageStyle < EnumSiteSetting
{ name: 'category_page_style.categories_and_top_topics', value: 'categories_and_top_topics' },
{ name: 'category_page_style.categories_boxes', value: 'categories_boxes' },
{ name: 'category_page_style.categories_boxes_with_topics', value: 'categories_boxes_with_topics' },
{ name: 'category_page_style.subcategories_with_featured_topics', value: 'subcategories_with_featured_topics' },
]
end

View File

@ -2016,6 +2016,7 @@ en:
categories_and_top_topics: "Categories and Top Topics"
categories_boxes: "Boxes with Subcategories"
categories_boxes_with_topics: "Boxes with Featured Topics"
subcategories_with_featured_topics: "Subcategories with Featured Topics"
shortcut_modifier_key:
shift: "Shift"

View File

@ -115,6 +115,50 @@ describe CategoriesController do
expect(subcategories_for_category).to eq(nil)
end
it 'includes topics for categories, subcategories and subsubcategories when requested' do
SiteSetting.max_category_nesting = 3
subcategory = Fabricate(:category, user: admin, parent_category: category)
subsubcategory = Fabricate(:category, user: admin, parent_category: subcategory)
topic1 = Fabricate(:topic, category: category)
topic2 = Fabricate(:topic, category: subcategory)
topic3 = Fabricate(:topic, category: subsubcategory)
CategoryFeaturedTopic.feature_topics
get "/categories.json?include_subcategories=true&include_topics=true"
expect(response.status).to eq(200)
category_list = response.parsed_body["category_list"]
category_response = category_list["categories"].find { |c| c["id"] == category.id }
expect(category_response["topics"].map { |c| c['id'] }).to contain_exactly(topic1.id)
subcategory_response = category_response["subcategory_list"][0]
expect(subcategory_response["topics"].map { |c| c['id'] }).to contain_exactly(topic2.id)
subsubcategory_response = subcategory_response["subcategory_list"][0]
expect(subsubcategory_response["topics"].map { |c| c['id'] }).to contain_exactly(topic3.id)
end
it 'includes subcategories and topics by default when view is subcategories_with_featured_topics' do
SiteSetting.max_category_nesting = 3
subcategory = Fabricate(:category, user: admin, parent_category: category)
topic1 = Fabricate(:topic, category: category)
CategoryFeaturedTopic.feature_topics
SiteSetting.desktop_category_page_style = "subcategories_with_featured_topics"
get "/categories.json"
expect(response.status).to eq(200)
category_list = response.parsed_body["category_list"]
category_response = category_list["categories"].find { |c| c["id"] == category.id }
expect(category_response["topics"].map { |c| c['id'] }).to contain_exactly(topic1.id)
expect(category_response["subcategory_list"][0]["id"]).to eq(subcategory.id)
end
it 'does not show uncategorized unless allow_uncategorized_topics' do
SiteSetting.desktop_category_page_style = "categories_boxes_with_topics"