mirror of
https://github.com/discourse/discourse.git
synced 2025-03-24 04:55:31 +08:00
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:
parent
d964709644
commit
77b032c2b5
@ -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>
|
||||
|
@ -4,6 +4,7 @@
|
||||
@noSubcategories={{this.noSubcategories}}
|
||||
@tag={{this.tag}}
|
||||
@additionalTags={{this.additionalTags}}
|
||||
@filterType={{this.filterType}}
|
||||
/>
|
||||
|
||||
{{#unless this.additionalTags}}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -0,0 +1,3 @@
|
||||
import DiscoveryCategoriesRoute from "discourse/routes/discovery-categories";
|
||||
|
||||
export default class DiscoverySubcategoriesRoute extends DiscoveryCategoriesRoute {}
|
@ -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}}
|
||||
|
@ -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");
|
||||
});
|
||||
});
|
@ -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"])
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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}}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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],
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user