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:
parent
e48750281e
commit
853bce2abc
|
@ -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>
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
@import "sidebar-categories-form";
|
||||
@import "user-card";
|
||||
@import "user-info";
|
||||
@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 "user-card";
|
||||
@import "user-stream-item";
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
.sidebar-categories-form-modal {
|
||||
.modal-inner-container {
|
||||
width: 35em;
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue