diff --git a/app/assets/javascripts/discourse/app/models/category-list.js b/app/assets/javascripts/discourse/app/models/category-list.js
index b7a44e042c5..cd31670b34b 100644
--- a/app/assets/javascripts/discourse/app/models/category-list.js
+++ b/app/assets/javascripts/discourse/app/models/category-list.js
@@ -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: `${number(stat)}`,
- unit: `${unit}`,
- });
-
- c.statTitle = I18n.t(
- `categories.topic_stat_sentence_${statPeriod}`,
- {
- count: stat,
- }
- );
-
- c.pickAll = false;
- break;
- }
- default:
- c.stat = `${number(c.topics_all_time)}`;
- 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: `${number(c.topics_all_time)}`,
- });
- }
-
- 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: `${number(stat)}`,
+ unit: `${unit}`,
+ });
+
+ c.statTitle = I18n.t(`categories.topic_stat_sentence_${statPeriod}`, {
+ count: stat,
+ });
+
+ c.pickAll = false;
+ break;
+ }
+ default:
+ c.stat = `${number(c.topics_all_time)}`;
+ 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: `${number(c.topics_all_time)}`,
+ });
+ }
+
+ const record = Site.current().updateCategory(c);
+ record.setupGroupsAndPermissions();
+ return record;
+ },
+
listForParent(store, category) {
return ajax(
`/categories.json?parent_category_id=${category.get("id")}`
diff --git a/app/assets/javascripts/discourse/app/templates/components/subcategories-with-featured-topics.hbs b/app/assets/javascripts/discourse/app/templates/components/subcategories-with-featured-topics.hbs
new file mode 100644
index 00000000000..974b0ce7fe9
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/templates/components/subcategories-with-featured-topics.hbs
@@ -0,0 +1,22 @@
+{{#each categories as |category|}}
+
+
+
+
+ {{category-title-link category=category}}
+ {{html-safe category.stat}}
+ |
+ {{i18n "categories.topics"}} |
+ {{i18n "categories.latest"}} |
+
+
+
+ {{#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}}
+
+
+{{/each}}
diff --git a/app/assets/javascripts/discourse/tests/acceptance/categories-test.js b/app/assets/javascripts/discourse/tests/acceptance/categories-test.js
new file mode 100644
index 00000000000..c1b4ed3bd38
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/acceptance/categories-test.js
@@ -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"
+ );
+ });
+ }
+);
diff --git a/app/assets/stylesheets/common/base/category-list.scss b/app/assets/stylesheets/common/base/category-list.scss
index d45b6b3e8d9..37eba8e9a7a 100644
--- a/app/assets/stylesheets/common/base/category-list.scss
+++ b/app/assets/stylesheets/common/base/category-list.scss
@@ -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;
diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb
index eddf3489478..746bc2d3aec 100644
--- a/app/controllers/categories_controller.rb
+++ b/app/controllers/categories_controller.rb
@@ -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
diff --git a/app/models/category_list.rb b/app/models/category_list.rb
index d6a1b67ed42..90ca8e2a7f3 100644
--- a/app/models/category_list.rb
+++ b/app/models/category_list.rb
@@ -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
diff --git a/app/models/category_page_style.rb b/app/models/category_page_style.rb
index d1598380909..4d0eaf75170 100644
--- a/app/models/category_page_style.rb
+++ b/app/models/category_page_style.rb
@@ -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
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index e6683a9fc16..d45d731e3a2 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -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"
diff --git a/spec/requests/categories_controller_spec.rb b/spec/requests/categories_controller_spec.rb
index 1a908dd19a5..7a539a4af01 100644
--- a/spec/requests/categories_controller_spec.rb
+++ b/spec/requests/categories_controller_spec.rb
@@ -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"