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:
Alan Guo Xiang Tan
2023-06-08 13:54:51 +09:00
committed by GitHub
parent e48750281e
commit 853bce2abc
11 changed files with 281 additions and 58 deletions

View File

@ -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>

View File

@ -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)) {

View File

@ -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;

View File

@ -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);
} }

View File

@ -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";

View File

@ -0,0 +1,5 @@
.sidebar-categories-form-modal {
.modal-inner-container {
min-width: var(--modal-max-width);
}
}

View File

@ -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";

View File

@ -0,0 +1,5 @@
.sidebar-categories-form-modal {
.modal-inner-container {
width: 35em;
}
}

View File

@ -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:

View File

@ -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

View File

@ -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