FEATURE: Add dropdown to filter by selected in edit nav menu modal (#22251)

What does this change do?

This change adds a dropdown filter that allows a user to filter by
selected or unselected categories/tags in the edit navigation menu
modal.

For the categories modal, parent categories that do not match the
dropdown filter will be displayed as disabled since those parent
categories need to be displayed to maintain the hieracy of the child
child categories.
This commit is contained in:
Alan Guo Xiang Tan 2023-06-23 10:29:00 +08:00 committed by GitHub
parent 2dd9ac6277
commit 303fcf303c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 295 additions and 31 deletions

View File

@ -12,6 +12,9 @@
"sidebar.categories_form_modal.filter_placeholder" "sidebar.categories_form_modal.filter_placeholder"
}} }}
@onFilterInput={{this.onFilterInput}} @onFilterInput={{this.onFilterInput}}
@resetFilter={{this.resetFilter}}
@filterSelected={{this.filterSelected}}
@filterUnselected={{this.filterUnselected}}
> >
<form class="sidebar-categories-form"> <form class="sidebar-categories-form">
{{#if (gt this.filteredCategoriesGroupings.length 0)}} {{#if (gt this.filteredCategoriesGroupings.length 0)}}
@ -50,6 +53,9 @@
this.selectedSidebarCategoryIds this.selectedSidebarCategoryIds
category.id category.id
}} }}
disabled={{(not
(includes this.filteredCategoryIds category.id)
)}}
{{on "click" (action "toggleCategory" category.id)}} {{on "click" (action "toggleCategory" category.id)}}
/> />
</div> </div>

View File

@ -13,6 +13,9 @@ export default class extends Component {
@service siteSettings; @service siteSettings;
@tracked filter = ""; @tracked filter = "";
@tracked filteredCategoryIds;
@tracked onlySelected = false;
@tracked onlyUnselected = false;
@tracked selectedSidebarCategoryIds = [ @tracked selectedSidebarCategoryIds = [
...this.currentUser.sidebar_category_ids, ...this.currentUser.sidebar_category_ids,
@ -44,37 +47,71 @@ export default class extends Component {
} }
get filteredCategoriesGroupings() { get filteredCategoriesGroupings() {
if (this.filter.length === 0) { const filteredCategoryIds = new Set();
return this.categoryGroupings;
} else {
return this.categoryGroupings.reduce((acc, categoryGrouping) => {
const filteredCategories = new Set();
categoryGrouping.forEach((category) => { const groupings = this.categoryGroupings.reduce((acc, categoryGrouping) => {
if (this.#matchesFilter(category, this.filter)) { const filteredCategories = new Set();
if (category.parentCategory?.parentCategory) {
filteredCategories.add(category.parentCategory.parentCategory);
}
if (category.parentCategory) { const addCategory = (category) => {
filteredCategories.add(category.parentCategory); if (this.#matchesFilter(category)) {
} if (category.parentCategory?.parentCategory) {
filteredCategories.add(category.parentCategory.parentCategory);
filteredCategories.add(category);
} }
});
if (filteredCategories.size > 0) { if (category.parentCategory) {
acc.push(Array.from(filteredCategories)); filteredCategories.add(category.parentCategory);
}
filteredCategoryIds.add(category.id);
filteredCategories.add(category);
} }
};
return acc; categoryGrouping.forEach((category) => {
}, []); if (this.onlySelected) {
} if (this.selectedSidebarCategoryIds.includes(category.id)) {
addCategory(category);
}
} else if (this.onlyUnselected) {
if (!this.selectedSidebarCategoryIds.includes(category.id)) {
addCategory(category);
}
} else {
addCategory(category);
}
});
if (filteredCategories.size > 0) {
acc.push(Array.from(filteredCategories));
}
return acc;
}, []);
this.filteredCategoryIds = Array.from(filteredCategoryIds);
return groupings;
} }
#matchesFilter(category, filter) { #matchesFilter(category) {
return category.nameLower.includes(filter); return this.filter.length === 0 || category.nameLower.includes(this.filter);
}
@action
resetFilter() {
this.onlySelected = false;
this.onlyUnselected = false;
}
@action
filterSelected() {
this.onlySelected = true;
this.onlyUnselected = false;
}
@action
filterUnselected() {
this.onlySelected = false;
this.onlyUnselected = true;
} }
@action @action

View File

@ -32,6 +32,16 @@
autofocus="true" autofocus="true"
/> />
</div> </div>
<div class="sidebar__edit-navigation-modal-form__filter-dropdown-wrapper">
<DropdownSelectBox
@class="sidebar__edit-navigation-modal-form__filter-dropdown"
@value={{this.filterDropdownValue}}
@content={{this.filterDropdownContent}}
@onChange={{this.onFilterDropdownChange}}
@options={{hash showCaret=true}}
/>
</div>
</div> </div>
{{yield}} {{yield}}

