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:
parent
07e80b52ef
commit
eb2e3b510d
|
@ -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")}`
|
||||
|
|
|
@ -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}}
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
Loading…
Reference in New Issue