From 9f78ff5572c8b46cb9949672380b38020c9a0f2f Mon Sep 17 00:00:00 2001 From: Krzysztof Kotlarek Date: Mon, 29 May 2023 15:20:23 +1000 Subject: [PATCH] FEATURE: modal for admins to edit Community section (#21668) Allow admins to edit Community section. This includes drag and drop reorder, change names, delete and reset to default. Visual improvements introduced in edit community section modal are available in edit custom section form as well. For example: - drag and drop links to change their position; - smaller icon picker. --- .../sidebar/common/custom-section.hbs | 83 ++++---- .../components/sidebar/more-section-link.hbs | 44 ++-- .../components/sidebar/section-form-link.hbs | 72 +++++++ .../components/sidebar/section-form-link.js | 68 ++++++ .../app/controllers/sidebar-section-form.js | 198 +++++++++++++++--- .../sidebar/base-community-section-link.js | 13 +- .../community-section/about-section-link.js | 7 +- .../community-section/badges-section-link.js | 7 +- .../everything-section-link.js | 7 +- .../community-section/faq-section-link.js | 7 +- .../community-section/groups-section-link.js | 7 +- .../community-section/users-section-link.js | 7 +- .../app/lib/sidebar/community-section.js | 41 +++- .../community-section/admin-section-link.js | 9 +- .../my-posts-section-link.js | 9 +- .../community-section/review-section-link.js | 7 +- .../templates/modal/sidebar-section-form.hbs | 115 +++++----- .../sidebar-user-community-section-test.js | 1 + .../tests/fixtures/session-fixtures.js | 2 +- .../discourse/tests/fixtures/site-fixtures.js | 2 +- .../common/base/sidebar-custom-section.scss | 4 + .../stylesheets/common/base/sidebar.scss | 72 ++++++- .../sidebar_sections_controller.rb | 2 +- config/locales/client.en.yml | 6 +- spec/system/custom_sidebar_sections_spec.rb | 43 ++-- .../system/page_objects/components/sidebar.rb | 4 + .../components/sidebar_header_dropdown.rb | 2 +- .../modals/sidebar_section_form.rb | 14 ++ 28 files changed, 651 insertions(+), 202 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/components/sidebar/section-form-link.hbs create mode 100644 app/assets/javascripts/discourse/app/components/sidebar/section-form-link.js diff --git a/app/assets/javascripts/discourse/app/components/sidebar/common/custom-section.hbs b/app/assets/javascripts/discourse/app/components/sidebar/common/custom-section.hbs index f42bc7fd71f..b0e25bf2288 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/common/custom-section.hbs +++ b/app/assets/javascripts/discourse/app/components/sidebar/common/custom-section.hbs @@ -7,51 +7,50 @@ @class={{this.section.dragCss}} > {{#each this.section.links as |link|}} - {{#if link.shouldDisplay}} - {{#if link.external}} - + {{else}} + - {{else}} - - {{/if}} + ) + )}} + /> {{/if}} {{/each}} diff --git a/app/assets/javascripts/discourse/app/components/sidebar/more-section-link.hbs b/app/assets/javascripts/discourse/app/components/sidebar/more-section-link.hbs index d23371fbdf6..a3cf4c2ece1 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/more-section-link.hbs +++ b/app/assets/javascripts/discourse/app/components/sidebar/more-section-link.hbs @@ -1,15 +1,29 @@ - \ No newline at end of file +{{#if @sectionLink.external}} + +{{else}} + +{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/sidebar/section-form-link.hbs b/app/assets/javascripts/discourse/app/components/sidebar/section-form-link.hbs new file mode 100644 index 00000000000..923dd7cdf24 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/sidebar/section-form-link.hbs @@ -0,0 +1,72 @@ +
+
+ {{d-icon "grip-lines"}} +
+
+ + {{#if @link.invalidIconMessage}} +
+ {{@link.invalidIconMessage}} +
+ {{/if}} +
+
+ + {{#if @link.invalidNameMessage}} +
+ {{@link.invalidNameMessage}} +
+ {{/if}} +
+
+ + {{#if @link.invalidValueMessage}} +
+ {{@link.invalidValueMessage}} +
+ {{/if}} +
+ +
\ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/sidebar/section-form-link.js b/app/assets/javascripts/discourse/app/components/sidebar/section-form-link.js new file mode 100644 index 00000000000..106d2eba74e --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/sidebar/section-form-link.js @@ -0,0 +1,68 @@ +import Component from "@glimmer/component"; +import { action } from "@ember/object"; +import { tracked } from "@glimmer/tracking"; + +export default class SectionFormLink extends Component { + @tracked dragCssClass; + + dragCount = 0; + + isAboveElement(event) { + event.preventDefault(); + const target = event.currentTarget; + const domRect = target.getBoundingClientRect(); + return event.offsetY < domRect.height / 2; + } + + @action + dragHasStarted(event) { + event.dataTransfer.effectAllowed = "move"; + event.dataTransfer.setData("linkId", this.args.link.objectId); + this.dragCssClass = "dragging"; + } + + @action + dragOver(event) { + event.preventDefault(); + if (!this.dragCssClass) { + if (this.isAboveElement(event)) { + this.dragCssClass = "drag-above"; + } else { + this.dragCssClass = "drag-below"; + } + } + } + @action + dragEnter() { + this.dragCount++; + } + + @action + dragLeave() { + this.dragCount--; + if ( + this.dragCount === 0 && + (this.dragCssClass === "drag-above" || this.dragCssClass === "drag-below") + ) { + this.dragCssClass = null; + } + } + + @action + dropItem(event) { + event.stopPropagation(); + this.dragCounter = 0; + this.args.reorderCallback( + parseInt(event.dataTransfer.getData("linkId"), 10), + this.args.link, + this.isAboveElement(event) + ); + this.dragCssClass = null; + } + + @action + dragEnd() { + this.dragCounter = 0; + this.dragCssClass = null; + } +} diff --git a/app/assets/javascripts/discourse/app/controllers/sidebar-section-form.js b/app/assets/javascripts/discourse/app/controllers/sidebar-section-form.js index 49ef501c9f1..42a95868f44 100644 --- a/app/assets/javascripts/discourse/app/controllers/sidebar-section-form.js +++ b/app/assets/javascripts/discourse/app/controllers/sidebar-section-form.js @@ -9,6 +9,7 @@ import { sanitize } from "discourse/lib/text"; import { tracked } from "@glimmer/tracking"; import { A } from "@ember/array"; import { SIDEBAR_SECTION, SIDEBAR_URL } from "discourse/lib/constants"; +import { bind } from "discourse-common/utils/decorators"; const FULL_RELOAD_LINKS_REGEX = [ /^\/my\/[a-z_\-\/]+$/, @@ -19,17 +20,30 @@ const FULL_RELOAD_LINKS_REGEX = [ class Section { @tracked title; @tracked links; + @tracked secondaryLinks; - constructor({ title, links, id, publicSection }) { + constructor({ + title, + links, + secondaryLinks, + id, + publicSection, + sectionType, + }) { this.title = title; this.public = publicSection; + this.sectionType = sectionType; this.links = links; + this.secondaryLinks = secondaryLinks; this.id = id; } get valid() { + const allLinks = this.links + .filter((link) => !link._destroy) + .concat(this.secondaryLinks?.filter((link) => !link._destroy) || []); const validLinks = - this.links.length > 0 && this.links.every((link) => link.valid); + allLinks.length > 0 && allLinks.every((link) => link.valid); return this.validTitle && validLinks; } @@ -70,7 +84,7 @@ class SectionLink { @tracked value; @tracked _destroy; - constructor({ router, icon, name, value, id }) { + constructor({ router, icon, name, value, id, objectId, segment }) { this.router = router; this.icon = icon || "link"; this.name = name; @@ -78,6 +92,8 @@ class SectionLink { this.id = id; this.httpHost = "http://" + window.location.host; this.httpsHost = "https://" + window.location.host; + this.objectId = objectId; + this.segment = segment; } get path() { @@ -165,6 +181,10 @@ class SectionLink { ); } + get isPrimary() { + return this.segment === "primary"; + } + get #blankIcon() { return isEmpty(this.icon); } @@ -221,6 +241,7 @@ export default Controller.extend(ModalFunctionality, { flashText: null, flashClass: null, }); + this.nextObjectId = 0; this.model = this.initModel(); }, @@ -233,27 +254,48 @@ export default Controller.extend(ModalFunctionality, { return new Section({ title: this.model.title, publicSection: this.model.public, - links: A( - this.model.links.map( - (link) => - new SectionLink({ - router: this.router, - icon: link.icon, - name: link.name, - value: link.value, - id: link.id, - }) - ) - ), + sectionType: this.model.section_type, + links: this.model.links.reduce((acc, link) => { + if (link.segment === "primary") { + this.nextObjectId++; + acc.push(this.initLink(link)); + } + return acc; + }, A()), + secondaryLinks: this.model.links.reduce((acc, link) => { + if (link.segment === "secondary") { + this.nextObjectId++; + acc.push(this.initLink(link)); + } + return acc; + }, A()), id: this.model.id, }); } else { return new Section({ - links: A([new SectionLink({ router: this.router })]), + links: A([ + new SectionLink({ + router: this.router, + objectId: this.nextObjectId, + segment: "primary", + }), + ]), }); } }, + initLink(link) { + return new SectionLink({ + router: this.router, + icon: link.icon, + name: link.name, + value: link.value, + id: link.id, + objectId: this.nextObjectId, + segment: link.segment, + }); + }, + create() { return ajax(`/sidebar_sections`, { type: "POST", @@ -294,15 +336,18 @@ export default Controller.extend(ModalFunctionality, { data: JSON.stringify({ title: this.model.title, public: this.model.public, - links: this.model.links.map((link) => { - return { - id: link.id, - icon: link.icon, - name: link.name, - value: link.path, - _destroy: link._destroy, - }; - }), + links: this.model.links + .concat(this.model?.secondaryLinks || []) + .map((link) => { + return { + id: link.id, + icon: link.icon, + name: link.name, + value: link.path, + segment: link.segment, + _destroy: link._destroy, + }; + }), }), }) .then((data) => { @@ -329,23 +374,112 @@ export default Controller.extend(ModalFunctionality, { return this.model.links.filter((link) => !link._destroy); }, + get activeSecondaryLinks() { + return this.model.secondaryLinks?.filter((link) => !link._destroy); + }, + get header() { return this.model.id ? "sidebar.sections.custom.edit" : "sidebar.sections.custom.add"; }, + @bind + reorder(linkFromId, linkTo, above) { + if (linkFromId === linkTo.objectId) { + return; + } + let linkFrom = this.model.links.find( + (link) => link.objectId === linkFromId + ); + if (!linkFrom) { + linkFrom = this.model.secondaryLinks.find( + (link) => link.objectId === linkFromId + ); + } + + if (linkFrom.isPrimary) { + this.model.links.removeObject(linkFrom); + } else { + this.model.secondaryLinks?.removeObject(linkFrom); + } + + if (linkTo.isPrimary) { + const toPosition = this.model.links.indexOf(linkTo); + linkFrom.segment = "primary"; + this.model.links.insertAt(above ? toPosition : toPosition + 1, linkFrom); + } else { + linkFrom.segment = "secondary"; + const toPosition = this.model.secondaryLinks.indexOf(linkTo); + this.model.secondaryLinks.insertAt( + above ? toPosition : toPosition + 1, + linkFrom + ); + } + }, + + get canDelete() { + return this.model.id && !this.model.sectionType; + }, + + @bind + deleteLink(link) { + if (link.id) { + link._destroy = "1"; + } else { + if (link.isPrimary) { + this.model.links.removeObject(link); + } else { + this.model.secondaryLinks.removeObject(link); + } + } + }, + actions: { addLink() { - this.model.links.pushObject(new SectionLink({ router: this.router })); + this.nextObjectId = this.nextObjectId + 1; + this.model.links.pushObject( + new SectionLink({ + router: this.router, + objectId: this.nextObjectId, + segment: "primary", + }) + ); }, - deleteLink(link) { - if (link.id) { - link._destroy = "1"; - } else { - this.model.links.removeObject(link); - } + addSecondaryLink() { + this.nextObjectId = this.nextObjectId + 1; + this.model.secondaryLinks.pushObject( + new SectionLink({ + router: this.router, + objectId: this.nextObjectId, + segment: "secondary", + }) + ); + }, + + resetToDefault() { + return this.dialog.yesNoConfirm({ + message: I18n.t("sidebar.sections.custom.reset_confirm"), + didConfirm: () => { + return ajax(`/sidebar_sections/reset/${this.model.id}`, { + type: "PUT", + }) + .then((data) => { + this.currentUser.sidebar_sections.shiftObject(); + this.currentUser.sidebar_sections.unshiftObject( + data["sidebar_section"] + ); + this.send("closeModal"); + }) + .catch((e) => + this.setProperties({ + flashText: sanitize(extractError(e)), + flashClass: "error", + }) + ); + }, + }); }, save() { diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/base-community-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/base-community-section-link.js index e7dc1603fdb..656fdd32174 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/base-community-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/base-community-section-link.js @@ -9,6 +9,8 @@ export default class BaseCommunitySectionLink { router, siteSettings, inMoreDrawer, + overridenName, + overridenIcon, } = {}) { this.router = router; this.topicTrackingState = topicTrackingState; @@ -16,6 +18,8 @@ export default class BaseCommunitySectionLink { this.appEvents = appEvents; this.siteSettings = siteSettings; this.inMoreDrawer = inMoreDrawer; + this.overridenName = overridenName; + this.overridenIcon = overridenIcon; } /** @@ -105,10 +109,17 @@ export default class BaseCommunitySectionLink { /** * @returns {string} The name of the fontawesome icon to be displayed before the link. Defaults to "link". */ - get prefixValue() { + get defaultPrefixValue() { return "link"; } + /** + * @returns {string} The name of the fontawesome icon to be displayed before the link. + */ + get prefixValue() { + return this.overridenIcon || this.defaultPrefixValue; + } + _notImplemented() { throw "not implemented"; } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/about-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/about-section-link.js index 46c38aa4e14..5b155fe0f2a 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/about-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/about-section-link.js @@ -16,10 +16,13 @@ export default class AboutSectionLink extends BaseSectionLink { } get text() { - return I18n.t("sidebar.sections.community.links.about.content"); + return I18n.t( + `sidebar.sections.community.links.${this.overridenName.toLowerCase()}.content`, + { defaultValue: this.overridenName } + ); } - get prefixValue() { + get defaultPrefixValue() { return "info-circle"; } } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/badges-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/badges-section-link.js index 6904b2cd8c0..139bca4fc2a 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/badges-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/badges-section-link.js @@ -16,14 +16,17 @@ export default class BadgesSectionLink extends BaseSectionLink { } get text() { - return I18n.t("sidebar.sections.community.links.badges.content"); + return I18n.t( + `sidebar.sections.community.links.${this.overridenName.toLowerCase()}.content`, + { defaultValue: this.overridenName } + ); } get shouldDisplay() { return this.siteSettings.enable_badges; } - get prefixValue() { + get defaultPrefixValue() { return "certificate"; } } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/everything-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/everything-section-link.js index 35ca0858c5d..da7dce35350 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/everything-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/everything-section-link.js @@ -44,7 +44,10 @@ export default class EverythingSectionLink extends BaseSectionLink { } get text() { - return I18n.t("sidebar.sections.community.links.everything.content"); + return I18n.t( + `sidebar.sections.community.links.${this.overridenName.toLowerCase()}.content`, + { defaultValue: this.overridenName } + ); } get currentWhen() { @@ -92,7 +95,7 @@ export default class EverythingSectionLink extends BaseSectionLink { return "discovery.latest"; } - get prefixValue() { + get defaultPrefixValue() { return "layer-group"; } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/faq-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/faq-section-link.js index 37a1136c0cd..38e83c58568 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/faq-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/faq-section-link.js @@ -20,10 +20,13 @@ export default class FAQSectionLink extends BaseSectionLink { } get text() { - return I18n.t("sidebar.sections.community.links.faq.content"); + return I18n.t( + `sidebar.sections.community.links.${this.overridenName.toLowerCase()}.content`, + { defaultValue: this.overridenName } + ); } - get prefixValue() { + get defaultPrefixValue() { return "question-circle"; } } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/groups-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/groups-section-link.js index f2d6fe78e8f..667d77dc67a 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/groups-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/groups-section-link.js @@ -16,14 +16,17 @@ export default class GroupsSectionLink extends BaseSectionLink { } get text() { - return I18n.t("sidebar.sections.community.links.groups.content"); + return I18n.t( + `sidebar.sections.community.links.${this.overridenName.toLowerCase()}.content`, + { defaultValue: this.overridenName } + ); } get shouldDisplay() { return this.siteSettings.enable_group_directory; } - get prefixValue() { + get defaultPrefixValue() { return "user-friends"; } } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/users-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/users-section-link.js index fafbf071245..02b979fea92 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/users-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/users-section-link.js @@ -16,7 +16,10 @@ export default class UsersSectionLink extends BaseSectionLink { } get text() { - return I18n.t("sidebar.sections.community.links.users.content"); + return I18n.t( + `sidebar.sections.community.links.${this.overridenName.toLowerCase()}.content`, + { defaultValue: this.overridenName } + ); } get shouldDisplay() { @@ -26,7 +29,7 @@ export default class UsersSectionLink extends BaseSectionLink { ); } - get prefixValue() { + get defaultPrefixValue() { return "users"; } } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/community-section.js b/app/assets/javascripts/discourse/app/lib/sidebar/community-section.js index 97e653ca46d..20fae4dba05 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/community-section.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/community-section.js @@ -20,6 +20,7 @@ import { customSectionLinks, secondaryCustomSectionLinks, } from "discourse/lib/sidebar/custom-community-section-links"; +import showModal from "discourse/lib/show-modal"; const LINKS_IN_BOTH_SEGMENTS = ["/review"]; @@ -111,13 +112,23 @@ export default class CommunitySection { const sectionLinkClass = SPECIAL_LINKS_MAP[link.value]; if (sectionLinkClass) { - return this.#initializeSectionLink(sectionLinkClass, inMoreDrawer); + return this.#initializeSectionLink( + sectionLinkClass, + inMoreDrawer, + link.name, + link.scon + ); } else { return new SectionLink(link, this, this.router); } } - #initializeSectionLink(sectionLinkClass, inMoreDrawer) { + #initializeSectionLink( + sectionLinkClass, + inMoreDrawer, + overridenName, + overridenIcon + ) { if (this.router.isDestroying) { return; } @@ -128,28 +139,48 @@ export default class CommunitySection { router: this.router, siteSettings: this.siteSettings, inMoreDrawer, + overridenName, + overridenIcon, }); } get decoratedTitle() { return I18n.t( - `sidebar.sections.${this.section.title.toLowerCase()}.header_link_text` + `sidebar.sections.${this.section.title.toLowerCase()}.header_link_text`, + { defaultValue: this.section.title } ); } get headerActions() { + if (this.currentUser?.admin) { + return [ + { + action: this.editSection, + title: I18n.t( + "sidebar.sections.community.header_action_edit_section_title" + ), + }, + ]; + } if (this.currentUser) { return [ { action: this.composeTopic, - title: I18n.t("sidebar.sections.community.header_action_title"), + title: I18n.t( + "sidebar.sections.community.header_action_create_topic_title" + ), }, ]; } } get headerActionIcon() { - return "plus"; + return this.currentUser?.admin ? "pencil-alt" : "plus"; + } + + @action + editSection() { + showModal("sidebar-section-form", { model: this.section }); } @action diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/admin-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/admin-section-link.js index 850a7c48d6b..86aa606f93d 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/admin-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/admin-section-link.js @@ -16,14 +16,17 @@ export default class AdminSectionLink extends BaseSectionLink { } get text() { - return I18n.t("sidebar.sections.community.links.admin.content"); + return I18n.t( + `sidebar.sections.community.links.${this.overridenName.toLowerCase()}.content`, + { defaultValue: this.overridenName } + ); } get shouldDisplay() { - return this.currentUser?.staff; + return !!this.currentUser?.staff; } - get prefixValue() { + get defaultPrefixValue() { return "wrench"; } } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/my-posts-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/my-posts-section-link.js index 94debfda7d2..4c7a54f20ee 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/my-posts-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/my-posts-section-link.js @@ -71,7 +71,12 @@ export default class MyPostsSectionLink extends BaseSectionLink { if (this._hasDraft && this.currentUser?.new_new_view_enabled) { return I18n.t("sidebar.sections.community.links.my_posts.content_drafts"); } else { - return I18n.t("sidebar.sections.community.links.my_posts.content"); + return I18n.t( + `sidebar.sections.community.links.${this.overridenName + .toLowerCase() + .replace(" ", "/")}.content`, + { defaultValue: this.overridenName } + ); } } @@ -90,7 +95,7 @@ export default class MyPostsSectionLink extends BaseSectionLink { return this.draftCount > 0; } - get prefixValue() { + get defaultPrefixValue() { if (this._hasDraft && this.currentUser?.new_new_view_enabled) { return "pencil-alt"; } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/review-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/review-section-link.js index 1ac7d1dabff..f9a66aa4885 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/review-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/community-section/review-section-link.js @@ -53,7 +53,10 @@ export default class ReviewSectionLink extends BaseSectionLink { } get text() { - return I18n.t("sidebar.sections.community.links.review.content"); + return I18n.t( + `sidebar.sections.community.links.${this.overridenName.toLowerCase()}.content`, + { defaultValue: this.overridenName } + ); } get shouldDisplay() { @@ -70,7 +73,7 @@ export default class ReviewSectionLink extends BaseSectionLink { } } - get prefixValue() { + get defaultPrefixValue() { return "flag"; } } diff --git a/app/assets/javascripts/discourse/app/templates/modal/sidebar-section-form.hbs b/app/assets/javascripts/discourse/app/templates/modal/sidebar-section-form.hbs index 831629cc97b..b7adff24ef8 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/sidebar-section-form.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/sidebar-section-form.hbs @@ -22,67 +22,23 @@ {{/if}} - {{#each this.activeLinks as |link|}} -
-
- - - {{#if link.invalidIconMessage}} -
- {{link.invalidIconMessage}} -
- {{/if}} -
-
- - - {{#if link.invalidNameMessage}} -
- {{link.invalidNameMessage}} -
- {{/if}} -
-
- - - {{#if link.invalidValueMessage}} -
- {{link.invalidValueMessage}} -
- {{/if}} -
- +
+ + + +
+ {{#each this.activeLinks as |link|}} + {{/each}} - {{#if this.currentUser.staff}} -
+ {{#if this.model.sectionType}} +
+

{{i18n "sidebar.sections.custom.more_menu"}}

+ {{#each this.activeSecondaryLinks as |link|}} + + {{/each}} + + + {{#if this.model.sectionType}} + + {{/if}} + {{/if}} + {{#if (and this.currentUser.staff (not this.model.sectionType))}} +
\ No newline at end of file diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js index cd32940cfc9..17734586c44 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js @@ -25,6 +25,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) { tracked_tags: ["tag1"], watched_tags: ["tag2"], watching_first_post_tags: ["tag3"], + admin: false, }); needs.settings({ diff --git a/app/assets/javascripts/discourse/tests/fixtures/session-fixtures.js b/app/assets/javascripts/discourse/tests/fixtures/session-fixtures.js index cd2713b541c..01d015a253d 100644 --- a/app/assets/javascripts/discourse/tests/fixtures/session-fixtures.js +++ b/app/assets/javascripts/discourse/tests/fixtures/session-fixtures.js @@ -75,7 +75,7 @@ export default { }, { id: 331, - name: "Info", + name: "About", value: "/about", icon: "info-circle", external: false, diff --git a/app/assets/javascripts/discourse/tests/fixtures/site-fixtures.js b/app/assets/javascripts/discourse/tests/fixtures/site-fixtures.js index c7c05895436..86225054781 100644 --- a/app/assets/javascripts/discourse/tests/fixtures/site-fixtures.js +++ b/app/assets/javascripts/discourse/tests/fixtures/site-fixtures.js @@ -723,7 +723,7 @@ export default { }, { id: 331, - name: "Info", + name: "About", value: "/about", icon: "info-circle", external: false, diff --git a/app/assets/stylesheets/common/base/sidebar-custom-section.scss b/app/assets/stylesheets/common/base/sidebar-custom-section.scss index 998ed4e4f7c..d574e47f47f 100644 --- a/app/assets/stylesheets/common/base/sidebar-custom-section.scss +++ b/app/assets/stylesheets/common/base/sidebar-custom-section.scss @@ -16,6 +16,10 @@ .sidebar-section-link-prefix.icon { cursor: move; } + .sidebar-section[data-section-name="community"] + .sidebar-section-link-prefix.icon { + cursor: pointer; + } a { -webkit-touch-callout: none !important; diff --git a/app/assets/stylesheets/common/base/sidebar.scss b/app/assets/stylesheets/common/base/sidebar.scss index df39a9eb167..f36a70e00e0 100644 --- a/app/assets/stylesheets/common/base/sidebar.scss +++ b/app/assets/stylesheets/common/base/sidebar.scss @@ -106,8 +106,21 @@ } } } - .sidebar-section-form-modal { + .draggable { + cursor: move; + align-self: center; + margin-left: auto; + margin-right: auto; + -webkit-user-drag: element; + -khtml-user-drag: element; + -moz-user-drag: element; + -o-user-drag: element; + user-drag: element; + } + .dragging { + opacity: 0.4; + } .modal-inner-container { width: var(--modal-max-width); } @@ -122,17 +135,52 @@ } .row-wrapper { display: grid; - grid-template-columns: auto auto auto 2em; + grid-template-columns: 25px 60px auto auto 2em; gap: 1em; - margin-top: 1em; + padding: 0.5em 1px; + -webkit-user-drag: none; + -khtml-user-drag: none; + -moz-user-drag: none; + -o-user-drag: none; + user-drag: none; + cursor: default; + + &.header { + padding-bottom: 0; + padding-top: 1em; + label { + margin-bottom: 0; + } + .link-url { + margin-left: -1em; + } + } + + &.drag-above { + border-top: 1px dotted #666; + margin-top: -1px; + } + &.drag-below { + border-bottom: 1px dotted #666; + padding-bottom: calc(0.5em - 1px); + } + .link-icon { + grid-column: 1 / span 2; + padding-left: calc(25px + 1em); + } + &.mark-public-wrapper { + label { + grid-column: 1 / -1; + } + } } .delete-link { height: 1em; - align-self: end; - margin-bottom: 0.75em; + align-self: center; margin-right: 1em; } - .btn-flat.add-link { + .btn-flat.add-link, + .btn-flat.reset-link { margin-top: 1em; margin-left: -0.65em; &:active, @@ -148,6 +196,9 @@ color: var(--tertiary-hover); } } + .btn-flat.reset-link { + float: right; + } .modal-footer { display: flex; justify-content: space-between; @@ -156,4 +207,13 @@ margin-right: 0; } } + .select-kit.multi-select .multi-select-header .formatted-selection { + display: none; + } + .modal-inner-container .select-kit { + width: 60px; + } + .select-kit.is-expanded .select-kit-body { + width: 220px !important; + } } diff --git a/app/controllers/sidebar_sections_controller.rb b/app/controllers/sidebar_sections_controller.rb index a9eea34f3dd..a61af92b7a8 100644 --- a/app/controllers/sidebar_sections_controller.rb +++ b/app/controllers/sidebar_sections_controller.rb @@ -59,7 +59,7 @@ class SidebarSectionsController < ApplicationController Site.clear_anon_cache! end - render_serialized(sidebar_section, SidebarSectionSerializer) + render_serialized(sidebar_section.reload, SidebarSectionSerializer) rescue ActiveRecord::RecordInvalid => e render_json_error(e.record.errors.full_messages.first) rescue Discourse::InvalidAccess diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 2cbc4c766bd..581b6d68204 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -4413,10 +4413,13 @@ en: save: "Save" delete: "Delete" delete_confirm: "Are you sure you want to delete this section?" + reset_confirm: "Are you sure you want to reset this section to default?" public: "Make this section public and visible to everyone" + more_menu: "More menu" links: add: "Add another link" delete: "Delete link" + reset: "Reset to default" icon: label: "Icon" validation: @@ -4473,7 +4476,8 @@ en: configure_defaults: "Configure defaults" community: header_link_text: "Community" - header_action_title: "Create a topic" + header_action_create_topic_title: "Create a topic" + header_action_edit_section_title: "Edit Community section" links: about: content: "About" diff --git a/spec/system/custom_sidebar_sections_spec.rb b/spec/system/custom_sidebar_sections_spec.rb index 33f69c55e39..b2f5356e12b 100644 --- a/spec/system/custom_sidebar_sections_spec.rb +++ b/spec/system/custom_sidebar_sections_spec.rb @@ -114,25 +114,17 @@ describe "Custom sidebar sections", type: :system, js: true do sign_in user visit("/latest") - within("[data-section-name='my-section'] .sidebar-section-link-wrapper:nth-child(1)") do - expect(sidebar).to have_section_link("Sidebar Tags") - end - - within("[data-section-name='my-section'] .sidebar-section-link-wrapper:nth-child(2)") do - expect(sidebar).to have_section_link("Sidebar Categories") - end + expect(sidebar.primary_section_links("my-section")).to eq( + ["Sidebar Tags", "Sidebar Categories"], + ) tags_link = find(".sidebar-section-link[data-link-name='Sidebar Tags']") categories_link = find(".sidebar-section-link[data-link-name='Sidebar Categories']") tags_link.drag_to(categories_link, html5: true, delay: 0.4) - within("[data-section-name='my-section'] .sidebar-section-link-wrapper:nth-child(1)") do - expect(sidebar).to have_section_link("Sidebar Categories") - end - - within("[data-section-name='my-section'] .sidebar-section-link-wrapper:nth-child(2)") do - expect(sidebar).to have_section_link("Sidebar Tags") - end + expect(sidebar.primary_section_links("my-section")).to eq( + ["Sidebar Categories", "Sidebar Tags"], + ) end it "does not allow the user to edit public section" do @@ -201,6 +193,29 @@ describe "Custom sidebar sections", type: :system, js: true do expect(sidebar).to have_no_section("Edited public section") end + it "allows admin to edit community section and reset to default" do + sign_in admin + visit("/latest") + + sidebar.edit_custom_section("Community") + section_modal.fill_name("Edited community section") + section_modal.everything_link.drag_to(section_modal.review_link, delay: 0.4) + section_modal.save + + expect(sidebar).to have_section("Edited community section") + expect(sidebar.primary_section_links("edited-community-section")).to eq( + ["My Posts", "Everything", "Admin", "More"], + ) + + sidebar.edit_custom_section("Edited community section") + section_modal.reset + + expect(sidebar).to have_section("Community") + expect(sidebar.primary_section_links("community")).to eq( + ["Everything", "My Posts", "Admin", "More"], + ) + end + it "shows anonymous public sections" do sidebar_section = Fabricate(:sidebar_section, title: "Public section", public: true) sidebar_url_1 = Fabricate(:sidebar_url, name: "Sidebar Tags", value: "/tags") diff --git a/spec/system/page_objects/components/sidebar.rb b/spec/system/page_objects/components/sidebar.rb index 87e2056030d..a8b944ba8ef 100644 --- a/spec/system/page_objects/components/sidebar.rb +++ b/spec/system/page_objects/components/sidebar.rb @@ -63,6 +63,10 @@ module PageObjects find(SIDEBAR_WRAPPER_SELECTOR).has_no_button?(name) end + def primary_section_links(slug) + all("[data-section-name='#{slug}'] .sidebar-section-link-wrapper").map(&:text) + end + private def section_link_present?(name, href: nil, active: false, present:) diff --git a/spec/system/page_objects/components/sidebar_header_dropdown.rb b/spec/system/page_objects/components/sidebar_header_dropdown.rb index d07cbc669c4..e855bcbcbb9 100644 --- a/spec/system/page_objects/components/sidebar_header_dropdown.rb +++ b/spec/system/page_objects/components/sidebar_header_dropdown.rb @@ -25,7 +25,7 @@ module PageObjects def click_community_header_button page.click_button( - I18n.t("js.sidebar.sections.community.header_action_title"), + I18n.t("js.sidebar.sections.community.header_action_create_topic_title"), class: "sidebar-section-header-button", ) end diff --git a/spec/system/page_objects/modals/sidebar_section_form.rb b/spec/system/page_objects/modals/sidebar_section_form.rb index 6b789a7f9a7..37100fd5a07 100644 --- a/spec/system/page_objects/modals/sidebar_section_form.rb +++ b/spec/system/page_objects/modals/sidebar_section_form.rb @@ -28,6 +28,11 @@ module PageObjects find(".dialog-container .btn-primary").click end + def reset + find(".reset-link").click + find(".dialog-footer .btn-primary").click + end + def save find("#save-section").click end @@ -39,9 +44,18 @@ module PageObjects def has_disabled_save? find_button("Save", disabled: true) end + def has_enabled_save? find_button("Save", disabled: false) end + + def everything_link + find(".draggable[data-link-name='Everything']") + end + + def review_link + find(".draggable[data-link-name='Review']") + end end end end