UX: First pass at edit categories navigation modal for sidebar (#21963)
What this change? We are currently not fully satisfied with the current way to edit the categories and tags that appears in the sidebar where the user is redirected to the tracking preferences tab in the user's profile causing the user to lose context of the current page. In addition, the dropdown to select categories or tags limits the amount of information we can display. Since editing or adding a custom categories section is already using a modal, we have decided to switch editing the categories and tags that appear in the sidebar to use a modal as well. This commit ships a first pass of the edit categories modal such that we can keep the commit small and reviewable. The incomplete nature of the feature is also reflected in the fact that the feature is hidden behind a new `new_edit_sidebar_categories_tags_interface_groups` site setting.
This commit is contained in:
parent
213d9dbe41
commit
fc296b9a81
|
@ -0,0 +1,81 @@
|
||||||
|
<DModalBody
|
||||||
|
@title="sidebar.categories_form.title"
|
||||||
|
@class="sidebar-categories-form-modal"
|
||||||
|
>
|
||||||
|
<form class="sidebar-categories-form">
|
||||||
|
{{#each this.siteCategories as |category|}}
|
||||||
|
<div
|
||||||
|
class="sidebar-categories-form__row"
|
||||||
|
style={{html-safe (border-color category.color "left")}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="sidebar-categories-form__category-row"
|
||||||
|
data-category-id={{category.id}}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="sidebar-categories-form__category-label"
|
||||||
|
for={{concat "sidebar-categories-form__input--" category.id}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="sidebar-categories-form__category-badge sidebar-categories-form__category-badge--parent-category"
|
||||||
|
>
|
||||||
|
{{category-badge category}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-categories-form__category-description">
|
||||||
|
{{dir-span category.description_excerpt htmlSafe="true"}}
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{{#if (gt category.subcategories.length 0)}}
|
||||||
|
{{#each category.subcategories as |subcategory|}}
|
||||||
|
<div
|
||||||
|
class="sidebar-categories-form__category-row"
|
||||||
|
data-category-id={{subcategory.id}}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="sidebar-categories-form__category-label"
|
||||||
|
for={{concat "sidebar-categories-form__input--" subcategory.id}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="sidebar-categories-form__category-badge sidebar-categories-form__category-badge--subcategory"
|
||||||
|
>
|
||||||
|
{{category-badge subcategory}}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id={{concat "sidebar-categories-form__input--" subcategory.id}}
|
||||||
|
class="sidebar-categories-form__input"
|
||||||
|
@type="checkbox"
|
||||||
|
@checked={{includes
|
||||||
|
this.selectedSidebarCategoryIds
|
||||||
|
subcategory.id
|
||||||
|
}}
|
||||||
|
{{on "click" (action "toggleCategory" subcategory.id)}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</form>
|
||||||
|
</DModalBody>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<DButton
|
||||||
|
@class="btn-primary sidebar-categories-form__save-button"
|
||||||
|
@label="sidebar.categories_form.save"
|
||||||
|
@disabled={{this.saving}}
|
||||||
|
@action={{this.save}}
|
||||||
|
/>
|
||||||
|
</div>
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
|
||||||
|
export default class extends Component {
|
||||||
|
@service site;
|
||||||
|
@service currentUser;
|
||||||
|
|
||||||
|
@tracked selectedSidebarCategoryIds = [
|
||||||
|
...this.currentUser.sidebar_category_ids,
|
||||||
|
];
|
||||||
|
|
||||||
|
@tracked siteCategories = this.site.categoriesList.filter((category) => {
|
||||||
|
return !category.parent_category_id && !category.isUncategorizedCategory;
|
||||||
|
});
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleCategory(categoryId) {
|
||||||
|
if (this.selectedSidebarCategoryIds.includes(categoryId)) {
|
||||||
|
this.selectedSidebarCategoryIds.removeObject(categoryId);
|
||||||
|
} else {
|
||||||
|
this.selectedSidebarCategoryIds.addObject(categoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
save() {
|
||||||
|
this.saving = true;
|
||||||
|
const initialSidebarCategoryIds = this.currentUser.sidebar_category_ids;
|
||||||
|
|
||||||
|
this.currentUser.set(
|
||||||
|
"sidebar_category_ids",
|
||||||
|
this.selectedSidebarCategoryIds
|
||||||
|
);
|
||||||
|
|
||||||
|
this.currentUser
|
||||||
|
.save(["sidebar_category_ids"])
|
||||||
|
.then(() => {
|
||||||
|
this.args.closeModal();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.currentUser.set("sidebar_category_ids", initialSidebarCategoryIds);
|
||||||
|
popupAjaxError(error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.saving = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,6 +33,20 @@
|
||||||
data-category-id={{sectionLink.category.id}}
|
data-category-id={{sectionLink.category.id}}
|
||||||
/>
|
/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
{{else if this.currentUser.new_edit_sidebar_categories_tags_interface_groups_enabled}}
|
||||||
|
<Sidebar::SectionLink
|
||||||
|
@linkName="configure-categories"
|
||||||
|
@prefixType="icon"
|
||||||
|
@prefixValue="pencil-alt"
|
||||||
|
@model={{this.currentUser}}
|
||||||
|
@content={{i18n
|
||||||
|
"sidebar.sections.categories.links.add_categories.content"
|
||||||
|
}}
|
||||||
|
@title={{i18n
|
||||||
|
"sidebar.sections.categories.links.add_categories.title"
|
||||||
|
}}
|
||||||
|
{{on "click" this.editTracked}}
|
||||||
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
<Sidebar::SectionLink
|
<Sidebar::SectionLink
|
||||||
@linkName="configure-categories"
|
@linkName="configure-categories"
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { cached } from "@glimmer/tracking";
|
||||||
import { debounce } from "discourse-common/utils/decorators";
|
import { debounce } from "discourse-common/utils/decorators";
|
||||||
import Category from "discourse/models/category";
|
import Category from "discourse/models/category";
|
||||||
import SidebarCommonCategoriesSection from "discourse/components/sidebar/common/categories-section";
|
import SidebarCommonCategoriesSection from "discourse/components/sidebar/common/categories-section";
|
||||||
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
|
||||||
export const REFRESH_COUNTS_APP_EVENT_NAME =
|
export const REFRESH_COUNTS_APP_EVENT_NAME =
|
||||||
"sidebar:refresh-categories-section-counts";
|
"sidebar:refresh-categories-section-counts";
|
||||||
|
@ -74,6 +75,12 @@ export default class SidebarUserCategoriesSection extends SidebarCommonCategorie
|
||||||
|
|
||||||
@action
|
@action
|
||||||
editTracked() {
|
editTracked() {
|
||||||
|
if (
|
||||||
|
this.currentUser.new_edit_sidebar_categories_tags_interface_groups_enabled
|
||||||
|
) {
|
||||||
|
showModal("sidebar-categories-form");
|
||||||
|
} else {
|
||||||
this.router.transitionTo("preferences.sidebar", this.currentUser);
|
this.router.transitionTo("preferences.sidebar", this.currentUser);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import Controller from "@ember/controller";
|
||||||
|
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||||
|
|
||||||
|
export default class SidebarCategoriesForm extends Controller.extend(
|
||||||
|
ModalFunctionality
|
||||||
|
) {}
|
|
@ -1,3 +1,14 @@
|
||||||
import { htmlHelper } from "discourse-common/lib/helpers";
|
import { htmlHelper } from "discourse-common/lib/helpers";
|
||||||
|
|
||||||
export default htmlHelper((color) => `border-color: #${color}; `);
|
const validDirections = ["top", "right", "bottom", "left"];
|
||||||
|
|
||||||
|
export default htmlHelper((color, direction) => {
|
||||||
|
const borderColor = `#${color}`;
|
||||||
|
|
||||||
|
const borderProperty =
|
||||||
|
direction && validDirections.includes(direction)
|
||||||
|
? `border-${direction}-color`
|
||||||
|
: "border-color";
|
||||||
|
|
||||||
|
return `${borderProperty}: ${borderColor} `;
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<Sidebar::CategoriesFormModal @closeModal={{(action "closeModal")}} />
|
|
@ -27,6 +27,7 @@
|
||||||
@import "relative-time-picker";
|
@import "relative-time-picker";
|
||||||
@import "share-and-invite-modal";
|
@import "share-and-invite-modal";
|
||||||
@import "download-calendar";
|
@import "download-calendar";
|
||||||
|
@import "sidebar-categories-form";
|
||||||
@import "svg";
|
@import "svg";
|
||||||
@import "tap-tile";
|
@import "tap-tile";
|
||||||
@import "time-input";
|
@import "time-input";
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
.sidebar-categories-form {
|
||||||
|
.sidebar-categories-form__row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid var(--primary-low);
|
||||||
|
border-left: 4px solid;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
padding: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-categories-form__input {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 0;
|
||||||
|
align-self: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-categories-form__category-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-right: 1em;
|
||||||
|
padding: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-categories-form__category-row:not(:first-child) {
|
||||||
|
border-top: 1px solid var(--primary-low);
|
||||||
|
|
||||||
|
.sidebar-categories-form__input {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-categories-form__category-row:nth-child(2) {
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-categories-form__category-row:nth-child(n + 3) {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-categories-form__category-label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-categories-form__category-badge {
|
||||||
|
.category-name {
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: var(--font-up-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-categories-form__category-description {
|
||||||
|
color: var(--primary-high);
|
||||||
|
font-size: var(--font-down-1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1827,6 +1827,10 @@ class User < ActiveRecord::Base
|
||||||
in_any_groups?(SiteSetting.experimental_new_new_view_groups_map)
|
in_any_groups?(SiteSetting.experimental_new_new_view_groups_map)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def new_edit_sidebar_categories_tags_interface_groups_enabled?
|
||||||
|
in_any_groups?(SiteSetting.new_edit_sidebar_categories_tags_interface_groups_map)
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def badge_grant
|
def badge_grant
|
||||||
|
|
|
@ -68,7 +68,8 @@ class CurrentUserSerializer < BasicUserSerializer
|
||||||
:sidebar_category_ids,
|
:sidebar_category_ids,
|
||||||
:sidebar_list_destination,
|
:sidebar_list_destination,
|
||||||
:sidebar_sections,
|
:sidebar_sections,
|
||||||
:new_new_view_enabled?
|
:new_new_view_enabled?,
|
||||||
|
:new_edit_sidebar_categories_tags_interface_groups_enabled?
|
||||||
|
|
||||||
delegate :user_stat, to: :object, private: true
|
delegate :user_stat, to: :object, private: true
|
||||||
delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat
|
delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat
|
||||||
|
|
|
@ -4421,6 +4421,12 @@ en:
|
||||||
more: "More"
|
more: "More"
|
||||||
all_categories: "All categories"
|
all_categories: "All categories"
|
||||||
all_tags: "All tags"
|
all_tags: "All tags"
|
||||||
|
categories_form:
|
||||||
|
save: "Save"
|
||||||
|
title: "Edit categories navigation"
|
||||||
|
filter_input:
|
||||||
|
placeholder: "Filter categories"
|
||||||
|
|
||||||
sections:
|
sections:
|
||||||
custom:
|
custom:
|
||||||
add: "Add custom section"
|
add: "Add custom section"
|
||||||
|
|
|
@ -2112,6 +2112,12 @@ developer:
|
||||||
experimental_topics_filter:
|
experimental_topics_filter:
|
||||||
client: true
|
client: true
|
||||||
default: false
|
default: false
|
||||||
|
new_edit_sidebar_categories_tags_interface_groups:
|
||||||
|
type: group_list
|
||||||
|
list_type: compact
|
||||||
|
default: ""
|
||||||
|
allow_any: false
|
||||||
|
hidden: true
|
||||||
|
|
||||||
navigation:
|
navigation:
|
||||||
navigation_menu:
|
navigation_menu:
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
Fabricator(:category) do
|
Fabricator(:category) do
|
||||||
name { sequence(:name) { |n| "Amazing Category #{n}" } }
|
name { sequence(:name) { |n| "Amazing Category #{n}" } }
|
||||||
skip_category_definition true
|
skip_category_definition true
|
||||||
|
color { SecureRandom.hex(3) }
|
||||||
user
|
user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
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) }
|
||||||
|
|
||||||
|
let(:sidebar) { PageObjects::Components::Sidebar.new }
|
||||||
|
|
||||||
|
before do
|
||||||
|
SiteSetting.new_edit_sidebar_categories_tags_interface_groups = group.name
|
||||||
|
SiteSetting.default_sidebar_categories = "#{category.id}|#{category2.id}"
|
||||||
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows a user to edit the sidebar categories navigation" do
|
||||||
|
visit "/latest"
|
||||||
|
|
||||||
|
expect(sidebar).to have_categories_section
|
||||||
|
|
||||||
|
modal = sidebar.click_edit_categories_button
|
||||||
|
|
||||||
|
expect(modal).to have_right_title(I18n.t("js.sidebar.categories_form.title"))
|
||||||
|
|
||||||
|
modal
|
||||||
|
.toggle_category_checkbox(category)
|
||||||
|
.toggle_category_checkbox(category_subcategory2)
|
||||||
|
.toggle_category_checkbox(category2)
|
||||||
|
.save
|
||||||
|
|
||||||
|
expect(modal).to be_closed
|
||||||
|
expect(sidebar).to have_section_link(category.name)
|
||||||
|
expect(sidebar).to have_section_link(category_subcategory2.name)
|
||||||
|
expect(sidebar).to have_section_link(category2.name)
|
||||||
|
|
||||||
|
visit "/latest"
|
||||||
|
|
||||||
|
expect(sidebar).to have_categories_section
|
||||||
|
expect(sidebar).to have_section_link(category.name)
|
||||||
|
expect(sidebar).to have_section_link(category_subcategory2.name)
|
||||||
|
expect(sidebar).to have_section_link(category2.name)
|
||||||
|
|
||||||
|
modal = sidebar.click_edit_categories_button
|
||||||
|
modal.toggle_category_checkbox(category_subcategory2).toggle_category_checkbox(category2).save
|
||||||
|
|
||||||
|
expect(modal).to be_closed
|
||||||
|
|
||||||
|
expect(sidebar).to have_section_link(category.name)
|
||||||
|
expect(sidebar).to have_no_section_link(category_subcategory2.name)
|
||||||
|
expect(sidebar).to have_no_section_link(category2.name)
|
||||||
|
end
|
||||||
|
end
|
|
@ -23,6 +23,14 @@ module PageObjects
|
||||||
page.has_no_button?(add_section_button_text)
|
page.has_no_button?(add_section_button_text)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def click_edit_categories_button
|
||||||
|
within(".sidebar-section[data-section-name='categories']") do
|
||||||
|
click_button(class: "sidebar-section-header-button", visible: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
PageObjects::Modals::SidebarEditCategories.new
|
||||||
|
end
|
||||||
|
|
||||||
def edit_custom_section(name)
|
def edit_custom_section(name)
|
||||||
find(".sidebar-section[data-section-name='#{name.parameterize}']").hover
|
find(".sidebar-section[data-section-name='#{name.parameterize}']").hover
|
||||||
|
|
||||||
|
@ -59,6 +67,10 @@ module PageObjects
|
||||||
find(SIDEBAR_WRAPPER_SELECTOR).has_button?(name)
|
find(SIDEBAR_WRAPPER_SELECTOR).has_button?(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_categories_section?
|
||||||
|
has_section?("Categories")
|
||||||
|
end
|
||||||
|
|
||||||
def has_no_section?(name)
|
def has_no_section?(name)
|
||||||
find(SIDEBAR_WRAPPER_SELECTOR).has_no_button?(name)
|
find(SIDEBAR_WRAPPER_SELECTOR).has_no_button?(name)
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module PageObjects
|
||||||
|
module Modals
|
||||||
|
class SidebarEditCategories < PageObjects::Modals::Base
|
||||||
|
MODAL_SELECTOR = ".sidebar-categories-form-modal"
|
||||||
|
|
||||||
|
def closed?
|
||||||
|
has_no_css?(MODAL_SELECTOR)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_right_title?(title)
|
||||||
|
has_css?("#{MODAL_SELECTOR} #discourse-modal-title", text: title)
|
||||||
|
end
|
||||||
|
|
||||||
|
def toggle_category_checkbox(category)
|
||||||
|
find(
|
||||||
|
"#{MODAL_SELECTOR} .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
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue