FEATURE: Filter with CategoryDrop on category page (#26689)

Using the CategoryDrop on the categories page redirected the user to the
"latest topics" page with topics only from that category. With these
changes, selecting a category will take the user to a "subcategories
page" where only the subcategories of the selected property will be
displayed.
This commit is contained in:
Bianca Nenciu 2024-05-16 10:45:13 +03:00 committed by GitHub
parent d964709644
commit 77b032c2b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 252 additions and 65 deletions

View File

@ -24,11 +24,13 @@
@tagId={{this.tag.id}}
@editingCategory={{this.editingCategory}}
@editingCategoryTab={{this.editingCategoryTab}}
@filterType={{this.filterType}}
@options={{hash
parentCategory=breadcrumb.parentCategory
subCategory=breadcrumb.isSubcategory
noSubcategories=breadcrumb.noSubcategories
autoFilterable=true
disableIfHasNoChildren=(eq this.filterType "categories")
}}
/>
</li>

View File

@ -4,6 +4,7 @@
@noSubcategories={{this.noSubcategories}}
@tag={{this.tag}}
@additionalTags={{this.additionalTags}}
@filterType={{this.filterType}}
/>
{{#unless this.additionalTags}}

View File

@ -4,6 +4,7 @@ import { number } from "discourse/lib/formatter";
import PreloadStore from "discourse/lib/preload-store";
import Site from "discourse/models/site";
import Topic from "discourse/models/topic";
import deprecated from "discourse-common/lib/deprecated";
import { bind } from "discourse-common/utils/decorators";
import I18n from "discourse-i18n";
@ -29,8 +30,8 @@ export default class CategoryList extends ArrayProxy {
result.category_list.categories.forEach((c) => {
c = this._buildCategoryResult(c, statPeriod);
if (
!c.parent_category_id ||
c.parent_category_id === parentCategory?.id
(parentCategory && c.parent_category_id === parentCategory.id) ||
(!parentCategory && !c.parent_category_id)
) {
categories.pushObject(c);
}
@ -79,28 +80,30 @@ export default class CategoryList extends ArrayProxy {
}
static listForParent(store, category) {
return ajax(
`/categories.json?parent_category_id=${category.get("id")}`
).then((result) =>
CategoryList.create({
store,
categories: this.categoriesFrom(store, result, category),
parentCategory: category,
})
deprecated(
"The listForParent method of CategoryList is deprecated. Use list instead",
{ id: "discourse.category-list.listForParent" }
);
return CategoryList.list(store, category);
}
static list(store) {
return PreloadStore.getAndRemove("categories_list", () =>
ajax("/categories.json")
).then((result) =>
CategoryList.create({
static list(store, parentCategory = null) {
return PreloadStore.getAndRemove("categories_list", () => {
const data = {};
if (parentCategory) {
data.parent_category_id = parentCategory?.id;
}
return ajax("/categories.json", { data });
}).then((result) => {
return CategoryList.create({
store,
categories: this.categoriesFrom(store, result),
categories: this.categoriesFrom(store, result, parentCategory),
parentCategory,
can_create_category: result.category_list.can_create_category,
can_create_topic: result.category_list.can_create_topic,
})
);
});
});
}
init() {
@ -119,6 +122,9 @@ export default class CategoryList extends ArrayProxy {
this.set("isLoading", true);
const data = { page: this.page + 1 };
if (this.parentCategory) {
data.parent_category_id = this.parentCategory.id;
}
const result = await ajax("/categories.json", { data });
this.set("page", data.page);
@ -127,7 +133,10 @@ export default class CategoryList extends ArrayProxy {
}
this.set("isLoading", false);
const newCategoryList = CategoryList.categoriesFrom(this.store, result);
newCategoryList.forEach((c) => this.categories.pushObject(c));
CategoryList.categoriesFrom(
this.store,
result,
this.parentCategory
).forEach((c) => this.categories.pushObject(c));
}
}

View File

@ -62,6 +62,9 @@ export default function () {
// default filter for a category
this.route("categoryNone", { path: "/c/*category_slug_path_with_id/none" });
this.route("categoryAll", { path: "/c/*category_slug_path_with_id/all" });
this.route("subcategories", {
path: "/c/*category_slug_path_with_id/subcategories",
});
this.route("category", { path: "/c/*category_slug_path_with_id" });
this.route("custom");

View File

@ -84,7 +84,7 @@ class AbstractCategoryRoute extends DiscourseRoute {
async _createSubcategoryList(category) {
if (category.isParent && category.show_subcategory_list) {
return CategoryList.listForParent(this.store, category);
return CategoryList.list(this.store, category);
}
}

View File

@ -4,6 +4,7 @@ import { hash } from "rsvp";
import { ajax } from "discourse/lib/ajax";
import PreloadStore from "discourse/lib/preload-store";
import { defaultHomepage } from "discourse/lib/utilities";
import Category from "discourse/models/category";
import CategoryList from "discourse/models/category-list";
import TopicList from "discourse/models/topic-list";
import DiscourseRoute from "discourse/routes/discourse";
@ -17,28 +18,40 @@ export default class DiscoveryCategoriesRoute extends DiscourseRoute {
templateName = "discovery/categories";
controllerName = "discovery/categories";
findCategories() {
let style =
async findCategories(parentCategory) {
let model;
const style =
this.site.desktopView && this.siteSettings.desktop_category_page_style;
if (
style === "categories_and_latest_topics" ||
style === "categories_and_latest_topics_created_date"
) {
return this._findCategoriesAndTopics("latest");
model = await this._findCategoriesAndTopics("latest", parentCategory);
} else if (style === "categories_and_top_topics") {
return this._findCategoriesAndTopics("top");
model = await this._findCategoriesAndTopics("top", parentCategory);
} else {
// The server may have serialized this. Based on the logic above, we don't need it
// so remove it to avoid it being used later by another TopicList route.
PreloadStore.remove("topic_list");
model = await CategoryList.list(this.store, parentCategory);
}
return CategoryList.list(this.store);
return model;
}
model() {
return this.findCategories().then((model) => {
async model(params) {
let parentCategory;
if (params.category_slug_path_with_id) {
parentCategory = this.site.lazy_load_categories
? await Category.asyncFindBySlugPathWithID(
params.category_slug_path_with_id
)
: Category.findBySlugPathWithID(params.category_slug_path_with_id);
}
return this.findCategories(parentCategory).then((model) => {
const tracking = this.topicTrackingState;
if (tracking) {
tracking.sync(model, "categories");
@ -79,7 +92,7 @@ export default class DiscoveryCategoriesRoute extends DiscourseRoute {
};
}
async _findCategoriesAndTopics(filter) {
async _findCategoriesAndTopics(filter, parentCategory = null) {
return hash({
categoriesList: PreloadStore.getAndRemove("categories_list"),
topicsList: PreloadStore.getAndRemove("topic_list"),
@ -92,7 +105,11 @@ export default class DiscoveryCategoriesRoute extends DiscourseRoute {
return { ...result.categoriesList, ...result.topicsList };
} else {
// Otherwise, return the ajax result
return ajax(`/categories_and_${filter}`);
const data = {};
if (parentCategory) {
data.parent_category_id = parentCategory.id;
}
return ajax(`/categories_and_${filter}`, { data });
}
})
.then((result) => {
@ -102,7 +119,12 @@ export default class DiscoveryCategoriesRoute extends DiscourseRoute {
return CategoryList.create({
store: this.store,
categories: CategoryList.categoriesFrom(this.store, result),
categories: CategoryList.categoriesFrom(
this.store,
result,
parentCategory
),
parentCategory,
topics: TopicList.topicsFrom(this.store, result),
can_create_category: result.category_list.can_create_category,
can_create_topic: result.category_list.can_create_topic,

View File

@ -0,0 +1,3 @@
import DiscoveryCategoriesRoute from "discourse/routes/discovery-categories";
export default class DiscoverySubcategoriesRoute extends DiscoveryCategoriesRoute {}

View File

@ -1,6 +1,7 @@
<Discovery::Layout @model={{this.model}}>
<:navigation>
<Discovery::Navigation
@category={{this.model.parentCategory}}
@showCategoryAdmin={{this.model.can_create_category}}
@canCreateTopic={{this.model.can_create_topic}}
@createTopic={{this.createTopic}}

View File

@ -0,0 +1,32 @@
import { currentRouteName, currentURL, visit } from "@ember/test-helpers";
import { test } from "qunit";
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
import selectKit from "discourse/tests/helpers/select-kit-helper";
acceptance("Subcategories", function (needs) {
needs.site({
lazy_load_categories: true,
});
test("navigation can be used to navigate subcategories pages", async function (assert) {
await visit("/categories");
let categoryDrop = selectKit(
".category-breadcrumb li:nth-of-type(1) .category-drop"
);
await categoryDrop.expand();
await categoryDrop.selectRowByValue("2"); // "feature" category
assert.strictEqual(currentRouteName(), "discovery.subcategories");
assert.strictEqual(currentURL(), "/c/feature/2/subcategories");
categoryDrop = selectKit(
".category-breadcrumb li:nth-of-type(2) .category-drop"
);
await categoryDrop.expand();
await categoryDrop.selectRowByValue("26"); // "spec" category
assert.strictEqual(currentRouteName(), "discovery.subcategories");
assert.strictEqual(currentURL(), "/c/feature/spec/26/subcategories");
});
});

View File

@ -492,9 +492,43 @@ export function applyDefaultHandlers(pretender) {
return response([{ id: 1234, cooked: "wat" }]);
});
pretender.get("/categories_and_latest", () =>
response(fixturesByUrl["/categories_and_latest.json"])
);
pretender.get("/categories.json", (request) => {
const data = cloneJSON(fixturesByUrl["/categories.json"]);
// replace categories list if parent_category_id filter is present
if (request.queryParams.parent_category_id) {
const parentCategoryId = parseInt(
request.queryParams.parent_category_id,
10
);
data.category_list.categories = fixturesByUrl[
"site.json"
].site.categories.filter(
(c) => c.parent_category_id === parentCategoryId
);
}
return response(data);
});
pretender.get("/categories_and_latest", (request) => {
const data = cloneJSON(fixturesByUrl["/categories_and_latest.json"]);
// replace categories list if parent_category_id filter is present
if (request.queryParams.parent_category_id) {
const parentCategoryId = parseInt(
request.queryParams.parent_category_id,
10
);
data.category_list.categories = fixturesByUrl[
"site.json"
].site.categories.filter(
(c) => c.parent_category_id === parentCategoryId
);
}
return response(data);
});
pretender.get("/c/bug/find_by_slug.json", () =>
response(fixturesByUrl["/c/1/show.json"])
@ -529,6 +563,26 @@ export function applyDefaultHandlers(pretender) {
response(fixturesByUrl["/c/11/show.json"])
);
pretender.get("/categories/find", () => {
return response({
categories: fixturesByUrl["site.json"].site.categories,
});
});
pretender.post("/categories/search", (request) => {
const data = parsePostData(request.requestBody);
if (data.include_ancestors) {
return response({
categories: fixturesByUrl["site.json"].site.categories,
ancestors: fixturesByUrl["site.json"].site.categories,
});
} else {
return response({
categories: fixturesByUrl["site.json"].site.categories,
});
}
});
pretender.get("/c/testing/find_by_slug.json", () =>
response(fixturesByUrl["/c/11/show.json"])
);

View File

@ -30,14 +30,24 @@ export default class CategoryDropMoreCollection extends Component {
<template>
{{#if this.moreCount}}
<div class="category-drop-footer">
<span>{{i18n
"categories.plus_more_count"
(hash count=this.moreCount)
}}</span>
<LinkTo @route="discovery.categories">
{{i18n "categories.view_all"}}
{{icon "external-link-alt"}}
</LinkTo>
<span>
{{i18n "categories.plus_more_count" (hash count=this.moreCount)}}
</span>
{{#if @selectKit.options.parentCategory}}
<LinkTo
@route="discovery.subcategories"
@model={{@selectKit.options.parentCategory.id}}
>
{{i18n "categories.view_all"}}
{{icon "external-link-alt"}}
</LinkTo>
{{else}}
<LinkTo @route="discovery.categories">
{{i18n "categories.view_all"}}
{{icon "external-link-alt"}}
</LinkTo>
{{/if}}
</div>
{{/if}}
</template>

View File

@ -46,6 +46,7 @@ export default ComboBoxComponent.extend({
headerComponent: "category-drop/category-drop-header",
parentCategory: false,
allowUncategorized: "allowUncategorized",
disableIfHasNoChildren: false,
},
init() {
@ -93,6 +94,7 @@ export default ComboBoxComponent.extend({
if (
this.selectKit.options.subCategory &&
this.filterType !== "categories" &&
(this.value || !this.selectKit.options.noSubcategories)
) {
shortcuts.push({
@ -157,6 +159,7 @@ export default ComboBoxComponent.extend({
if (this.editingCategory) {
return this.noCategoriesLabel;
}
if (this.selectKit.options.subCategory) {
return I18n.t("categories.all_subcategories", {
categoryName: this.parentCategoryName,
@ -225,17 +228,35 @@ export default ComboBoxComponent.extend({
? this.selectKit.options.parentCategory
: Category.findById(parseInt(categoryId, 10));
const route = this.editingCategory
? getEditCategoryUrl(
category,
categoryId !== NO_CATEGORIES_ID,
this.editingCategoryTab
)
: getCategoryAndTagUrl(
category,
categoryId !== NO_CATEGORIES_ID,
this.tagId
);
let route;
if (this.editingCategoryTab) {
// rendered on category page
route = getEditCategoryUrl(
category,
categoryId !== NO_CATEGORIES_ID,
this.editingCategoryTab
);
} else if (
this.site.lazy_load_categories &&
this.filterType === "categories"
) {
// rendered on categories page
if (categoryId === "all-categories" || categoryId === "no-categories") {
route = this.selectKit.options.parentCategory
? `${this.selectKit.options.parentCategory.url}/subcategories`
: "/categories";
} else if (categoryId) {
route = `${Category.findById(categoryId).url}/subcategories`;
} else {
route = "/categories";
}
} else {
route = getCategoryAndTagUrl(
category,
categoryId !== NO_CATEGORIES_ID,
this.tagId
);
}
DiscourseURL.routeToUrl(route);
},

View File

@ -90,6 +90,13 @@ export default class CategoryRow extends Component {
return this.category.description_text;
}
get isDisabled() {
return (
this.args.selectKit.options.disableIfHasNoChildren &&
this.args.item.has_children === false
);
}
@cached
get category() {
if (isEmpty(this.rowValue)) {
@ -187,7 +194,9 @@ export default class CategoryRow extends Component {
handleClick(event) {
event.preventDefault();
event.stopPropagation();
this.args.selectKit.select(this.rowValue, this.args.item);
if (!this.isDisabled) {
this.args.selectKit.select(this.rowValue, this.args.item);
}
return false;
}
@ -226,10 +235,12 @@ export default class CategoryRow extends Component {
} else if (event.key === "Enter") {
event.stopImmediatePropagation();
this.args.selectKit.select(
this.args.selectKit.highlighted.id,
this.args.selectKit.highlighted
);
if (!this.isDisabled) {
this.args.selectKit.select(
this.args.selectKit.highlighted.id,
this.args.selectKit.highlighted
);
}
event.preventDefault();
} else if (event.key === "Escape") {
this.args.selectKit.close(event);
@ -272,6 +283,7 @@ export default class CategoryRow extends Component {
(if this.isSelected "is-selected")
(if this.isHighlighted "is-highlighted")
(if this.isNone "is-none")
(if this.isDisabled "is-disabled")
}}
role="menuitemradio"
data-index={{@index}}

View File

@ -29,6 +29,14 @@
font-weight: 700;
}
&.is-disabled {
cursor: not-allowed;
.badge-category__name {
color: var(--primary-low-mid);
}
}
.category-desc {
font-weight: normal;
color: var(--primary-medium);
@ -60,6 +68,10 @@
span {
color: var(--primary-high);
margin: 0 10px;
&.active {
display: none;
}
}
}
}

View File

@ -34,8 +34,12 @@ class CategoriesController < ApplicationController
@description = SiteSetting.site_description
parent_category =
Category.find_by_slug(params[:parent_category_id]) ||
Category.find_by(id: params[:parent_category_id].to_i)
if params[:parent_category_id].present?
Category.find_by_slug(params[:parent_category_id]) ||
Category.find_by(id: params[:parent_category_id].to_i)
elsif params[:category_slug_path_with_id].present?
Category.find_by_slug_path_with_id(params[:category_slug_path_with_id])
end
include_subcategories =
SiteSetting.desktop_category_page_style == "subcategories_with_featured_topics" ||
@ -43,7 +47,7 @@ class CategoriesController < ApplicationController
category_options = {
is_homepage: current_homepage == "categories",
parent_category_id: params[:parent_category_id],
parent_category_id: parent_category&.id,
include_topics: include_topics(parent_category),
include_subcategories: include_subcategories,
tag: params[:tag],

View File

@ -180,9 +180,6 @@ class CategoryList
include_subcategories = @options[:include_subcategories] == true
notification_levels = CategoryUser.notification_levels_for(@guardian.user)
default_notification_level = CategoryUser.default_notification_level
if @guardian.can_lazy_load_categories?
subcategory_ids = {}
Category

View File

@ -1166,10 +1166,11 @@ Discourse::Application.routes.draw do
get "/c", to: redirect(relative_url_root + "categories")
resources :categories, except: %i[show new edit]
resources :categories, only: %i[index create update destroy]
post "categories/reorder" => "categories#reorder"
get "categories/find" => "categories#find"
post "categories/search" => "categories#search"
get "categories/:parent_category_id" => "categories#index"
scope path: "category/:category_id" do
post "/move" => "categories#move"
@ -1211,6 +1212,9 @@ Discourse::Application.routes.draw do
:constraints => {
format: "html",
}
get "/subcategories" => "categories#index"
get "/" => "list#category_default", :as => "category_default"
end