mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
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"
|
||||
}}
|
||||
@onFilterInput={{this.onFilterInput}}
|
||||
@resetFilter={{this.resetFilter}}
|
||||
@filterSelected={{this.filterSelected}}
|
||||
@filterUnselected={{this.filterUnselected}}
|
||||
>
|
||||
<form class="sidebar-categories-form">
|
||||
{{#if (gt this.filteredCategoriesGroupings.length 0)}}
|
||||
@ -50,6 +53,9 @@
|
||||
this.selectedSidebarCategoryIds
|
||||
category.id
|
||||
}}
|
||||
disabled={{(not
|
||||
(includes this.filteredCategoryIds category.id)
|
||||
)}}
|
||||
{{on "click" (action "toggleCategory" category.id)}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -13,6 +13,9 @@ export default class extends Component {
|
||||
@service siteSettings;
|
||||
|
||||
@tracked filter = "";
|
||||
@tracked filteredCategoryIds;
|
||||
@tracked onlySelected = false;
|
||||
@tracked onlyUnselected = false;
|
||||
|
||||
@tracked selectedSidebarCategoryIds = [
|
||||
...this.currentUser.sidebar_category_ids,
|
||||
@ -44,37 +47,71 @@ 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();
|
||||
const filteredCategoryIds = new Set();
|
||||
|
||||
categoryGrouping.forEach((category) => {
|
||||
if (this.#matchesFilter(category, this.filter)) {
|
||||
if (category.parentCategory?.parentCategory) {
|
||||
filteredCategories.add(category.parentCategory.parentCategory);
|
||||
}
|
||||
const groupings = this.categoryGroupings.reduce((acc, categoryGrouping) => {
|
||||
const filteredCategories = new Set();
|
||||
|
||||
if (category.parentCategory) {
|
||||
filteredCategories.add(category.parentCategory);
|
||||
}
|
||||
|
||||
filteredCategories.add(category);
|
||||
const addCategory = (category) => {
|
||||
if (this.#matchesFilter(category)) {
|
||||
if (category.parentCategory?.parentCategory) {
|
||||
filteredCategories.add(category.parentCategory.parentCategory);
|
||||
}
|
||||
});
|
||||
|
||||
if (filteredCategories.size > 0) {
|
||||
acc.push(Array.from(filteredCategories));
|
||||
if (category.parentCategory) {
|
||||
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) {
|
||||
return category.nameLower.includes(filter);
|
||||
#matchesFilter(category) {
|
||||
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
|
||||
|
@ -32,6 +32,16 @@
|
||||
autofocus="true"
|
||||
/>
|
||||
</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>
|
||||
|
||||
{{yield}}
|
||||
|
@ -1,9 +1,30 @@
|
||||
import I18n from "I18n";
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
|
||||
export default class extends Component {
|
||||
@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() {
|
||||
return document.getElementById("modal-header-after-title");
|
||||
@ -13,4 +34,21 @@ export default class extends Component {
|
||||
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"}}
|
||||
@inputFilterPlaceholder={{i18n "sidebar.tags_form_modal.filter_placeholder"}}
|
||||
@onFilterInput={{this.onFilterInput}}
|
||||
@resetFilter={{this.resetFilter}}
|
||||
@filterSelected={{this.filterSelected}}
|
||||
@filterUnselected={{this.filterUnselected}}
|
||||
>
|
||||
<form class="sidebar-tags-form">
|
||||
{{#if this.tagsLoading}}
|
||||
|
@ -13,6 +13,8 @@ export default class extends Component {
|
||||
@service store;
|
||||
|
||||
@tracked filter = "";
|
||||
@tracked onlySelected = false;
|
||||
@tracked onlyUnSelected = false;
|
||||
@tracked tags = [];
|
||||
@tracked tagsLoading = true;
|
||||
@tracked selectedTags = [...this.currentUser.sidebarTagNames];
|
||||
@ -40,17 +42,45 @@ export default class extends Component {
|
||||
}
|
||||
|
||||
get filteredTags() {
|
||||
if (this.filter.length === 0) {
|
||||
return this.tags;
|
||||
} else {
|
||||
return this.tags.reduce((acc, tag) => {
|
||||
if (tag.name.toLowerCase().includes(this.filter)) {
|
||||
return this.tags.reduce((acc, tag) => {
|
||||
if (this.onlySelected) {
|
||||
if (this.selectedTags.includes(tag.name) && this.#matchesFilter(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
|
||||
|
@ -9,6 +9,27 @@
|
||||
margin-bottom: 1em;
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -2,4 +2,13 @@
|
||||
.modal-inner-container {
|
||||
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
|
||||
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
|
||||
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(tag3.name)
|
||||
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
|
||||
|
@ -60,6 +60,12 @@ module PageObjects
|
||||
|
||||
self
|
||||
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
|
||||
|
@ -31,6 +31,38 @@ module PageObjects
|
||||
click_button(I18n.t("js.sidebar.edit_navigation_modal_form.deselect_button_text"))
|
||||
self
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user