DEV: Refactor edit tags/categories modal to reduce duplication (#22240)

Why this change?

There was alot of duplication between the edit navigation menu tags/categories modal which
was making it hard to introduce new changes as the work had to be
duplicated into multiple places.

This commit mainly extracts the duplicated code into common components
such that it is easier to make styling changes across both modals.
This commit is contained in:
Alan Guo Xiang Tan 2023-06-23 08:28:55 +08:00 committed by GitHub
parent 92389e5475
commit 6e3f3dff86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 359 additions and 430 deletions

View File

@ -1,103 +0,0 @@
{{#in-element this.modalHeaderAfterTitleElement}}
<p class="sidebar-categories-form__deselect">
<DButton
@class="btn-flat sidebar-categories-form__deselect-all-btn"
@label="sidebar.categories_form_modal.subtitle.button_text"
@ariaLabel="sidebar.categories_form_modal.subtitle.button_text"
@action={{this.deselectAll}}
/>
{{i18n "sidebar.categories_form_modal.subtitle.text"}}
</p>
{{/in-element}}
<div class="sidebar-categories-form-modal">
<DModalBody
@title="sidebar.categories_form_modal.title"
@class="sidebar-categories-form__modal-body"
>
<form class="sidebar-categories-form">
<div class="sidebar-categories-form__filter">
{{d-icon "search" class="sidebar-categories-form__filter-input-icon"}}
<Input
class="sidebar-categories-form__filter-input-field"
placeholder={{i18n
"sidebar.categories_form_modal.filter_placeholder"
}}
@type="text"
@value={{this.filter}}
{{on "input" (action "onFilterInput" value="target.value")}}
autofocus="true"
/>
</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}}
>
<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 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_modal.no_categories"}}
</div>
{{/if}}
</form>
</DModalBody>
<div class="modal-footer sidebar-categories-form__modal-footer">
<DButton
@class="btn-primary sidebar-categories-form__save-button"
@label="save"
@disabled={{this.saving}}
@action={{this.save}}
/>
{{#if (gt this.siteSettings.default_navigation_menu_categories.length 0)}}
<DButton
@icon="undo"
@class="btn-flat btn-text sidebar-categories-form__reset-to-defaults-btn"
@label="sidebar.categories_form_modal.reset_to_defaults"
@disabled={{this.saving}}
@action={{this.resetToDefaults}}
/>
{{/if}}
</div>
</div>

View File

@ -0,0 +1,65 @@
<Sidebar::EditNavigationModalForm::Modal
@title="sidebar.categories_form_modal.title"
@disableSaveButton={{this.saving}}
@save={{this.save}}
@showResetDefaultsButton={{(gt
this.siteSettings.default_navigation_menu_categories.length 0
)}}
@resetToDefaults={{this.resetToDefaults}}
@deselectAll={{this.deselectAll}}
@deselectAllText={{i18n "sidebar.categories_form_modal.subtitle.text"}}
@inputFilterPlaceholder={{i18n
"sidebar.categories_form_modal.filter_placeholder"
}}
@onFilterInput={{this.onFilterInput}}
>
<form class="sidebar-categories-form">
{{#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}}
>
<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 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_modal.no_categories"}}
</div>
{{/if}}
</form>
</Sidebar::EditNavigationModalForm::Modal>

View File

@ -100,10 +100,6 @@ export default class extends Component {
}
}
get modalHeaderAfterTitleElement() {
return document.getElementById("modal-header-after-title");
}
@action
resetToDefaults() {
this.selectedSidebarCategoryIds =

View File

@ -0,0 +1,58 @@
{{#in-element this.modalHeaderAfterTitleElement}}
<p class="sidebar__edit-navigation-modal-form__deselect-wrapper">
<DButton
@class="btn-flat sidebar__edit-navigation-modal-form__deselect-button"
@label="sidebar.edit_navigation_modal_form.deselect_button_text"
@ariaLabel="sidebar.edit_navigation_modal_form.deselect_button_text"
@action={{@deselectAll}}
/>
{{@deselectAllText}}
</p>
{{/in-element}}
<div class="sidebar__edit-navigation-modal-form">
<DModalBody
@title={{@title}}
@class="sidebar__edit-navigation-modal-form__body"
>
<div class="sidebar__edit-navigation-modal-form__filter">
<div class="sidebar__edit-navigation-modal-form__filter-input">
{{d-icon
"search"
class="sidebar__edit-navigation-modal-form__filter-input-icon"
}}
<Input
class="sidebar__edit-navigation-modal-form__filter-input-field"
placeholder={{@inputFilterPlaceholder}}
@type="text"
@value={{this.filter}}
{{on "input" (action @onFilterInput value="target.value")}}
autofocus="true"
/>
</div>
</div>
{{yield}}
</DModalBody>
</div>
<div class="modal-footer sidebar__edit-navigation-modal-form__footer">
<DButton
@class="btn-primary sidebar__edit-navigation-modal-form__save-button"
@label="save"
@disabled={{@saving}}
@action={{@save}}
/>
{{#if @showResetDefaultsButton}}
<DButton
@icon="undo"
@class="btn-flat btn-text sidebar__edit-navigation-modal-form__reset-defaults-button"
@label="sidebar.edit_navigation_modal_form.reset_to_defaults"
@disabled={{@saving}}
@action={{@resetToDefaults}}
/>
{{/if}}
</div>

View File

@ -0,0 +1,16 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
export default class extends Component {
@tracked filter = "";
get modalHeaderAfterTitleElement() {
return document.getElementById("modal-header-after-title");
}
@action
onFilterInput(value) {
this.args.onFilterInput(value);
}
}

View File

@ -0,0 +1,52 @@
<Sidebar::EditNavigationModalForm::Modal
@title="sidebar.tags_form_modal.title"
@saving={{this.saving}}
@save={{this.save}}
@showResetDefaultsButton={{(gt
this.siteSettings.default_navigation_menu_tags.length 0
)}}
@resetToDefaults={{this.resetToDefaults}}
@deselectAll={{this.deselectAll}}
@deselectAllText={{i18n "sidebar.tags_form_modal.subtitle.text"}}
@inputFilterPlaceholder={{i18n "sidebar.tags_form_modal.filter_placeholder"}}
@onFilterInput={{this.onFilterInput}}
>
<form class="sidebar-tags-form">
{{#if this.tagsLoading}}
<div class="spinner"></div>
{{else}}
{{#if (gt this.filteredTags.length 0)}}
{{#each this.filteredTags as |tag|}}
<div class="sidebar-tags-form__tag" data-tag-name={{tag.name}}>
<Input
id={{concat "sidebar-tags-form__input--" tag.name}}
class="sidebar-tags-form__input"
@type="checkbox"
@checked={{includes this.selectedTags tag.name}}
{{on "click" (action "toggleTag" tag.name)}}
/>
<label
class="sidebar-tags-form__tag-label"
for={{concat "sidebar-tags-form__input--" tag.name}}
>
<p>
<span class="sidebar-tags-form__tag-label-name">
{{tag.name}}
</span>
<span class="sidebar-tags-form__tag-label-count">
({{tag.count}})
</span>
</p>
</label>
</div>
{{/each}}
{{else}}
<div class="sidebar-tags-form__no-tags">
{{i18n "sidebar.tags_form_modal.no_tags"}}
</div>
{{/if}}
{{/if}}
</form>
</Sidebar::EditNavigationModalForm::Modal>

View File

@ -53,10 +53,6 @@ export default class extends Component {
}
}
get modalHeaderAfterTitleElement() {
return document.getElementById("modal-header-after-title");
}
@action
onFilterInput(filter) {
discourseDebounce(this, this.#performFiltering, filter, INPUT_DELAY);

View File

@ -1,89 +0,0 @@
{{#in-element this.modalHeaderAfterTitleElement}}
<p class="sidebar-tags-form__deselect">
<DButton
@class="btn-flat sidebar-tags-form__deselect-all-btn"
@label="sidebar.tags_form_modal.subtitle.button_text"
@ariaLabel="sidebar.tags_form_modal.subtitle.button_text"
@action={{this.deselectAll}}
/>
{{i18n "sidebar.tags_form_modal.subtitle.text"}}
</p>
{{/in-element}}
<DModalBody
@title="sidebar.tags_form_modal.title"
@class="sidebar-tags-form-modal"
>
<form class="sidebar-tags-form">
<div class="sidebar-tags-form__filter">
{{d-icon "search" class="sidebar-tags-form__filter-input-icon"}}
<Input
class="sidebar-tags-form__filter-input-field"
placeholder={{i18n "sidebar.tags_form_modal.filter_placeholder"}}
@type="text"
@value={{this.filter}}
{{on "input" (action "onFilterInput" value="target.value")}}
autofocus="true"
/>
</div>
{{#if this.tagsLoading}}
<div class="spinner"></div>
{{else}}
{{#if (gt this.filteredTags.length 0)}}
{{#each this.filteredTags as |tag|}}
<div class="sidebar-tags-form__tag" data-tag-name={{tag.name}}>
<Input
id={{concat "sidebar-tags-form__input--" tag.name}}
class="sidebar-tags-form__input"
@type="checkbox"
@checked={{includes this.selectedTags tag.name}}
{{on "click" (action "toggleTag" tag.name)}}
/>
<label
class="sidebar-tags-form__tag-label"
for={{concat "sidebar-tags-form__input--" tag.name}}
>
<p>
<span class="sidebar-tags-form__tag-label-name">
{{tag.name}}
</span>
<span class="sidebar-tags-form__tag-label-count">
({{tag.count}})
</span>
</p>
</label>
</div>
{{/each}}
{{else}}
<div class="sidebar-tags-form__no-tags">
{{i18n "sidebar.tags_form_modal.no_tags"}}
</div>
{{/if}}
{{/if}}
</form>
</DModalBody>
<div class="modal-footer sidebar-tags-form__modal-footer">
<DButton
@class="btn-primary sidebar-tags-form__save-button"
@label="save"
@disabled={{this.saving}}
@action={{this.save}}
/>
{{#if (gt this.siteSettings.default_navigation_menu_tags.length 0)}}
<DButton
@icon="undo"
@class="btn-flat btn-text sidebar-tags-form__reset-to-defaults-btn"
@label="sidebar.tags_form_modal.reset_to_defaults"
@disabled={{this.saving}}
@action={{this.resetToDefaults}}
/>
{{/if}}
</div>

View File

@ -1 +1,3 @@
<Sidebar::CategoriesFormModal @closeModal={{(action "closeModal")}} />
<Sidebar::EditNavigationModalForm::CategoriesForm
@closeModal={{(action "closeModal")}}
/>

View File

@ -1 +1,3 @@
<Sidebar::TagsFormModal @closeModal={{(action "closeModal")}} />
<Sidebar::EditNavigationModalForm::TagsForm
@closeModal={{(action "closeModal")}}
/>

View File

@ -136,7 +136,7 @@
&:not(.history-modal) {
.modal-body:not(.reorder-categories):not(.poll-ui-builder):not(
.poll-breakdown
):not(.sidebar-categories-form-modal):not(.sidebar-tags-form-modal) {
):not(.sidebar__edit-navigation-modal-form__body) {
max-height: 80vh !important;
@media screen and (max-height: 500px) {
max-height: 65vh !important;

View File

@ -28,8 +28,9 @@
@import "relative-time-picker";
@import "share-and-invite-modal";
@import "download-calendar";
@import "sidebar-categories-form";
@import "sidebar-tags-form";
@import "sidebar/edit-navigation-modal-form/categories-form";
@import "sidebar/edit-navigation-modal-form/modal";
@import "sidebar/edit-navigation-modal-form/tags-form";
@import "svg";
@import "tap-tile";
@import "time-input";

View File

@ -1,93 +0,0 @@
.sidebar-tags-form-modal {
.modal-body {
min-height: 30vh;
}
}
.sidebar-tags-form {
display: flex;
flex-wrap: wrap;
.sidebar-tags-form__tag {
flex-basis: 30%;
display: flex;
flex-direction: row;
align-items: center;
padding: 0.5em 0.25em;
.sidebar-tags-form__tag-label {
margin-bottom: 0;
p {
margin: 0;
}
}
.sidebar-tags-form__input {
margin-top: 0;
}
}
.sidebar-tags-form__tag-label-count {
color: var(--primary-medium);
}
.sidebar-tags-form__filter {
display: flex;
flex-direction: row;
margin-right: auto;
width: 100%;
position: relative;
}
.sidebar-tags-form__filter-input-icon {
position: absolute;
left: 0.5em;
top: 0.65em;
color: var(--primary-low-mid);
}
.sidebar-tags-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-tags-form__modal-footer.modal-footer {
.sidebar-tags-form__reset-to-defaults-btn {
margin-left: auto;
margin-right: 0em;
.d-icon {
font-size: var(--font-down-1);
color: var(--tertiary);
}
&:focus,
&:active {
background: none;
}
}
}
.sidebar-tags-form__deselect {
margin: 0;
.btn-flat.sidebar-tags-form__deselect-all-btn {
margin-left: auto;
padding: 0;
&:focus,
&:active {
background: none;
}
}
}

View File

@ -1,69 +1,4 @@
.sidebar-categories-form-modal {
.modal-body {
min-height: 50vh;
}
}
.sidebar-categories-form__modal-footer.modal-footer {
.sidebar-categories-form__reset-to-defaults-btn {
margin-left: auto;
margin-right: 0em;
.d-icon {
font-size: var(--font-down-1);
color: var(--tertiary);
}
&:focus,
&:active {
background: none;
}
}
}
.sidebar-categories-form__deselect {
margin: 0;
.btn-flat.sidebar-categories-form__deselect-all-btn {
margin-left: auto;
padding: 0;
&:focus,
&:active {
background: none;
}
}
}
.sidebar-categories-form {
.sidebar-categories-form__filter {
display: flex;
flex-direction: row;
margin-right: auto;
width: 100%;
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;

View File

@ -0,0 +1,71 @@
.sidebar__edit-navigation-modal-form {
.modal-body {
min-height: 50vh;
}
.sidebar__edit-navigation-modal-form__filter {
display: flex;
flex-direction: row;
margin-bottom: 1em;
width: 100%;
.sidebar__edit-navigation-modal-form__filter-input {
display: flex;
flex-direction: row;
margin-right: auto;
width: 100%;
position: relative;
}
.sidebar__edit-navigation-modal-form__filter-input-icon {
position: absolute;
left: 0.5em;
top: 0.65em;
color: var(--primary-low-mid);
}
.sidebar__edit-navigation-modal-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__edit-navigation-modal-form__footer {
.btn.sidebar__edit-navigation-modal-form__reset-defaults-button {
margin-left: auto;
margin-right: 0em;
.d-icon {
font-size: var(--font-down-1);
color: var(--tertiary);
}
&:focus,
&:active {
background: none;
}
}
}
.sidebar__edit-navigation-modal-form__deselect-wrapper {
margin: 0;
.btn-flat.sidebar__edit-navigation-modal-form__deselect-button {
margin-left: auto;
padding: 0;
&:focus,
&:active {
background: none;
}
}
}

View File

@ -0,0 +1,28 @@
.sidebar-tags-form {
display: flex;
flex-wrap: wrap;
.sidebar-tags-form__tag {
flex-basis: 30%;
display: flex;
flex-direction: row;
align-items: center;
padding: 0.5em 0.25em;
.sidebar-tags-form__tag-label {
margin-bottom: 0;
p {
margin: 0;
}
}
.sidebar-tags-form__input {
margin-top: 0;
}
}
.sidebar-tags-form__tag-label-count {
color: var(--primary-medium);
}
}

View File

@ -1,5 +1,5 @@
@import "sidebar-categories-form";
@import "sidebar-tags-form";
@import "sidebar/edit-navigation-modal-form/categories-form";
@import "sidebar/edit-navigation-modal-form/tags-form";
@import "user-card";
@import "user-info";
@import "user-stream-item";

View File

@ -1,5 +1,5 @@
@import "sidebar-categories-form";
@import "sidebar-tags-form";
@import "sidebar/edit-navigation-modal-form/modal";
@import "sidebar/edit-navigation-modal-form/tags-form";
@import "topic-footer-mobile-dropdown";
@import "user-card";
@import "user-stream-item";

View File

@ -1,4 +1,4 @@
.sidebar-categories-form-modal {
.sidebar__edit-navigation-modal-form {
.modal-inner-container {
width: 35em;
}

View File

@ -4392,19 +4392,22 @@ en:
categories_form_modal:
title: "Edit categories navigation"
subtitle:
button_text: "Deselect all"
text: "and we'll automatically show this site's most popular categories"
filter_placeholder: "Filter categories"
no_categories: "There are no categories matching the given term."
reset_to_defaults: "Reset to defaults"
tags_form_modal:
title: "Edit tags navigation"
filter_placeholder: "Filter tags"
no_tags: "There are no tags matching the given term."
subtitle:
button_text: "Deselect all"
text: "and we'll automatically show this site's top tags"
edit_navigation_modal_form:
deselect_button_text: "Deselect all"
reset_to_defaults: "Reset to defaults"
filter_dropdown:
all: "All"
selected: "Selected"
unselected: "Unselected"
sections:
custom:

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
require_relative "sidebar_edit_navigation_modal"
module PageObjects
module Modals
class SidebarEditCategories < PageObjects::Modals::Base
class SidebarEditCategories < SidebarEditNavigationModal
def closed?
has_no_css?(".sidebar-categories-form-modal")
end
@ -45,6 +47,12 @@ module PageObjects
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
def toggle_category_checkbox(category)
find(
".sidebar-categories-form-modal .sidebar-categories-form__category-row[data-category-id='#{category.id}'] .sidebar-categories-form__input",
@ -52,38 +60,6 @@ module PageObjects
self
end
def save
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
def deselect_all
click_button(I18n.t("js.sidebar.categories_form_modal.subtitle.button_text"))
self
end
def click_reset_to_defaults_button
click_button(I18n.t("js.sidebar.categories_form_modal.reset_to_defaults"))
self
end
def has_no_reset_to_defaults_button?
has_no_button?(I18n.t("js.sidebar.categories_form_modal.reset_to_defaults"))
end
def has_focus_on_filter_input?
evaluate_script("document.activeElement").native ==
find(".sidebar-categories-form-modal .sidebar-categories-form__filter-input-field").native
end
end
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
module PageObjects
module Modals
class SidebarEditNavigationModal < PageObjects::Modals::Base
def has_focus_on_filter_input?
evaluate_script("document.activeElement").native ==
find(".sidebar__edit-navigation-modal-form__filter-input-field").native
end
def filter(text)
find(".sidebar__edit-navigation-modal-form__filter-input-field").fill_in(with: text)
self
end
def click_reset_to_defaults_button
click_button(I18n.t("js.sidebar.edit_navigation_modal_form.reset_to_defaults"))
self
end
def has_no_reset_to_defaults_button?
has_no_button?(I18n.t("js.sidebar.edit_navigation_modal_form.reset_to_defaults"))
end
def save
find(".sidebar__edit-navigation-modal-form__save-button").click
self
end
def deselect_all
click_button(I18n.t("js.sidebar.edit_navigation_modal_form.deselect_button_text"))
self
end
end
end
end

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
require_relative "sidebar_edit_navigation_modal"
module PageObjects
module Modals
class SidebarEditTags < PageObjects::Modals::Base
class SidebarEditTags < SidebarEditNavigationModal
def closed?
has_no_css?(".sidebar-tags-form-modal")
end
@ -35,31 +37,6 @@ module PageObjects
self
end
def save
find(".sidebar-tags-form-modal .sidebar-tags-form__save-button").click
self
end
def filter(text)
find(".sidebar-tags-form-modal .sidebar-tags-form__filter-input-field").fill_in(with: text)
self
end
def deselect_all
click_button(I18n.t("js.sidebar.tags_form_modal.subtitle.button_text"))
self
end
def click_reset_to_defaults_button
click_button(I18n.t("js.sidebar.tags_form_modal.reset_to_defaults"))
self
end
def has_focus_on_filter_input?
evaluate_script("document.activeElement").native ==
find(".sidebar-tags-form-modal .sidebar-tags-form__filter-input-field").native
end
end
end
end