FEATURE: Show remaining count in category-drop (#25938)

When "lazy load categories" is enabled, the CategoryDrop component will
render at most 15 categories. If there are more categories, a "Show
more" link pointing to the categories page will be displayed.
This commit is contained in:
Bianca Nenciu 2024-03-07 16:14:50 +02:00 committed by GitHub
parent 821402d024
commit e89bdea830
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 177 additions and 51 deletions

View File

@ -400,6 +400,7 @@ export default class Category extends RestModel {
categories: result["categories"].map((category) =>
Site.current().updateCategory(category)
),
categoriesCount: result["categories_count"],
};
} else {
return result["categories"].map((category) =>

View File

@ -68,7 +68,7 @@ module("Integration | Component | select-kit/category-drop", function (hooks) {
const text = this.subject.header().label();
assert.strictEqual(
text,
I18n.t("category.all").toLowerCase(),
I18n.t("categories.categories_label"),
"it uses the noneLabel"
);
});

View File

@ -0,0 +1,44 @@
import Component from "@glimmer/component";
import { hash } from "@ember/helper";
import { LinkTo } from "@ember/routing";
import { inject as service } from "@ember/service";
import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
import {
ALL_CATEGORIES_ID,
NO_CATEGORIES_ID,
} from "select-kit/components/category-drop";
export default class CategoryDropMoreCollection extends Component {
@service site;
tagName = "";
get moreCount() {
if (!this.args.selectKit.totalCount) {
return 0;
}
const currentCount = this.args.collection.content.filter(
(category) =>
category.id !== NO_CATEGORIES_ID && category.id !== ALL_CATEGORIES_ID
).length;
return this.args.selectKit.totalCount - currentCount;
}
<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>
</div>
{{/if}}
</template>
}

View File

@ -9,18 +9,22 @@ import DiscourseURL, {
} from "discourse/lib/url";
import Category from "discourse/models/category";
import I18n from "discourse-i18n";
import CategoryDropMoreCollection from "select-kit/components/category-drop-more-collection";
import CategoryRow from "select-kit/components/category-row";
import ComboBoxComponent from "select-kit/components/combo-box";
import { MAIN_COLLECTION } from "select-kit/components/select-kit";
export const NO_CATEGORIES_ID = "no-categories";
export const ALL_CATEGORIES_ID = "all-categories";
const MORE_COLLECTION = "MORE_COLLECTION";
export default ComboBoxComponent.extend({
pluginApiIdentifiers: ["category-drop"],
classNames: ["category-drop"],
value: readOnly("category.id"),
content: readOnly("categoriesWithShortcuts.[]"),
noCategoriesLabel: I18n.t("categories.no_subcategory"),
noCategoriesLabel: I18n.t("categories.no_subcategories"),
navigateToEdit: false,
editingCategory: false,
editingCategoryTab: null,
@ -44,6 +48,18 @@ export default ComboBoxComponent.extend({
allowUncategorized: "allowUncategorized",
},
init() {
this._super(...arguments);
this.insertAfterCollection(MAIN_COLLECTION, MORE_COLLECTION);
},
modifyComponentForCollection(collection) {
if (collection === MORE_COLLECTION) {
return CategoryDropMoreCollection;
}
},
modifyComponentForRow() {
return CategoryRow;
},
@ -85,6 +101,12 @@ export default ComboBoxComponent.extend({
});
}
// If there is a single shortcut, we can have a single "remove filter"
// option
if (shortcuts.length === 1 && shortcuts[0].id === ALL_CATEGORIES_ID) {
shortcuts[0].name = I18n.t("categories.remove_filter");
}
return shortcuts;
}
),
@ -96,9 +118,17 @@ export default ComboBoxComponent.extend({
modifyNoSelection() {
if (this.selectKit.options.noSubcategories) {
return this.defaultItem(NO_CATEGORIES_ID, this.noCategoriesLabel);
return this.defaultItem(
NO_CATEGORIES_ID,
I18n.t("categories.no_subcategories")
);
} else {
return this.defaultItem(ALL_CATEGORIES_ID, this.allCategoriesLabel);
return this.defaultItem(
ALL_CATEGORIES_ID,
this.selectKit.options.subCategory
? I18n.t("categories.subcategories_label")
: I18n.t("categories.categories_label")
);
}
},
@ -149,16 +179,23 @@ export default ComboBoxComponent.extend({
parentCategoryId = -1;
}
const results = (
await Category.asyncSearch(filter, {
parentCategoryId,
includeUncategorized: this.siteSettings.allow_uncategorized_topics,
includeAncestors: true,
limit: 15,
})
).categories;
const result = await Category.asyncSearch(filter, {
parentCategoryId,
includeUncategorized: this.siteSettings.allow_uncategorized_topics,
includeAncestors: true,
// Show all categories if possible (up to 18), otherwise show just
// first 15 and let CategoryDropMoreCollection show the "show more" link
limit: 18,
});
return this.shortcuts.concat(results);
const categories =
result.categoriesCount > 18
? result.categories.slice(0, 15)
: result.categories;
this.selectKit.totalCount = result.categoriesCount;
return this.shortcuts.concat(categories);
}
const opts = {

View File

@ -1,8 +1,9 @@
import { computed } from "@ember/object";
import { equal, readOnly } from "@ember/object/computed";
import { i18n, setting } from "discourse/lib/computed";
import { readOnly } from "@ember/object/computed";
import { setting } from "discourse/lib/computed";
import DiscourseURL, { getCategoryAndTagUrl } from "discourse/lib/url";
import { makeArray } from "discourse-common/lib/helpers";
import I18n from "discourse-i18n";
import ComboBoxComponent from "select-kit/components/combo-box";
import FilterForMore from "select-kit/components/filter-for-more";
import { MAIN_COLLECTION } from "select-kit/components/select-kit";
@ -10,7 +11,8 @@ import TagsMixin from "select-kit/mixins/tags";
export const NO_TAG_ID = "no-tags";
export const ALL_TAGS_ID = "all-tags";
export const NONE_TAG_ID = "none";
export const NONE_TAG = "none";
const MORE_TAGS_COLLECTION = "MORE_TAGS_COLLECTION";
@ -45,8 +47,6 @@ export default ComboBoxComponent.extend(TagsMixin, {
autoInsertNoneItem: false,
},
noTagsSelected: equal("tagId", NONE_TAG_ID),
init() {
this._super(...arguments);
@ -68,20 +68,18 @@ export default ComboBoxComponent.extend(TagsMixin, {
},
modifyNoSelection() {
if (this.noTagsSelected) {
return this.defaultItem(NO_TAG_ID, this.noTagsLabel);
if (this.tagId === NONE_TAG) {
return this.defaultItem(NO_TAG_ID, I18n.t("tagging.selector_no_tags"));
} else {
return this.defaultItem(ALL_TAGS_ID, this.allTagsLabel);
return this.defaultItem(ALL_TAGS_ID, I18n.t("tagging.selector_tags"));
}
},
modifySelection(content) {
if (this.tagId) {
if (this.noTagsSelected) {
content = this.defaultItem(NO_TAG_ID, this.noTagsLabel);
} else {
content = this.defaultItem(this.tagId, this.tagId);
}
if (this.tagId === NONE_TAG) {
content = this.defaultItem(NO_TAG_ID, I18n.t("tagging.selector_no_tags"));
} else if (this.tagId) {
content = this.defaultItem(this.tagId, this.tagId);
}
return content;
@ -91,10 +89,6 @@ export default ComboBoxComponent.extend(TagsMixin, {
return this.tagId ? `tag-${this.tagId}` : "tag_all";
}),
allTagsLabel: i18n("tagging.selector_all_tags"),
noTagsLabel: i18n("tagging.selector_no_tags"),
modifyComponentForRow() {
return "tag-row";
},
@ -102,15 +96,24 @@ export default ComboBoxComponent.extend(TagsMixin, {
shortcuts: computed("tagId", function () {
const shortcuts = [];
if (this.tagId !== NONE_TAG_ID) {
if (this.tagId !== NONE_TAG) {
shortcuts.push({
id: NO_TAG_ID,
name: this.noTagsLabel,
name: I18n.t("tagging.selector_no_tags"),
});
}
if (this.tagId) {
shortcuts.push({ id: ALL_TAGS_ID, name: this.allTagsLabel });
shortcuts.push({
id: ALL_TAGS_ID,
name: I18n.t("tagging.selector_all_tags"),
});
}
// If there is a single shortcut, we can have a single "remove filter"
// option
if (shortcuts.length === 1 && shortcuts[0].id === ALL_TAGS_ID) {
shortcuts[0].name = I18n.t("tagging.selector_remove_filter");
}
return shortcuts;
@ -173,7 +176,7 @@ export default ComboBoxComponent.extend(TagsMixin, {
actions: {
onChange(tagId, tag) {
if (tagId === NO_TAG_ID) {
tagId = NONE_TAG_ID;
tagId = NONE_TAG;
} else if (tagId === ALL_TAGS_ID) {
tagId = null;
} else if (tag && tag.targetTagId) {

View File

@ -1 +1,5 @@
{{discourse-tag this.rowValue noHref=true count=this.item.count}}
{{#if this.isTag}}
{{discourse-tag this.rowValue noHref=true count=this.item.count}}
{{else}}
<span class="name">{{this.item.name}}</span>
{{/if}}

View File

@ -1,5 +1,11 @@
import discourseComputed from "discourse-common/utils/decorators";
import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row";
export default SelectKitRowComponent.extend({
classNames: ["tag-row"],
@discourseComputed("item")
isTag(item) {
return item.id !== "no-tags" && item.id !== "all-tags";
},
});

View File

@ -10,6 +10,10 @@
}
.category-drop-header {
&[data-value=""] {
color: var(--primary-high);
}
&.is-none .selected-name {
color: inherit;
}
@ -42,6 +46,22 @@
font-size: var(--font-down-1);
}
}
.category-drop-footer {
align-items: center;
border-top: 1px solid var(--primary-low);
display: flex;
font-size: var(--font-down-1);
height: 30px;
justify-content: space-between;
width: 100%;
a,
span {
color: var(--primary-high);
margin: 0 10px;
}
}
}
}
}

View File

@ -10,6 +10,10 @@
font-weight: 700;
}
}
.tag-drop-header {
color: var(--primary-high);
}
}
}
}

View File

@ -392,6 +392,8 @@ class CategoriesController < ApplicationController
categories = categories.where(parent_category_id: nil) if !include_subcategories
categories_count = categories.count
categories = categories.limit(limit || MAX_CATEGORIES_LIMIT)
Category.preload_user_fields!(guardian, categories)
@ -409,18 +411,17 @@ class CategoriesController < ApplicationController
]
end
response = {
categories_count: categories_count,
categories: serialize_data(categories, SiteCategorySerializer, scope: guardian),
}
if include_ancestors
ancestors = Category.secured(guardian).ancestors_of(categories.map(&:id))
render_json_dump(
{
categories: serialize_data(categories, SiteCategorySerializer, scope: guardian),
ancestors: serialize_data(ancestors, SiteCategorySerializer, scope: guardian),
},
)
else
render_serialized(categories, SiteCategorySerializer, root: :categories, scope: guardian)
response[:ancestors] = serialize_data(ancestors, SiteCategorySerializer, scope: guardian)
end
render_json_dump(response)
end
private

View File

@ -1034,9 +1034,13 @@ en:
"15": "Drafts"
categories:
all: "all categories"
all_subcategories: "all"
no_subcategory: "none"
categories_label: "categories"
subcategories_label: "subcategories"
all_subcategories: "all subcategories"
no_subcategories: "no subcategories"
remove_filter: "remove filter"
plus_more_count: "+%{count} more"
view_all: "view all"
category: "Category"
category_list: "Display category list"
reorder:
@ -4353,8 +4357,10 @@ en:
tagging:
all_tags: "All tags"
other_tags: "Other Tags"
selector_tags: "tags"
selector_all_tags: "all tags"
selector_no_tags: "no tags"
selector_remove_filter: "remove filter"
tags: "Tags"
choose_for_topic: "optional tags"
choose_for_topic_required:

View File

@ -61,7 +61,7 @@ describe "Navigating with breadcrumbs", type: :system do
expect(discovery.topic_list).to have_topic(c1_topic_tagged)
expect(discovery.topic_list).to have_topics(count: 2)
expect(discovery.tag_drop).to have_selected_name("all tags")
expect(discovery.tag_drop).to have_selected_name("tags")
discovery.tag_drop.select_row_by_value(tag.name)
expect(discovery.topic_list).to have_topics(count: 1)
@ -74,8 +74,8 @@ describe "Navigating with breadcrumbs", type: :system do
expect(discovery.topic_list).to have_topic(c3_topic_tagged)
expect(discovery.topic_list).to have_topics(count: 2)
expect(discovery.subcategory_drop).to have_selected_name("none")
expect(discovery.tag_drop).to have_selected_name("all tags")
expect(discovery.subcategory_drop).to have_selected_name("no subcategories")
expect(discovery.tag_drop).to have_selected_name("tags")
discovery.tag_drop.select_row_by_value(tag.name)
expect(page).to have_current_path(