mirror of
https://github.com/discourse/discourse.git
synced 2025-06-06 21:14:39 +08:00
UX: Allow users to filter categories in edit sidebar categories modal (#21996)
What does this change do? This change is a continuation of 2191b879c693f898bf7602b4f9ab6780b1eead67 and adds an input filter to the edit sidebar categories modal which the user can use to filter through the list of categories by the category's name. Note that if a child category is being shown, all of its ancestors will be shown even if the names of the ancestors do not match the given filter. This is to ensure that we continue to display the hierarchy of a child category even if the parent category does not match the filter.
This commit is contained in:

committed by
GitHub

parent
e48750281e
commit
853bce2abc
@ -3,44 +3,65 @@
|
|||||||
@class="sidebar-categories-form-modal"
|
@class="sidebar-categories-form-modal"
|
||||||
>
|
>
|
||||||
<form class="sidebar-categories-form">
|
<form class="sidebar-categories-form">
|
||||||
{{#each this.categoryGroupings as |categories|}}
|
<div class="sidebar-categories-form__filter">
|
||||||
<div
|
{{d-icon "search" class="sidebar-categories-form__filter-input-icon"}}
|
||||||
class="sidebar-categories-form__row"
|
|
||||||
style={{html-safe (border-color categories.1.color "left")}}
|
|
||||||
>
|
|
||||||
|
|
||||||
{{#each categories as |category|}}
|
<Input
|
||||||
<div
|
class="sidebar-categories-form__filter-input-field"
|
||||||
class="sidebar-categories-form__category-row"
|
placeholder={{i18n "sidebar.categories_form.filter_placeholder"}}
|
||||||
data-category-id={{category.id}}
|
@type="text"
|
||||||
data-category-level={{category.level}}
|
@value={{this.filter}}
|
||||||
>
|
{{on "input" (action "onFilterInput" value="target.value")}}
|
||||||
<label
|
/>
|
||||||
class="sidebar-categories-form__category-label"
|
</div>
|
||||||
for={{concat "sidebar-categories-form__input--" category.id}}
|
|
||||||
|
{{#if (gt this.filteredCategoriesGroupings.length 0)}}
|
||||||
|
{{#each this.filteredCategoriesGroupings as |categories|}}
|
||||||
|
<div
|
||||||
|
class="sidebar-categories-form__row"
|
||||||
|
style={{html-safe (border-color categories.0.color "left")}}
|
||||||
|
>
|
||||||
|
|
||||||
|
{{#each categories as |category|}}
|
||||||
|
<div
|
||||||
|
class="sidebar-categories-form__category-row"
|
||||||
|
data-category-id={{category.id}}
|
||||||
|
data-category-level={{category.level}}
|
||||||
>
|
>
|
||||||
<div class="sidebar-categories-form__category-badge">
|
<label
|
||||||
{{category-badge category}}
|
class="sidebar-categories-form__category-label"
|
||||||
</div>
|
for={{concat "sidebar-categories-form__input--" category.id}}
|
||||||
|
>
|
||||||
{{#unless category.parentCategory}}
|
<div class="sidebar-categories-form__category-badge">
|
||||||
<div class="sidebar-categories-form__category-description">
|
{{category-badge category}}
|
||||||
{{dir-span category.description_excerpt htmlSafe="true"}}
|
|
||||||
</div>
|
</div>
|
||||||
{{/unless}}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Input
|
{{#unless category.parentCategory}}
|
||||||
id={{concat "sidebar-categories-form__input--" category.id}}
|
<div class="sidebar-categories-form__category-description">
|
||||||
class="sidebar-categories-form__input"
|
{{dir-span category.description_excerpt htmlSafe="true"}}
|
||||||
@type="checkbox"
|
</div>
|
||||||
@checked={{includes this.selectedSidebarCategoryIds category.id}}
|
{{/unless}}
|
||||||
{{on "click" (action "toggleCategory" category.id)}}
|
</label>
|
||||||
/>
|
|
||||||
</div>
|
<Input
|
||||||
{{/each}}
|
id={{concat "sidebar-categories-form__input--" category.id}}
|
||||||
|
class="sidebar-categories-form__input"
|
||||||
|
@type="checkbox"
|
||||||
|
@checked={{includes
|
||||||
|
this.selectedSidebarCategoryIds
|
||||||
|
category.id
|
||||||
|
}}
|
||||||
|
{{on "click" (action "toggleCategory" category.id)}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
{{else}}
|
||||||
|
<div class="sidebar-categories-form__no-categories">
|
||||||
|
{{i18n "sidebar.categories_form.no_categories"}}
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/if}}
|
||||||
</form>
|
</form>
|
||||||
</DModalBody>
|
</DModalBody>
|
||||||
|
|
||||||
|
@ -3,12 +3,16 @@ import { inject as service } from "@ember/service";
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
|
||||||
|
import { INPUT_DELAY } from "discourse-common/config/environment";
|
||||||
|
import discourseDebounce from "discourse-common/lib/debounce";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
|
||||||
export default class extends Component {
|
export default class extends Component {
|
||||||
@service site;
|
@service site;
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
|
|
||||||
|
@tracked filter = "";
|
||||||
|
|
||||||
@tracked selectedSidebarCategoryIds = [
|
@tracked selectedSidebarCategoryIds = [
|
||||||
...this.currentUser.sidebar_category_ids,
|
...this.currentUser.sidebar_category_ids,
|
||||||
];
|
];
|
||||||
@ -38,6 +42,49 @@ export default class extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get filteredCategoriesGroupings() {
|
||||||
|
if (this.filter.length === 0) {
|
||||||
|
return this.categoryGroupings;
|
||||||
|
} else {
|
||||||
|
return this.categoryGroupings.reduce((acc, categoryGrouping) => {
|
||||||
|
const filteredCategories = new Set();
|
||||||
|
|
||||||
|
categoryGrouping.forEach((category) => {
|
||||||
|
if (this.#matchesFilter(category, this.filter)) {
|
||||||
|
if (category.parentCategory?.parentCategory) {
|
||||||
|
filteredCategories.add(category.parentCategory.parentCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category.parentCategory) {
|
||||||
|
filteredCategories.add(category.parentCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredCategories.add(category);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filteredCategories.size > 0) {
|
||||||
|
acc.push(Array.from(filteredCategories));
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#matchesFilter(category, filter) {
|
||||||
|
return category.nameLower.includes(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onFilterInput(filter) {
|
||||||
|
discourseDebounce(this, this.#performFiltering, filter, INPUT_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#performFiltering(filter) {
|
||||||
|
this.filter = filter.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
toggleCategory(categoryId) {
|
toggleCategory(categoryId) {
|
||||||
if (this.selectedSidebarCategoryIds.includes(categoryId)) {
|
if (this.selectedSidebarCategoryIds.includes(categoryId)) {
|
||||||
|
@ -134,7 +134,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:not(.history-modal) {
|
&:not(.history-modal) {
|
||||||
.modal-body:not(.reorder-categories):not(.poll-ui-builder):not(.poll-breakdown) {
|
.modal-body:not(.reorder-categories):not(.poll-ui-builder):not(.poll-breakdown):not(.sidebar-categories-form-modal) {
|
||||||
max-height: 80vh !important;
|
max-height: 80vh !important;
|
||||||
@media screen and (max-height: 500px) {
|
@media screen and (max-height: 500px) {
|
||||||
max-height: 65vh !important;
|
max-height: 65vh !important;
|
||||||
|
@ -1,4 +1,39 @@
|
|||||||
|
.sidebar-categories-form-modal {
|
||||||
|
.modal-body {
|
||||||
|
min-height: 50vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-categories-form {
|
.sidebar-categories-form {
|
||||||
|
.sidebar-categories-form__filter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-categories-form__filter-input-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 0.5em;
|
||||||
|
top: 0.65em;
|
||||||
|
color: var(--primary-low-mid);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-categories-form__filter-input-field {
|
||||||
|
border-color: var(--primary-low-mid);
|
||||||
|
padding-left: 1.75em;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--tertiary);
|
||||||
|
outline: none;
|
||||||
|
outline-offset: 0;
|
||||||
|
box-shadow: inset 0px 0px 0px 1px var(--tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-categories-form__row {
|
.sidebar-categories-form__row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -21,7 +56,7 @@
|
|||||||
padding: 0.5em 0;
|
padding: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-categories-form__category-row[data-category-level="0"] {
|
.sidebar-categories-form__category-row[data-category-level="0"]:not(:only-child) {
|
||||||
border-bottom: 1px solid var(--primary-low);
|
border-bottom: 1px solid var(--primary-low);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
@import "sidebar-categories-form";
|
||||||
@import "user-card";
|
@import "user-card";
|
||||||
@import "user-info";
|
@import "user-info";
|
||||||
@import "user-stream-item";
|
@import "user-stream-item";
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
.sidebar-categories-form-modal {
|
||||||
|
.modal-inner-container {
|
||||||
|
min-width: var(--modal-max-width);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
@import "sidebar-categories-form";
|
||||||
@import "topic-footer-mobile-dropdown";
|
@import "topic-footer-mobile-dropdown";
|
||||||
@import "user-card";
|
@import "user-card";
|
||||||
@import "user-stream-item";
|
@import "user-stream-item";
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
.sidebar-categories-form-modal {
|
||||||
|
.modal-inner-container {
|
||||||
|
width: 35em;
|
||||||
|
}
|
||||||
|
}
|
@ -4425,8 +4425,8 @@ en:
|
|||||||
categories_form:
|
categories_form:
|
||||||
save: "Save"
|
save: "Save"
|
||||||
title: "Edit categories navigation"
|
title: "Edit categories navigation"
|
||||||
filter_input:
|
filter_placeholder: "Filter categories"
|
||||||
placeholder: "Filter categories"
|
no_categories: "There are no categories matching the given term."
|
||||||
|
|
||||||
sections:
|
sections:
|
||||||
custom:
|
custom:
|
||||||
|
@ -3,11 +3,20 @@
|
|||||||
RSpec.describe "Editing sidebar categories navigation", type: :system do
|
RSpec.describe "Editing sidebar categories navigation", type: :system do
|
||||||
fab!(:user) { Fabricate(:user) }
|
fab!(:user) { Fabricate(:user) }
|
||||||
fab!(:group) { Fabricate(:group).tap { |g| g.add(user) } }
|
fab!(:group) { Fabricate(:group).tap { |g| g.add(user) } }
|
||||||
fab!(:category) { Fabricate(:category) }
|
fab!(:category) { Fabricate(:category, name: "category") }
|
||||||
fab!(:category_subcategory) { Fabricate(:category, parent_category_id: category.id) }
|
fab!(:category_subcategory) do
|
||||||
fab!(:category_subcategory2) { Fabricate(:category, parent_category_id: category.id) }
|
Fabricate(:category, parent_category_id: category.id, name: "category subcategory")
|
||||||
fab!(:category2) { Fabricate(:category) }
|
end
|
||||||
fab!(:category2_subcategory) { Fabricate(:category, parent_category_id: category2.id) }
|
|
||||||
|
fab!(:category_subcategory2) do
|
||||||
|
Fabricate(:category, parent_category_id: category.id, name: "category subcategory 2")
|
||||||
|
end
|
||||||
|
|
||||||
|
fab!(:category2) { Fabricate(:category, name: "category2") }
|
||||||
|
|
||||||
|
fab!(:category2_subcategory) do
|
||||||
|
Fabricate(:category, parent_category_id: category2.id, name: "category2 subcategory")
|
||||||
|
end
|
||||||
|
|
||||||
let(:sidebar) { PageObjects::Components::Sidebar.new }
|
let(:sidebar) { PageObjects::Components::Sidebar.new }
|
||||||
|
|
||||||
@ -25,6 +34,14 @@ RSpec.describe "Editing sidebar categories navigation", type: :system do
|
|||||||
modal = sidebar.click_edit_categories_button
|
modal = sidebar.click_edit_categories_button
|
||||||
|
|
||||||
expect(modal).to have_right_title(I18n.t("js.sidebar.categories_form.title"))
|
expect(modal).to have_right_title(I18n.t("js.sidebar.categories_form.title"))
|
||||||
|
expect(modal).to have_parent_category_color(category)
|
||||||
|
expect(modal).to have_category_description_excerpt(category)
|
||||||
|
expect(modal).to have_parent_category_color(category2)
|
||||||
|
expect(modal).to have_category_description_excerpt(category2)
|
||||||
|
|
||||||
|
expect(modal).to have_categories(
|
||||||
|
[category, category_subcategory, category_subcategory2, category2, category2_subcategory],
|
||||||
|
)
|
||||||
|
|
||||||
modal
|
modal
|
||||||
.toggle_category_checkbox(category)
|
.toggle_category_checkbox(category)
|
||||||
@ -54,19 +71,56 @@ RSpec.describe "Editing sidebar categories navigation", type: :system do
|
|||||||
expect(sidebar).to have_no_section_link(category2.name)
|
expect(sidebar).to have_no_section_link(category2.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "allows a user to filter the categories in the modal by the category's name" do
|
||||||
|
visit "/latest"
|
||||||
|
|
||||||
|
expect(sidebar).to have_categories_section
|
||||||
|
|
||||||
|
modal = sidebar.click_edit_categories_button
|
||||||
|
|
||||||
|
modal.filter("category subcategory 2")
|
||||||
|
|
||||||
|
expect(modal).to have_categories([category, category_subcategory2])
|
||||||
|
|
||||||
|
modal.filter("2")
|
||||||
|
|
||||||
|
expect(modal).to have_categories(
|
||||||
|
[category, category_subcategory2, category2, category2_subcategory],
|
||||||
|
)
|
||||||
|
|
||||||
|
modal.filter("someinvalidterm")
|
||||||
|
|
||||||
|
expect(modal).to have_no_categories
|
||||||
|
end
|
||||||
|
|
||||||
describe "when max_category_nesting has been set to 3" do
|
describe "when max_category_nesting has been set to 3" do
|
||||||
before { SiteSetting.max_category_nesting = 3 }
|
before_all { SiteSetting.max_category_nesting = 3 }
|
||||||
|
|
||||||
|
fab!(:category_subcategory_subcategory) do
|
||||||
|
Fabricate(
|
||||||
|
:category,
|
||||||
|
parent_category_id: category_subcategory.id,
|
||||||
|
name: "category subcategory subcategory",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
fab!(:category_subcategory_subcategory2) do
|
||||||
|
Fabricate(
|
||||||
|
:category,
|
||||||
|
parent_category_id: category_subcategory.id,
|
||||||
|
name: "category subcategory subcategory 2",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
fab!(:category2_subcategory_subcategory) do
|
||||||
|
Fabricate(
|
||||||
|
:category,
|
||||||
|
parent_category_id: category2_subcategory.id,
|
||||||
|
name: "category2 subcategory subcategory",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
it "allows a user to edit sub-subcategories to be included in the sidebar categories section" do
|
it "allows a user to edit sub-subcategories to be included in the sidebar categories section" do
|
||||||
category_subcategory_subcategory =
|
|
||||||
Fabricate(:category, parent_category_id: category_subcategory.id)
|
|
||||||
|
|
||||||
category_subcategory_subcategory2 =
|
|
||||||
Fabricate(:category, parent_category_id: category_subcategory.id)
|
|
||||||
|
|
||||||
category2_subcategory_subcategory =
|
|
||||||
Fabricate(:category, parent_category_id: category2_subcategory.id)
|
|
||||||
|
|
||||||
visit "/latest"
|
visit "/latest"
|
||||||
|
|
||||||
expect(sidebar).to have_categories_section
|
expect(sidebar).to have_categories_section
|
||||||
@ -87,5 +141,18 @@ RSpec.describe "Editing sidebar categories navigation", type: :system do
|
|||||||
expect(sidebar).to have_section_link(category_subcategory_subcategory2.name)
|
expect(sidebar).to have_section_link(category_subcategory_subcategory2.name)
|
||||||
expect(sidebar).to have_section_link(category2_subcategory_subcategory.name)
|
expect(sidebar).to have_section_link(category2_subcategory_subcategory.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "allows a user to filter the categories in the modal by the category's name" do
|
||||||
|
visit "/latest"
|
||||||
|
|
||||||
|
expect(sidebar).to have_categories_section
|
||||||
|
|
||||||
|
modal = sidebar.click_edit_categories_button
|
||||||
|
modal.filter("category2 subcategory subcategory")
|
||||||
|
|
||||||
|
expect(modal).to have_categories(
|
||||||
|
[category2, category2_subcategory, category2_subcategory_subcategory],
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -3,26 +3,67 @@
|
|||||||
module PageObjects
|
module PageObjects
|
||||||
module Modals
|
module Modals
|
||||||
class SidebarEditCategories < PageObjects::Modals::Base
|
class SidebarEditCategories < PageObjects::Modals::Base
|
||||||
MODAL_SELECTOR = ".sidebar-categories-form-modal"
|
|
||||||
|
|
||||||
def closed?
|
def closed?
|
||||||
has_no_css?(MODAL_SELECTOR)
|
has_no_css?(".sidebar-categories-form-modal")
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_right_title?(title)
|
def has_right_title?(title)
|
||||||
has_css?("#{MODAL_SELECTOR} #discourse-modal-title", text: title)
|
has_css?(".sidebar-categories-form-modal #discourse-modal-title", text: title)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_parent_category_color?(category)
|
||||||
|
has_css?(
|
||||||
|
".sidebar-categories-form-modal .sidebar-categories-form__row",
|
||||||
|
style: "border-left-color: ##{category.color} ",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_category_description_excerpt?(category)
|
||||||
|
has_css?(
|
||||||
|
".sidebar-categories-form-modal .sidebar-categories-form__category-row",
|
||||||
|
text: category.description_excerpt,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_no_categories?
|
||||||
|
has_no_css?(".sidebar-categories-form-modal .sidebar-categories-form__category-row") &&
|
||||||
|
has_css?(
|
||||||
|
".sidebar-categories-form-modal .sidebar-categories-form__no-categories",
|
||||||
|
text: I18n.t("js.sidebar.categories_form.no_categories"),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_categories?(categories)
|
||||||
|
category_ids = categories.map(&:id)
|
||||||
|
|
||||||
|
has_css?(
|
||||||
|
".sidebar-categories-form-modal .sidebar-categories-form__category-row",
|
||||||
|
count: category_ids.length,
|
||||||
|
) &&
|
||||||
|
all(".sidebar-categories-form-modal .sidebar-categories-form__category-row").all? do |row|
|
||||||
|
category_ids.include?(row["data-category-id"].to_i)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def toggle_category_checkbox(category)
|
def toggle_category_checkbox(category)
|
||||||
find(
|
find(
|
||||||
"#{MODAL_SELECTOR} .sidebar-categories-form__category-row[data-category-id='#{category.id}'] .sidebar-categories-form__input",
|
".sidebar-categories-form-modal .sidebar-categories-form__category-row[data-category-id='#{category.id}'] .sidebar-categories-form__input",
|
||||||
).click
|
).click
|
||||||
|
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def save
|
def save
|
||||||
find("#{MODAL_SELECTOR} .sidebar-categories-form__save-button").click
|
find(".sidebar-categories-form-modal .sidebar-categories-form__save-button").click
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter(text)
|
||||||
|
find(".sidebar-categories-form-modal .sidebar-categories-form__filter-input-field").fill_in(
|
||||||
|
with: text,
|
||||||
|
)
|
||||||
|
|
||||||
|
self
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Reference in New Issue
Block a user