View File

@ -1,9 +1,30 @@
import I18n from "I18n";
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking"; import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object"; import { action } from "@ember/object";
export default class extends Component { export default class extends Component {
@tracked filter = ""; @tracked filter = "";
@tracked filterDropdownValue = "all";
filterDropdownContent = [
{
id: "all",
name: I18n.t("sidebar.edit_navigation_modal_form.filter_dropdown.all"),
},
{
id: "selected",
name: I18n.t(
"sidebar.edit_navigation_modal_form.filter_dropdown.selected"
),
},
{
id: "unselected",
name: I18n.t(
"sidebar.edit_navigation_modal_form.filter_dropdown.unselected"
),
},
];
get modalHeaderAfterTitleElement() { get modalHeaderAfterTitleElement() {
return document.getElementById("modal-header-after-title"); return document.getElementById("modal-header-after-title");
@ -13,4 +34,21 @@ export default class extends Component {
onFilterInput(value) { onFilterInput(value) {
this.args.onFilterInput(value); this.args.onFilterInput(value);
} }
@action
onFilterDropdownChange(value) {
this.filterDropdownValue = value;
switch (value) {
case "all":
this.args.resetFilter();
break;
case "selected":
this.args.filterSelected();
break;
case "unselected":
this.args.filterUnselected();
break;
}
}
} }

View File

@ -10,6 +10,9 @@
@deselectAllText={{i18n "sidebar.tags_form_modal.subtitle.text"}} @deselectAllText={{i18n "sidebar.tags_form_modal.subtitle.text"}}
@inputFilterPlaceholder={{i18n "sidebar.tags_form_modal.filter_placeholder"}} @inputFilterPlaceholder={{i18n "sidebar.tags_form_modal.filter_placeholder"}}
@onFilterInput={{this.onFilterInput}} @onFilterInput={{this.onFilterInput}}
@resetFilter={{this.resetFilter}}
@filterSelected={{this.filterSelected}}
@filterUnselected={{this.filterUnselected}}
> >
<form class="sidebar-tags-form"> <form class="sidebar-tags-form">
{{#if this.tagsLoading}} {{#if this.tagsLoading}}

View File

@ -13,6 +13,8 @@ export default class extends Component {
@service store; @service store;
@tracked filter = ""; @tracked filter = "";
@tracked onlySelected = false;
@tracked onlyUnSelected = false;
@tracked tags = []; @tracked tags = [];
@tracked tagsLoading = true; @tracked tagsLoading = true;
@tracked selectedTags = [...this.currentUser.sidebarTagNames]; @tracked selectedTags = [...this.currentUser.sidebarTagNames];
@ -40,17 +42,45 @@ export default class extends Component {
} }
get filteredTags() { get filteredTags() {
if (this.filter.length === 0) { return this.tags.reduce((acc, tag) => {
return this.tags; if (this.onlySelected) {
} else { if (this.selectedTags.includes(tag.name) && this.#matchesFilter(tag)) {
return this.tags.reduce((acc, tag) => {
if (tag.name.toLowerCase().includes(this.filter)) {
acc.push(tag); acc.push(tag);
} }
} else if (this.onlyUnselected) {
if (!this.selectedTags.includes(tag.name) && this.#matchesFilter(tag)) {
acc.push(tag);
}
} else if (this.#matchesFilter(tag)) {
acc.push(tag);
}
return acc; return acc;
}, []); }, []);
} }
#matchesFilter(tag) {
return (
this.filter.length === 0 || tag.name.toLowerCase().includes(this.filter)
);
}
@action
resetFilter() {
this.onlySelected = false;
this.onlyUnselected = false;
}
@action
filterSelected() {
this.onlySelected = true;
this.onlyUnselected = false;
}
@action
filterUnselected() {
this.onlySelected = false;
this.onlyUnselected = true;
} }
@action @action

View File

@ -9,6 +9,27 @@
margin-bottom: 1em; margin-bottom: 1em;
width: 100%; width: 100%;
.sidebar__edit-navigation-modal-form__filter-dropdown {
margin-left: 0.5em;
.select-kit-header {
background: var(--secondary);
color: var(--primary);
border: 1px solid var(--primary-low-mid);
font-size: var(--font-0);
&:hover,
&:focus {
background: var(--secondary);
color: var(--primary);
.d-icon {
color: var(--primary);
}
}
}
}
.sidebar__edit-navigation-modal-form__filter-input { .sidebar__edit-navigation-modal-form__filter-input {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -2,4 +2,13 @@
.modal-inner-container { .modal-inner-container {
width: 35em; width: 35em;
} }
.sidebar__edit-navigation-modal-form__filter {
flex-direction: column;
.sidebar__edit-navigation-modal-form__filter-dropdown {
margin-left: 0;
width: 100%;
}
}
} }

View File

@ -135,6 +135,52 @@ RSpec.describe "Editing sidebar categories navigation", type: :system do
expect(modal).to have_no_categories expect(modal).to have_no_categories
end end
it "allows a user to filter the categories in the modal by selection" do
Fabricate(:category_sidebar_section_link, linkable: category_subcategory, user: user)
Fabricate(:category_sidebar_section_link, linkable: category2, user: user)
visit "/latest"
expect(sidebar).to have_categories_section
modal = sidebar.click_edit_categories_button
modal.filter_by_selected
expect(modal).to have_categories([category, category_subcategory, category2])
expect(modal).to have_checkbox(category, disabled: true)
expect(modal).to have_checkbox(category_subcategory)
expect(modal).to have_checkbox(category2)
modal.filter("category subcategory")
expect(modal).to have_categories([category, category_subcategory])
expect(modal).to have_checkbox(category, disabled: true)
expect(modal).to have_checkbox(category_subcategory)
modal.filter("").filter_by_unselected
expect(modal).to have_categories(
[category, category_subcategory2, category2, category2_subcategory],
)
expect(modal).to have_checkbox(category)
expect(modal).to have_checkbox(category_subcategory2)
expect(modal).to have_checkbox(category2, disabled: true)
expect(modal).to have_checkbox(category2_subcategory)
modal.filter_by_all
expect(modal).to have_categories(
[category, category_subcategory, category_subcategory2, category2, category2_subcategory],
)
expect(modal).to have_checkbox(category)
expect(modal).to have_checkbox(category_subcategory)
expect(modal).to have_checkbox(category_subcategory2)
expect(modal).to have_checkbox(category2)
expect(modal).to have_checkbox(category2_subcategory)
end
describe "when max_category_nesting has been set to 3" do describe "when max_category_nesting has been set to 3" do
before_all { SiteSetting.max_category_nesting = 3 } before_all { SiteSetting.max_category_nesting = 3 }

View File

@ -118,4 +118,30 @@ RSpec.describe "Editing sidebar tags navigation", type: :system do
expect(sidebar).to have_section_link(tag2.name) expect(sidebar).to have_section_link(tag2.name)
expect(sidebar).to have_section_link(tag3.name) expect(sidebar).to have_section_link(tag3.name)
end end
it "allows a user to filter the tag in the modal by selection" do
Fabricate(:tag_sidebar_section_link, linkable: tag1, user: user)
Fabricate(:tag_sidebar_section_link, linkable: tag2, user: user)
visit "/latest"
expect(sidebar).to have_tags_section
modal = sidebar.click_edit_tags_button
modal.filter_by_selected
expect(modal).to have_tag_checkboxes([tag1, tag2])
modal.filter("tag2")
expect(modal).to have_tag_checkboxes([tag2])
modal.filter("").filter_by_unselected
expect(modal).to have_tag_checkboxes([tag3])
modal.filter_by_all
expect(modal).to have_tag_checkboxes([tag1, tag2, tag3])
end
end end

View File

@ -60,6 +60,12 @@ module PageObjects
self self
end end
def has_checkbox?(category, disabled: false)
has_selector?(
".sidebar-categories-form-modal .sidebar-categories-form__category-row[data-category-id='#{category.id}'] .sidebar-categories-form__input#{disabled ? "[disabled]" : ""}",
)
end
end end
end end
end end

View File

@ -31,6 +31,38 @@ module PageObjects
click_button(I18n.t("js.sidebar.edit_navigation_modal_form.deselect_button_text")) click_button(I18n.t("js.sidebar.edit_navigation_modal_form.deselect_button_text"))
self self
end end
def filter_by_selected
dropdown_filter.select_row_by_name(
I18n.t("js.sidebar.edit_navigation_modal_form.filter_dropdown.selected"),
)
self
end
def filter_by_unselected
dropdown_filter.select_row_by_name(
I18n.t("js.sidebar.edit_navigation_modal_form.filter_dropdown.unselected"),
)
self
end
def filter_by_all
dropdown_filter.select_row_by_name(
I18n.t("js.sidebar.edit_navigation_modal_form.filter_dropdown.all"),
)
self
end
private
def dropdown_filter
PageObjects::Components::SelectKit.new(
".sidebar__edit-navigation-modal-form__filter-dropdown",
)
end
end end
end end
end end