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:
parent
821402d024
commit
e89bdea830
|
@ -400,6 +400,7 @@ export default class Category extends RestModel {
|
||||||
categories: result["categories"].map((category) =>
|
categories: result["categories"].map((category) =>
|
||||||
Site.current().updateCategory(category)
|
Site.current().updateCategory(category)
|
||||||
),
|
),
|
||||||
|
categoriesCount: result["categories_count"],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return result["categories"].map((category) =>
|
return result["categories"].map((category) =>
|
||||||
|
|
|
@ -68,7 +68,7 @@ module("Integration | Component | select-kit/category-drop", function (hooks) {
|
||||||
const text = this.subject.header().label();
|
const text = this.subject.header().label();
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
text,
|
text,
|
||||||
I18n.t("category.all").toLowerCase(),
|
I18n.t("categories.categories_label"),
|
||||||
"it uses the noneLabel"
|
"it uses the noneLabel"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
}
|
|
@ -9,18 +9,22 @@ import DiscourseURL, {
|
||||||
} from "discourse/lib/url";
|
} from "discourse/lib/url";
|
||||||
import Category from "discourse/models/category";
|
import Category from "discourse/models/category";
|
||||||
import I18n from "discourse-i18n";
|
import I18n from "discourse-i18n";
|
||||||
|
import CategoryDropMoreCollection from "select-kit/components/category-drop-more-collection";
|
||||||
import CategoryRow from "select-kit/components/category-row";
|
import CategoryRow from "select-kit/components/category-row";
|
||||||
import ComboBoxComponent from "select-kit/components/combo-box";
|
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 NO_CATEGORIES_ID = "no-categories";
|
||||||
export const ALL_CATEGORIES_ID = "all-categories";
|
export const ALL_CATEGORIES_ID = "all-categories";
|
||||||
|
|
||||||
|
const MORE_COLLECTION = "MORE_COLLECTION";
|
||||||
|
|
||||||
export default ComboBoxComponent.extend({
|
export default ComboBoxComponent.extend({
|
||||||
pluginApiIdentifiers: ["category-drop"],
|
pluginApiIdentifiers: ["category-drop"],
|
||||||
classNames: ["category-drop"],
|
classNames: ["category-drop"],
|
||||||
value: readOnly("category.id"),
|
value: readOnly("category.id"),
|
||||||
content: readOnly("categoriesWithShortcuts.[]"),
|
content: readOnly("categoriesWithShortcuts.[]"),
|
||||||
noCategoriesLabel: I18n.t("categories.no_subcategory"),
|
noCategoriesLabel: I18n.t("categories.no_subcategories"),
|
||||||
navigateToEdit: false,
|
navigateToEdit: false,
|
||||||
editingCategory: false,
|
editingCategory: false,
|
||||||
editingCategoryTab: null,
|
editingCategoryTab: null,
|
||||||
|
@ -44,6 +48,18 @@ export default ComboBoxComponent.extend({
|
||||||
allowUncategorized: "allowUncategorized",
|
allowUncategorized: "allowUncategorized",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._super(...arguments);
|
||||||
|
|
||||||
|
this.insertAfterCollection(MAIN_COLLECTION, MORE_COLLECTION);
|
||||||
|
},
|
||||||
|
|
||||||
|
modifyComponentForCollection(collection) {
|
||||||
|
if (collection === MORE_COLLECTION) {
|
||||||
|
return CategoryDropMoreCollection;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
modifyComponentForRow() {
|
modifyComponentForRow() {
|
||||||
return CategoryRow;
|
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;
|
return shortcuts;
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@ -96,9 +118,17 @@ export default ComboBoxComponent.extend({
|
||||||
|
|
||||||
modifyNoSelection() {
|
modifyNoSelection() {
|
||||||
if (this.selectKit.options.noSubcategories) {
|
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 {
|
} 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;
|
parentCategoryId = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = (
|
const result = await Category.asyncSearch(filter, {
|
||||||
await Category.asyncSearch(filter, {
|
parentCategoryId,
|
||||||
parentCategoryId,
|
includeUncategorized: this.siteSettings.allow_uncategorized_topics,
|
||||||
includeUncategorized: this.siteSettings.allow_uncategorized_topics,
|
includeAncestors: true,
|
||||||
includeAncestors: true,
|
// Show all categories if possible (up to 18), otherwise show just
|
||||||
limit: 15,
|
// first 15 and let CategoryDropMoreCollection show the "show more" link
|
||||||
})
|
limit: 18,
|
||||||
).categories;
|
});
|
||||||
|
|
||||||
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 = {
|
const opts = {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { computed } from "@ember/object";
|
import { computed } from "@ember/object";
|
||||||
import { equal, readOnly } from "@ember/object/computed";
|
import { readOnly } from "@ember/object/computed";
|
||||||
import { i18n, setting } from "discourse/lib/computed";
|
import { setting } from "discourse/lib/computed";
|
||||||
import DiscourseURL, { getCategoryAndTagUrl } from "discourse/lib/url";
|
import DiscourseURL, { getCategoryAndTagUrl } from "discourse/lib/url";
|
||||||
import { makeArray } from "discourse-common/lib/helpers";
|
import { makeArray } from "discourse-common/lib/helpers";
|
||||||
|
import I18n from "discourse-i18n";
|
||||||
import ComboBoxComponent from "select-kit/components/combo-box";
|
import ComboBoxComponent from "select-kit/components/combo-box";
|
||||||
import FilterForMore from "select-kit/components/filter-for-more";
|
import FilterForMore from "select-kit/components/filter-for-more";
|
||||||
import { MAIN_COLLECTION } from "select-kit/components/select-kit";
|
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 NO_TAG_ID = "no-tags";
|
||||||
export const ALL_TAGS_ID = "all-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";
|
const MORE_TAGS_COLLECTION = "MORE_TAGS_COLLECTION";
|
||||||
|
|
||||||
|
@ -45,8 +47,6 @@ export default ComboBoxComponent.extend(TagsMixin, {
|
||||||
autoInsertNoneItem: false,
|
autoInsertNoneItem: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
noTagsSelected: equal("tagId", NONE_TAG_ID),
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
|
||||||
|
@ -68,20 +68,18 @@ export default ComboBoxComponent.extend(TagsMixin, {
|
||||||
},
|
},
|
||||||
|
|
||||||
modifyNoSelection() {
|
modifyNoSelection() {
|
||||||
if (this.noTagsSelected) {
|
if (this.tagId === NONE_TAG) {
|
||||||
return this.defaultItem(NO_TAG_ID, this.noTagsLabel);
|
return this.defaultItem(NO_TAG_ID, I18n.t("tagging.selector_no_tags"));
|
||||||
} else {
|
} else {
|
||||||
return this.defaultItem(ALL_TAGS_ID, this.allTagsLabel);
|
return this.defaultItem(ALL_TAGS_ID, I18n.t("tagging.selector_tags"));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
modifySelection(content) {
|
modifySelection(content) {
|
||||||
if (this.tagId) {
|
if (this.tagId === NONE_TAG) {
|
||||||
if (this.noTagsSelected) {
|
content = this.defaultItem(NO_TAG_ID, I18n.t("tagging.selector_no_tags"));
|
||||||
content = this.defaultItem(NO_TAG_ID, this.noTagsLabel);
|
} else if (this.tagId) {
|
||||||
} else {
|
content = this.defaultItem(this.tagId, this.tagId);
|
||||||
content = this.defaultItem(this.tagId, this.tagId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
|
@ -91,10 +89,6 @@ export default ComboBoxComponent.extend(TagsMixin, {
|
||||||
return this.tagId ? `tag-${this.tagId}` : "tag_all";
|
return this.tagId ? `tag-${this.tagId}` : "tag_all";
|
||||||
}),
|
}),
|
||||||
|
|
||||||
allTagsLabel: i18n("tagging.selector_all_tags"),
|
|
||||||
|
|
||||||
noTagsLabel: i18n("tagging.selector_no_tags"),
|
|
||||||
|
|
||||||
modifyComponentForRow() {
|
modifyComponentForRow() {
|
||||||
return "tag-row";
|
return "tag-row";
|
||||||
},
|
},
|
||||||
|
@ -102,15 +96,24 @@ export default ComboBoxComponent.extend(TagsMixin, {
|
||||||
shortcuts: computed("tagId", function () {
|
shortcuts: computed("tagId", function () {
|
||||||
const shortcuts = [];
|
const shortcuts = [];
|
||||||
|
|
||||||
if (this.tagId !== NONE_TAG_ID) {
|
if (this.tagId !== NONE_TAG) {
|
||||||
shortcuts.push({
|
shortcuts.push({
|
||||||
id: NO_TAG_ID,
|
id: NO_TAG_ID,
|
||||||
name: this.noTagsLabel,
|
name: I18n.t("tagging.selector_no_tags"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.tagId) {
|
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;
|
return shortcuts;
|
||||||
|
@ -173,7 +176,7 @@ export default ComboBoxComponent.extend(TagsMixin, {
|
||||||
actions: {
|
actions: {
|
||||||
onChange(tagId, tag) {
|
onChange(tagId, tag) {
|
||||||
if (tagId === NO_TAG_ID) {
|
if (tagId === NO_TAG_ID) {
|
||||||
tagId = NONE_TAG_ID;
|
tagId = NONE_TAG;
|
||||||
} else if (tagId === ALL_TAGS_ID) {
|
} else if (tagId === ALL_TAGS_ID) {
|
||||||
tagId = null;
|
tagId = null;
|
||||||
} else if (tag && tag.targetTagId) {
|
} else if (tag && tag.targetTagId) {
|
||||||
|
|
|
@ -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}}
|
|
@ -1,5 +1,11 @@
|
||||||
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row";
|
import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row";
|
||||||
|
|
||||||
export default SelectKitRowComponent.extend({
|
export default SelectKitRowComponent.extend({
|
||||||
classNames: ["tag-row"],
|
classNames: ["tag-row"],
|
||||||
|
|
||||||
|
@discourseComputed("item")
|
||||||
|
isTag(item) {
|
||||||
|
return item.id !== "no-tags" && item.id !== "all-tags";
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,6 +10,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-drop-header {
|
.category-drop-header {
|
||||||
|
&[data-value=""] {
|
||||||
|
color: var(--primary-high);
|
||||||
|
}
|
||||||
|
|
||||||
&.is-none .selected-name {
|
&.is-none .selected-name {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
@ -42,6 +46,22 @@
|
||||||
font-size: var(--font-down-1);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,10 @@
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-drop-header {
|
||||||
|
color: var(--primary-high);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -392,6 +392,8 @@ class CategoriesController < ApplicationController
|
||||||
|
|
||||||
categories = categories.where(parent_category_id: nil) if !include_subcategories
|
categories = categories.where(parent_category_id: nil) if !include_subcategories
|
||||||
|
|
||||||
|
categories_count = categories.count
|
||||||
|
|
||||||
categories = categories.limit(limit || MAX_CATEGORIES_LIMIT)
|
categories = categories.limit(limit || MAX_CATEGORIES_LIMIT)
|
||||||
|
|
||||||
Category.preload_user_fields!(guardian, categories)
|
Category.preload_user_fields!(guardian, categories)
|
||||||
|
@ -409,18 +411,17 @@ class CategoriesController < ApplicationController
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
response = {
|
||||||
|
categories_count: categories_count,
|
||||||
|
categories: serialize_data(categories, SiteCategorySerializer, scope: guardian),
|
||||||
|
}
|
||||||
|
|
||||||
if include_ancestors
|
if include_ancestors
|
||||||
ancestors = Category.secured(guardian).ancestors_of(categories.map(&:id))
|
ancestors = Category.secured(guardian).ancestors_of(categories.map(&:id))
|
||||||
|
response[:ancestors] = serialize_data(ancestors, SiteCategorySerializer, scope: guardian)
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
render_json_dump(response)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -1034,9 +1034,13 @@ en:
|
||||||
"15": "Drafts"
|
"15": "Drafts"
|
||||||
|
|
||||||
categories:
|
categories:
|
||||||
all: "all categories"
|
categories_label: "categories"
|
||||||
all_subcategories: "all"
|
subcategories_label: "subcategories"
|
||||||
no_subcategory: "none"
|
all_subcategories: "all subcategories"
|
||||||
|
no_subcategories: "no subcategories"
|
||||||
|
remove_filter: "remove filter"
|
||||||
|
plus_more_count: "+%{count} more"
|
||||||
|
view_all: "view all"
|
||||||
category: "Category"
|
category: "Category"
|
||||||
category_list: "Display category list"
|
category_list: "Display category list"
|
||||||
reorder:
|
reorder:
|
||||||
|
@ -4353,8 +4357,10 @@ en:
|
||||||
tagging:
|
tagging:
|
||||||
all_tags: "All tags"
|
all_tags: "All tags"
|
||||||
other_tags: "Other Tags"
|
other_tags: "Other Tags"
|
||||||
|
selector_tags: "tags"
|
||||||
selector_all_tags: "all tags"
|
selector_all_tags: "all tags"
|
||||||
selector_no_tags: "no tags"
|
selector_no_tags: "no tags"
|
||||||
|
selector_remove_filter: "remove filter"
|
||||||
tags: "Tags"
|
tags: "Tags"
|
||||||
choose_for_topic: "optional tags"
|
choose_for_topic: "optional tags"
|
||||||
choose_for_topic_required:
|
choose_for_topic_required:
|
||||||
|
|
|
@ -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_topic(c1_topic_tagged)
|
||||||
expect(discovery.topic_list).to have_topics(count: 2)
|
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)
|
discovery.tag_drop.select_row_by_value(tag.name)
|
||||||
|
|
||||||
expect(discovery.topic_list).to have_topics(count: 1)
|
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_topic(c3_topic_tagged)
|
||||||
expect(discovery.topic_list).to have_topics(count: 2)
|
expect(discovery.topic_list).to have_topics(count: 2)
|
||||||
|
|
||||||
expect(discovery.subcategory_drop).to have_selected_name("none")
|
expect(discovery.subcategory_drop).to have_selected_name("no subcategories")
|
||||||
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)
|
discovery.tag_drop.select_row_by_value(tag.name)
|
||||||
|
|
||||||
expect(page).to have_current_path(
|
expect(page).to have_current_path(
|
||||||
|
|
Loading…
Reference in New Issue