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.
This commit is contained in:
Krzysztof Kotlarek 2023-05-29 15:20:23 +10:00 committed by GitHub
parent 7d9a823a55
commit 9f78ff5572
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 651 additions and 202 deletions

View File

@ -7,51 +7,50 @@
@class={{this.section.dragCss}}
>
{{#each this.section.links as |link|}}
{{#if link.shouldDisplay}}
{{#if link.external}}
<Sidebar::SectionLink
@linkName={{link.name}}
@content={{replace-emoji link.text}}
@prefixType="icon"
@prefixValue={{link.prefixValue}}
@href={{link.value}}
@class={{link.linkDragCss}}
{{draggable
{{#if link.external}}
<Sidebar::SectionLink
@shouldDisplay={{link.shouldDisplay}}
@linkName={{link.name}}
@content={{replace-emoji link.text}}
@prefixType="icon"
@prefixValue={{link.prefixValue}}
@href={{link.value}}
@class={{link.linkDragCss}}
{{draggable
didStartDrag=link.didStartDrag
didEndDrag=link.didEndDrag
dragMove=link.dragMove
}}
/>
{{else}}
<Sidebar::SectionLink
@shouldDisplay={{link.shouldDisplay}}
@href={{link.href}}
@title={{link.title}}
@linkName={{link.name}}
@route={{link.route}}
@model={{link.model}}
@models={{link.models}}
@query={{link.query}}
@content={{replace-emoji link.text}}
@badgeText={{link.badgeText}}
@prefixType="icon"
@prefixValue={{link.prefixValue}}
@suffixCSSClass={{link.suffixCSSClass}}
@suffixValue={{link.suffixValue}}
@suffixType={{link.suffixType}}
@currentWhen={{link.currentWhen}}
@class={{link.linkDragCss}}
{{(if
link.didStartDrag
(modifier
"draggable"
didStartDrag=link.didStartDrag
didEndDrag=link.didEndDrag
dragMove=link.dragMove
}}
/>
{{else}}
<Sidebar::SectionLink
@shouldDisplay={{link.shouldDisplay}}
@href={{link.href}}
@title={{link.title}}
@linkName={{link.name}}
@route={{link.route}}
@model={{link.model}}
@models={{link.models}}
@query={{link.query}}
@content={{replace-emoji link.text}}
@badgeText={{link.badgeText}}
@prefixType="icon"
@prefixValue={{link.prefixValue}}
@suffixCSSClass={{link.suffixCSSClass}}
@suffixValue={{link.suffixValue}}
@suffixType={{link.suffixType}}
@currentWhen={{link.currentWhen}}
@class={{link.linkDragCss}}
{{(if
link.didStartDrag
(modifier
"draggable"
didStartDrag=link.didStartDrag
didEndDrag=link.didEndDrag
dragMove=link.dragMove
)
)}}
/>
{{/if}}
)
)}}
/>
{{/if}}
{{/each}}

View File

@ -1,15 +1,29 @@
<Sidebar::SectionLink
@shouldDisplay={{@sectionLink.shouldDisplay}}
@linkName={{@sectionLink.name}}
@route={{@sectionLink.route}}
@href={{@sectionLink.href}}
@query={{@sectionLink.query}}
@title={{@sectionLink.title}}
@content={{@sectionLink.text}}
@currentWhen={{@sectionLink.currentWhen}}
@badgeText={{@sectionLink.badgeText}}
@model={{@sectionLink.model}}
@models={{@sectionLink.models}}
@prefixType={{@sectionLink.prefixType}}
@prefixValue={{@sectionLink.prefixValue}}
/>
{{#if @sectionLink.external}}
<Sidebar::SectionLink
@shouldDisplay={{@sectionLink.shouldDisplay}}
@linkName={{@sectionLink.name}}
@content={{replace-emoji @sectionLink.text}}
@prefixType="icon"
@prefixValue={{@sectionLink.prefixValue}}
@href={{@sectionLink.value}}
/>
{{else}}
<Sidebar::SectionLink
@shouldDisplay={{@sectionLink.shouldDisplay}}
@href={{@sectionLink.href}}
@title={{@sectionLink.title}}
@linkName={{@sectionLink.name}}
@route={{@sectionLink.route}}
@model={{@sectionLink.model}}
@models={{@sectionLink.models}}
@query={{@sectionLink.query}}
@content={{replace-emoji @sectionLink.text}}
@badgeText={{@sectionLink.badgeText}}
@prefixType="icon"
@prefixValue={{@sectionLink.prefixValue}}
@suffixCSSClass={{@sectionLink.suffixCSSClass}}
@suffixValue={{@sectionLink.suffixValue}}
@suffixType={{@sectionLink.suffixType}}
@currentWhen={{@sectionLink.currentWhen}}
/>
{{/if}}

View File

@ -0,0 +1,72 @@
<div
class={{concat-class
"sidebar-section-form-link"
"row-wrapper"
this.dragCssClass
}}
draggable="true"
{{on "dragstart" this.dragHasStarted}}
{{on "dragover" this.dragOver}}
{{on "dragenter" this.dragEnter}}
{{on "dragleave" this.dragLeave}}
{{on "dragend" this.dragEnd}}
{{on "drop" this.dropItem}}
>
<div class="draggable" data-link-name={{@link.name}}>
{{d-icon "grip-lines"}}
</div>
<div class="input-group">
<IconPicker
@name="icon"
@value={{@link.icon}}
@options={{hash
maximum=1
caretDownIcon="caret-down"
caretUpIcon="caret-up"
icons=@link.icon
}}
class={{@link.iconCssClass}}
@onlyAvailable={{true}}
@onChange={{action (mut @link.icon)}}
/>
{{#if @link.invalidIconMessage}}
<div class="icon warning">
{{@link.invalidIconMessage}}
</div>
{{/if}}
</div>
<div class="input-group">
<Input
name="link-name"
@type="text"
@value={{@link.name}}
class={{@link.nameCssClass}}
{{on "input" (action (mut @link.name) value="target.value")}}
/>
{{#if @link.invalidNameMessage}}
<div class="name warning">
{{@link.invalidNameMessage}}
</div>
{{/if}}
</div>
<div class="input-group">
<Input
name="link-url"
@type="text"
@value={{@link.value}}
class={{@link.valueCssClass}}
{{on "input" (action (mut @link.value) value="target.value")}}
/>
{{#if @link.invalidValueMessage}}
<div class="value warning">
{{@link.invalidValueMessage}}
</div>
{{/if}}
</div>
<DButton
@icon="trash-alt"
@action={{action @deleteLink @link}}
@class="btn-flat delete-link"
@title="sidebar.sections.custom.links.delete"
/>
</div>

View File

@ -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;
}
}

View File

@ -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() {

View File

@ -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";
}

View File

@ -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";
}
}

View File

@ -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";
}
}

View File

@ -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";
}

View File

@ -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";
}
}

View File

@ -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";
}
}

View File

@ -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";
}
}

View File

@ -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

View File

@ -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";
}
}

View File

@ -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";
}

