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) => {
|
result.category_list.categories.forEach((c) =>
|
||||||
if (c.parent_category_id) {
|
categories.pushObject(this._buildCategoryResult(c, list, statPeriod))
|
||||||
c.parentCategory = list.findBy("id", c.parent_category_id);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
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) {
|
listForParent(store, category) {
|
||||||
return ajax(
|
return ajax(
|
||||||
`/categories.json?parent_category_id=${category.get("id")}`
|
`/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,
|
||||||
.category-boxes-with-topics {
|
.category-boxes-with-topics {
|
||||||
display: flex;
|
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)
|
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 = {
|
category_options = {
|
||||||
is_homepage: current_homepage == "categories",
|
is_homepage: current_homepage == "categories",
|
||||||
parent_category_id: params[:parent_category_id],
|
parent_category_id: params[:parent_category_id],
|
||||||
include_topics: include_topics(parent_category),
|
include_topics: include_topics(parent_category),
|
||||||
include_subcategories: params[:include_subcategories] == "true"
|
include_subcategories: include_subcategories
|
||||||
}
|
}
|
||||||
|
|
||||||
@category_list = CategoryList.new(guardian, category_options)
|
@category_list = CategoryList.new(guardian, category_options)
|
||||||
|
@ -377,6 +380,7 @@ class CategoriesController < ApplicationController
|
||||||
params[:include_topics] ||
|
params[:include_topics] ||
|
||||||
(parent_category && parent_category.subcategory_list_includes_topics?) ||
|
(parent_category && parent_category.subcategory_list_includes_topics?) ||
|
||||||
style == "categories_with_featured_topics" ||
|
style == "categories_with_featured_topics" ||
|
||||||
|
style == "subcategories_with_featured_topics" ||
|
||||||
style == "categories_boxes_with_topics" ||
|
style == "categories_boxes_with_topics" ||
|
||||||
style == "categories_with_top_topics"
|
style == "categories_with_top_topics"
|
||||||
end
|
end
|
||||||
|
|
|
@ -113,13 +113,6 @@ class CategoryList
|
||||||
notification_levels = CategoryUser.notification_levels_for(@guardian.user)
|
notification_levels = CategoryUser.notification_levels_for(@guardian.user)
|
||||||
default_notification_level = CategoryUser.default_notification_level
|
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?
|
if @options[:parent_category_id].blank?
|
||||||
subcategory_ids = {}
|
subcategory_ids = {}
|
||||||
subcategory_list = {}
|
subcategory_list = {}
|
||||||
|
@ -144,8 +137,15 @@ class CategoryList
|
||||||
@categories.delete_if { |c| to_delete.include?(c) }
|
@categories.delete_if { |c| to_delete.include?(c) }
|
||||||
end
|
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
|
if @topics_by_category_id
|
||||||
@categories.each do |c|
|
categories_with_descendants.each do |c|
|
||||||
topics_in_cat = @topics_by_category_id[c.id]
|
topics_in_cat = @topics_by_category_id[c.id]
|
||||||
if topics_in_cat.present?
|
if topics_in_cat.present?
|
||||||
c.displayable_topics = []
|
c.displayable_topics = []
|
||||||
|
@ -178,7 +178,7 @@ class CategoryList
|
||||||
# Put unpinned topics at the end of the list
|
# Put unpinned topics at the end of the list
|
||||||
def sort_unpinned
|
def sort_unpinned
|
||||||
if @guardian.current_user && @all_topics.present?
|
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
|
next if c.displayable_topics.blank? || c.displayable_topics.size <= c.num_featured_topics
|
||||||
unpinned = []
|
unpinned = []
|
||||||
c.displayable_topics.each do |t|
|
c.displayable_topics.each do |t|
|
||||||
|
@ -198,10 +198,22 @@ class CategoryList
|
||||||
end
|
end
|
||||||
|
|
||||||
def trim_results
|
def trim_results
|
||||||
@categories.each do |c|
|
categories_with_descendants.each do |c|
|
||||||
next if c.displayable_topics.blank?
|
next if c.displayable_topics.blank?
|
||||||
c.displayable_topics = c.displayable_topics[0, c.num_featured_topics]
|
c.displayable_topics = c.displayable_topics[0, c.num_featured_topics]
|
||||||
end
|
end
|
||||||
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
|
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_and_top_topics', value: 'categories_and_top_topics' },
|
||||||
{ name: 'category_page_style.categories_boxes', value: 'categories_boxes' },
|
{ 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.categories_boxes_with_topics', value: 'categories_boxes_with_topics' },
|
||||||
|
{ name: 'category_page_style.subcategories_with_featured_topics', value: 'subcategories_with_featured_topics' },
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2016,6 +2016,7 @@ en:
|
||||||
categories_and_top_topics: "Categories and Top Topics"
|
categories_and_top_topics: "Categories and Top Topics"
|
||||||
categories_boxes: "Boxes with Subcategories"
|
categories_boxes: "Boxes with Subcategories"
|
||||||
categories_boxes_with_topics: "Boxes with Featured Topics"
|
categories_boxes_with_topics: "Boxes with Featured Topics"
|
||||||
|
subcategories_with_featured_topics: "Subcategories with Featured Topics"
|
||||||
|
|
||||||
shortcut_modifier_key:
|
shortcut_modifier_key:
|
||||||
shift: "Shift"
|
shift: "Shift"
|
||||||
|
|
|
@ -115,6 +115,50 @@ describe CategoriesController do
|
||||||
expect(subcategories_for_category).to eq(nil)
|
expect(subcategories_for_category).to eq(nil)
|
||||||
end
|
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
|
it 'does not show uncategorized unless allow_uncategorized_topics' do
|
||||||
SiteSetting.desktop_category_page_style = "categories_boxes_with_topics"
|
SiteSetting.desktop_category_page_style = "categories_boxes_with_topics"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue