UX: Allow users to filter categories in edit sidebar categories modal (#21996)

What does this change do?

This change is a continuation of
2191b879c6 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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 281 additions and 58 deletions

View File

@ -3,44 +3,65 @@
@class="sidebar-categories-form-modal"
>
<form class="sidebar-categories-form">
{{#each this.categoryGroupings as |categories|}}
<div
class="sidebar-categories-form__row"
style={{html-safe (border-color categories.1.color "left")}}
>
<div class="sidebar-categories-form__filter">
{{d-icon "search" class="sidebar-categories-form__filter-input-icon"}}
{{#each categories as |category|}}
<div
class="sidebar-categories-form__category-row"
data-category-id={{category.id}}
data-category-level={{category.level}}
>
<label
class="sidebar-categories-form__category-label"
for={{concat "sidebar-categories-form__input--" category.id}}
<Input
class="sidebar-categories-form__filter-input-field"
placeholder={{i18n "sidebar.categories_form.filter_placeholder"}}
@type="text"
@value={{this.filter}}
{{on "input" (action "onFilterInput" value="target.value")}}
/>
</div>
{{#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">
{{category-badge category}}
</div>
{{#unless category.parentCategory}}
<div class="sidebar-categories-form__category-description">
{{dir-span category.description_excerpt htmlSafe="true"}}
<label
class="sidebar-categories-form__category-label"
for={{concat "sidebar-categories-form__input--" category.id}}
>
<div class="sidebar-categories-form__category-badge">
{{category-badge category}}
</div>
{{/unless}}
</label>
<Input
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}}
{{#unless category.parentCategory}}
<div class="sidebar-categories-form__category-description">
{{dir-span category.description_excerpt htmlSafe="true"}}
</div>
{{/unless}}
</label>
<Input
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>
{{/each}}
{{/if}}
</form>
</DModalBody>

View File

@ -3,12 +3,16 @@ import { inject as service } from "@ember/service";
import Component from "@glimmer/component";
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";
export default class extends Component {
@service site;
@service currentUser;
@tracked filter = "";
@tracked selectedSidebarCategoryIds = [
...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
toggleCategory(categoryId) {
if (this.selectedSidebarCategoryIds.includes(categoryId)) {

View File

@ -134,7 +134,7 @@
}
&: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;
@media screen and (max-height: 500px) {
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__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 {
display: flex;
flex-direction: column;
@ -21,7 +56,7 @@
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);
}

View File

@ -1,3 +1,4 @@
@import "sidebar-categories-form";
@import "user-card";
@import "user-info";
@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 "user-card";
@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:
save: "Save"
title: "Edit categories navigation"
filter_input:
placeholder: "Filter categories"
filter_placeholder: "Filter categories"
no_categories: "There are no categories matching the given term."
sections:
custom:

View File

@ -3,11 +3,20 @@
RSpec.describe "Editing sidebar categories navigation", type: :system do
fab!(:user) { Fabricate(:user) }
fab!(:group) { Fabricate(:group).tap { |g| g.add(user) } }
fab!(:category) { Fabricate(:category) }
fab!(:category_subcategory) { Fabricate(:category, parent_category_id: category.id) }
fab!(:category_subcategory2) { Fabricate(:category, parent_category_id: category.id) }
fab!(:category2) { Fabricate(:category) }
fab!(:category2_subcategory) { Fabricate(:category, parent_category_id: category2.id) }
fab!(:category) { Fabricate(:category, name: "category") }
fab!(:category_subcategory) do
Fabricate(:category, parent_category_id: category.id, name: "category subcategory")
end
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 }
@ -25,6 +34,14 @@ RSpec.describe "Editing sidebar categories navigation", type: :system do
modal = sidebar.click_edit_categories_button
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
.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)
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
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
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"
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(category2_subcategory_subcategory.name)
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

View File

@ -3,26 +3,67 @@
module PageObjects
module Modals
class SidebarEditCategories < PageObjects::Modals::Base
MODAL_SELECTOR = ".sidebar-categories-form-modal"
def closed?
has_no_css?(MODAL_SELECTOR)
has_no_css?(".sidebar-categories-form-modal")
end
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
def toggle_category_checkbox(category)
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
self
end
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