View File

@ -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";
}
}

View File

@ -22,67 +22,23 @@
</div>
{{/if}}
</div>
{{#each this.activeLinks as |link|}}
<div class="row-wrapper">
<div class="input-group">
<label for="link-name">{{i18n
"sidebar.sections.custom.links.icon.label"
}}</label>
<IconPicker
@name="icon"
@value={{link.icon}}
@options={{hash maximum=1}}
class={{link.iconCssClass}}
@onlyAvailable={{true}}
@onChange={{action (mut link.icon)}}
/>
{{#if link.invalidIconMessage}}
<div class="icon warning">
{{link.invalidIconMessage}}
</div>
{{/if}}
</div>
<div class="input-group">
<label for="link-name">{{i18n
"sidebar.sections.custom.links.name.label"
}}</label>
<Input
name="link-name"
@type="text"
@value={{link.name}}
class={{link.nameCssClass}}
{{on "input" (action (mut link.name) value="target.value")}}
/>
{{#if link.invalidNameMessage}}
<div class="name warning">
{{link.invalidNameMessage}}
</div>
{{/if}}
</div>
<div class="input-group">
<label for="link-url">{{i18n
"sidebar.sections.custom.links.value.label"
}}</label>
<Input
name="link-url"
@type="text"
@value={{link.value}}
class={{link.valueCssClass}}
{{on "input" (action (mut link.value) value="target.value")}}
/>
{{#if link.invalidValueMessage}}
<div class="value warning">
{{link.invalidValueMessage}}
</div>
{{/if}}
</div>
<DButton
@icon="trash-alt"
@action={{action "deleteLink" link}}
@class="btn-flat delete-link"
@title="sidebar.sections.custom.links.delete"
/>
<div class="row-wrapper header">
<div class="input-group link-icon">
<label>{{i18n "sidebar.sections.custom.links.icon.label"}}</label>
</div>
<div class="input-group link-name">
<label>{{i18n "sidebar.sections.custom.links.name.label"}}</label>
</div>
<div class="input-group link-url">
<label>{{i18n "sidebar.sections.custom.links.value.label"}}</label>
</div>
</div>
{{#each this.activeLinks as |link|}}
<Sidebar::SectionFormLink
@link={{link}}
@deleteLink={{this.deleteLink}}
@reorderCallback={{this.reorder}}
/>
{{/each}}
<DButton
@action={{action "addLink"}}
@ -90,9 +46,40 @@
@title="sidebar.sections.custom.links.add"
@icon="plus"
@label="sidebar.sections.custom.links.add"
@ariaLabel="sidebar.sections.custom.links.add"
/>
{{#if this.currentUser.staff}}
<div class="row-wrapper">
{{#if this.model.sectionType}}
<hr />
<h3>{{i18n "sidebar.sections.custom.more_menu"}}</h3>
{{#each this.activeSecondaryLinks as |link|}}
<Sidebar::SectionFormLink
@link={{link}}
@deleteLink={{this.deleteLink}}
@reorderCallback={{this.reorder}}
/>
{{/each}}
<DButton
@action={{action "addSecondaryLink"}}
@class="btn-flat btn-text add-link"
@title="sidebar.sections.custom.links.add"
@icon="plus"
@label="sidebar.sections.custom.links.add"
@ariaLabel="sidebar.sections.custom.links.add"
/>
{{#if this.model.sectionType}}
<DButton
@action={{action "resetToDefault"}}
@class="btn-flat btn-text reset-link"
@icon="undo"
@title="sidebar.sections.custom.links.reset"
@label="sidebar.sections.custom.links.reset"
@ariaLabel="sidebar.sections.custom.links.reset"
/>
{{/if}}
{{/if}}
{{#if (and this.currentUser.staff (not this.model.sectionType))}}
<div class="row-wrapper mark-public-wrapper">
<label class="checkbox-label">
<Input
@type="checkbox"
@ -112,15 +99,17 @@
@action={{action "save"}}
@class="btn-primary"
@label="sidebar.sections.custom.save"
@ariaLabel="sidebar.sections.custom.save"
@disabled={{not this.model.valid}}
/>
{{#if this.model.id}}
{{#if this.canDelete}}
<DButton
@icon="trash-alt"
@id="delete-section"
@class="btn-danger delete"
@action={{action "delete"}}
@label="sidebar.sections.custom.delete"
@ariaLabel="sidebar.sections.custom.delete"
/>
{{/if}}
</div>

View File

@ -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({

View File

@ -75,7 +75,7 @@ export default {
},
{
id: 331,
name: "Info",
name: "About",
value: "/about",
icon: "info-circle",
external: false,

View File

@ -723,7 +723,7 @@ export default {
},
{
id: 331,
name: "Info",
name: "About",
value: "/about",
icon: "info-circle",
external: false,

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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

View File

@ -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"

View File

@ -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")

View File

@ -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:)

View File

@ -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

View File

@ -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