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:
parent
2dd9ac6277
commit
303fcf303c
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue