DEV: configurable custom sidebar sections (#20057)
Allows users to configure their own custom sidebar sections with links withing Discourse instance. Links can be passed as relative path, for example "/tags" or full URL. Only path is saved in DB, so when Discourse domain is changed, links will be still valid. Feature is hidden behind SiteSetting.enable_custom_sidebar_sections. This hidden setting determines the group which members have access to this new feature.
This commit is contained in:
parent
5d28cb709a
commit
84a87a703c
|
@ -7,7 +7,7 @@
|
||||||
<a
|
<a
|
||||||
href={{@href}}
|
href={{@href}}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target={{or @target "_blank"}}
|
||||||
class={{this.classNames}}
|
class={{this.classNames}}
|
||||||
title={{@title}}
|
title={{@title}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
<div class="sidebar-custom-sections">
|
||||||
|
{{#each this.currentUser.sidebarSections as |section|}}
|
||||||
|
<Sidebar::Section
|
||||||
|
@sectionName={{section.slug}}
|
||||||
|
@headerLinkText={{section.title}}
|
||||||
|
@collapsable={{true}}
|
||||||
|
@headerActions={{array
|
||||||
|
(hash
|
||||||
|
action=(action this.editSection section)
|
||||||
|
title=(i18n "sidebar.sections.custom.edit")
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
@headerActionsIcon="pencil-alt"
|
||||||
|
>
|
||||||
|
{{#each section.links as |link|}}
|
||||||
|
<Sidebar::SectionLink
|
||||||
|
@linkName={{link.name}}
|
||||||
|
@href={{link.value}}
|
||||||
|
@target="_self"
|
||||||
|
@content={{link.name}}
|
||||||
|
@class={{link.class}}
|
||||||
|
/>
|
||||||
|
{{/each}}
|
||||||
|
</Sidebar::Section>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
<DButton
|
||||||
|
@icon="plus"
|
||||||
|
@action={{action this.addSection}}
|
||||||
|
@class="btn-flat add-section"
|
||||||
|
@title="sidebar.sections.custom.add"
|
||||||
|
/>
|
||||||
|
</div>
|
|
@ -0,0 +1,31 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
|
export default class SidebarUserCustomSections extends Component {
|
||||||
|
@service currentUser;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
this.sections.forEach((section) => {
|
||||||
|
section.links.forEach((link) => {
|
||||||
|
link.class = window.location.pathname === link.value ? "active" : "";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get sections() {
|
||||||
|
return this.currentUser.sidebar_sections || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
editSection(section) {
|
||||||
|
showModal("sidebar-section-form", { model: section });
|
||||||
|
}
|
||||||
|
|
||||||
|
addSection() {
|
||||||
|
showModal("sidebar-section-form");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,8 @@
|
||||||
<div class="sidebar-sections">
|
<div class="sidebar-sections">
|
||||||
<Sidebar::User::CommunitySection @collapsable={{@collapsableSections}} />
|
<Sidebar::User::CommunitySection @collapsable={{@collapsableSections}} />
|
||||||
|
{{#if this.currentUser.custom_sidebar_sections_enabled}}
|
||||||
|
<Sidebar::User::CustomSections />
|
||||||
|
{{/if}}
|
||||||
<Sidebar::User::CategoriesSection @collapsable={{@collapsableSections}} />
|
<Sidebar::User::CategoriesSection @collapsable={{@collapsableSections}} />
|
||||||
|
|
||||||
{{#if this.currentUser.display_sidebar_tags}}
|
{{#if this.currentUser.display_sidebar_tags}}
|
||||||
|
|
|
@ -0,0 +1,234 @@
|
||||||
|
import Controller from "@ember/controller";
|
||||||
|
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { isEmpty } from "@ember/utils";
|
||||||
|
import { extractError } from "discourse/lib/ajax-error";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import I18n from "I18n";
|
||||||
|
import { sanitize } from "discourse/lib/text";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { A } from "@ember/array";
|
||||||
|
|
||||||
|
class Section {
|
||||||
|
@tracked title;
|
||||||
|
@tracked links;
|
||||||
|
|
||||||
|
constructor({ title, links, id }) {
|
||||||
|
this.title = title;
|
||||||
|
this.links = links;
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get valid() {
|
||||||
|
const validLinks =
|
||||||
|
this.links.length > 0 && this.links.every((link) => link.valid);
|
||||||
|
return this.validTitle && validLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
get validTitle() {
|
||||||
|
return !isEmpty(this.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
get titleCssClass() {
|
||||||
|
return this.title === undefined || this.validTitle ? "" : "warning";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SectionLink {
|
||||||
|
@tracked name;
|
||||||
|
@tracked value;
|
||||||
|
@tracked _destroy;
|
||||||
|
|
||||||
|
constructor({ router, name, value, id }) {
|
||||||
|
this.router = router;
|
||||||
|
this.name = name;
|
||||||
|
this.value = value ? `${this.protocolAndHost}${value}` : value;
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get protocolAndHost() {
|
||||||
|
return window.location.protocol + "//" + window.location.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
get path() {
|
||||||
|
return this.value?.replace(this.protocolAndHost, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
get valid() {
|
||||||
|
return this.validName && this.validValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
get validName() {
|
||||||
|
return !isEmpty(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
get nameCssClass() {
|
||||||
|
return this.name === undefined || this.validName ? "" : "warning";
|
||||||
|
}
|
||||||
|
|
||||||
|
get validValue() {
|
||||||
|
return (
|
||||||
|
!isEmpty(this.value) &&
|
||||||
|
(this.value.startsWith(this.protocolAndHost) ||
|
||||||
|
this.value.startsWith("/")) &&
|
||||||
|
this.path &&
|
||||||
|
this.router.recognize(this.path).name !== "unknown"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get valueCssClass() {
|
||||||
|
return this.value === undefined || this.validValue ? "" : "warning";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Controller.extend(ModalFunctionality, {
|
||||||
|
dialog: service(),
|
||||||
|
router: service(),
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
this.setProperties({
|
||||||
|
flashText: null,
|
||||||
|
flashClass: null,
|
||||||
|
});
|
||||||
|
this.model = this.initModel();
|
||||||
|
},
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
this.model = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
initModel() {
|
||||||
|
if (this.model) {
|
||||||
|
return new Section({
|
||||||
|
title: this.model.title,
|
||||||
|
links: A(
|
||||||
|
this.model.links.map(
|
||||||
|
(link) =>
|
||||||
|
new SectionLink({
|
||||||
|
router: this.router,
|
||||||
|
name: link.name,
|
||||||
|
value: link.value,
|
||||||
|
id: link.id,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
id: this.model.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new Section({
|
||||||
|
links: A([new SectionLink({ router: this.router })]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
create() {
|
||||||
|
return ajax(`/sidebar_sections`, {
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
dataType: "json",
|
||||||
|
data: JSON.stringify({
|
||||||
|
title: this.model.title,
|
||||||
|
links: this.model.links.map((link) => {
|
||||||
|
return {
|
||||||
|
name: link.name,
|
||||||
|
value: link.path,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
this.currentUser.sidebar_sections.pushObject(data.sidebar_section);
|
||||||
|
this.send("closeModal");
|
||||||
|
})
|
||||||
|
.catch((e) =>
|
||||||
|
this.setProperties({
|
||||||
|
flashText: sanitize(extractError(e)),
|
||||||
|
flashClass: "error",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
return ajax(`/sidebar_sections/${this.model.id}`, {
|
||||||
|
type: "PUT",
|
||||||
|
contentType: "application/json",
|
||||||
|
dataType: "json",
|
||||||
|
data: JSON.stringify({
|
||||||
|
title: this.model.title,
|
||||||
|
links: this.model.links.map((link) => {
|
||||||
|
return {
|
||||||
|
id: link.id,
|
||||||
|
name: link.name,
|
||||||
|
value: link.path,
|
||||||
|
_destroy: link._destroy,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
const newSidebarSections = this.currentUser.sidebar_sections.map(
|
||||||
|
(section) => {
|
||||||
|
if (section.id === data["sidebar_section"].id) {
|
||||||
|
return data["sidebar_section"];
|
||||||
|
}
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.currentUser.set("sidebar_sections", newSidebarSections);
|
||||||
|
this.send("closeModal");
|
||||||
|
})
|
||||||
|
.catch((e) =>
|
||||||
|
this.setProperties({
|
||||||
|
flashText: sanitize(extractError(e)),
|
||||||
|
flashClass: "error",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
get activeLinks() {
|
||||||
|
return this.model.links.filter((link) => !link._destroy);
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
addLink() {
|
||||||
|
this.model.links.pushObject(new SectionLink({ router: this.router }));
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteLink(link) {
|
||||||
|
if (link.id) {
|
||||||
|
link._destroy = "1";
|
||||||
|
} else {
|
||||||
|
this.model.links.removeObject(link);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
this.model.id ? this.update() : this.create();
|
||||||
|
},
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
return this.dialog.yesNoConfirm({
|
||||||
|
message: I18n.t("sidebar.sections.custom.delete_confirm"),
|
||||||
|
didConfirm: () => {
|
||||||
|
return ajax(`/sidebar_sections/${this.model.id}`, {
|
||||||
|
type: "DELETE",
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
const newSidebarSections =
|
||||||
|
this.currentUser.sidebar_sections.filter((section) => {
|
||||||
|
return section.id !== data["sidebar_section"].id;
|
||||||
|
});
|
||||||
|
this.currentUser.set("sidebar_sections", newSidebarSections);
|
||||||
|
this.send("closeModal");
|
||||||
|
})
|
||||||
|
.catch((e) =>
|
||||||
|
this.setProperties({
|
||||||
|
flashText: sanitize(extractError(e)),
|
||||||
|
flashClass: "error",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -407,6 +407,8 @@ const User = RestModel.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sidebarSections: alias("sidebar_sections"),
|
||||||
|
|
||||||
sidebarTagNames: mapBy("sidebarTags", "name"),
|
sidebarTagNames: mapBy("sidebarTags", "name"),
|
||||||
sidebarListDestination: readOnly("sidebar_list_destination"),
|
sidebarListDestination: readOnly("sidebar_list_destination"),
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
{{#if this.flashText}}
|
||||||
|
<div id="modal-alert" role="alert" class="alert alert-{{this.flashClass}}">
|
||||||
|
{{this.flashText}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
<DModalBody @title="sidebar.sections.custom.add">
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="section-name">{{i18n "sidebar.sections.custom.name"}}</label>
|
||||||
|
<Input
|
||||||
|
name="section-name"
|
||||||
|
@type="text"
|
||||||
|
@value={{this.model.title}}
|
||||||
|
class={{this.model.titleCssClass}}
|
||||||
|
{{on "input" (action (mut this.model.title) value="target.value")}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{#each this.activeLinks as |link|}}
|
||||||
|
<div class="row-wrapper">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="link-name">{{i18n
|
||||||
|
"sidebar.sections.custom.links.name"
|
||||||
|
}}</label>
|
||||||
|
<Input
|
||||||
|
name="link-name"
|
||||||
|
@type="text"
|
||||||
|
@value={{link.name}}
|
||||||
|
class={{link.nameCssClass}}
|
||||||
|
{{on "input" (action (mut link.name) value="target.value")}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="link-url">{{i18n
|
||||||
|
"sidebar.sections.custom.links.points_to"
|
||||||
|
}}</label>
|
||||||
|
<Input
|
||||||
|
name="link-url"
|
||||||
|
@type="text"
|
||||||
|
@value={{link.value}}
|
||||||
|
class={{link.valueCssClass}}
|
||||||
|
{{on "input" (action (mut link.value) value="target.value")}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DButton
|
||||||
|
@icon="times"
|
||||||
|
@action={{action "deleteLink" link}}
|
||||||
|
@class="btn-flat delete-link"
|
||||||
|
@title="sidebar.sections.custom.links.delete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
<DButton
|
||||||
|
@action={{action "addLink"}}
|
||||||
|
@class="btn-flat btn-text add-link"
|
||||||
|
@title="sidebar.sections.custom.links.add"
|
||||||
|
@label="sidebar.sections.custom.links.add"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</DModalBody>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<DButton
|
||||||
|
@id="save-section"
|
||||||
|
@action={{action "save"}}
|
||||||
|
@class="btn-primary"
|
||||||
|
@icon="plus"
|
||||||
|
@label="sidebar.sections.custom.save"
|
||||||
|
@disabled={{not this.model.valid}}
|
||||||
|
/>
|
||||||
|
{{#if this.model.id}}
|
||||||
|
<DButton
|
||||||
|
@icon="trash-alt"
|
||||||
|
@id="delete-section"
|
||||||
|
@class="btn-danger delete"
|
||||||
|
@action={{action "delete"}}
|
||||||
|
@label="sidebar.sections.custom.delete"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
|
@ -110,3 +110,79 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-custom-sections {
|
||||||
|
.btn-flat.add-section {
|
||||||
|
margin-left: calc(var(--d-sidebar-section-link-prefix-width) / 2);
|
||||||
|
margin-right: calc(var(--d-sidebar-section-link-prefix-width) / 2);
|
||||||
|
width: calc(100% - var(--d-sidebar-section-link-prefix-width));
|
||||||
|
svg {
|
||||||
|
height: 0.75em;
|
||||||
|
width: 0.75em;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
flex: 1 1;
|
||||||
|
border-bottom: 1px solid var(--primary-low-mid);
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background: var(--d-sidebar-highlight-color);
|
||||||
|
border-radius: 5px;
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
border-bottom: 1px solid var(--primary-high);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.sidebar-section-link {
|
||||||
|
padding-left: calc(
|
||||||
|
var(--d-sidebar-section-link-prefix-width) +
|
||||||
|
var(--d-sidebar-section-link-prefix-margin-right) +
|
||||||
|
var(--d-sidebar-row-horizontal-padding)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sidebar-section-form-modal {
|
||||||
|
.modal-inner-container {
|
||||||
|
width: var(--modal-max-width);
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
input.warning {
|
||||||
|
border: 1px solid var(--danger);
|
||||||
|
}
|
||||||
|
.row-wrapper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto 2em;
|
||||||
|
gap: 1em;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
.delete-link {
|
||||||
|
height: 1em;
|
||||||
|
align-self: end;
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
.btn-flat.add-link {
|
||||||
|
float: right;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-right: -0.5em;
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SidebarSectionsController < ApplicationController
|
||||||
|
requires_login
|
||||||
|
before_action :check_if_member_of_group
|
||||||
|
|
||||||
|
def create
|
||||||
|
sidebar_section =
|
||||||
|
SidebarSection.create!(
|
||||||
|
section_params.merge(user: current_user, sidebar_urls_attributes: links_params),
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: SidebarSectionSerializer.new(sidebar_section)
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
render_json_error(e.record.errors.full_messages.first)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
sidebar_section = SidebarSection.find_by(id: section_params["id"])
|
||||||
|
@guardian.ensure_can_edit!(sidebar_section)
|
||||||
|
|
||||||
|
sidebar_section.update!(section_params.merge(sidebar_urls_attributes: links_params))
|
||||||
|
|
||||||
|
render json: SidebarSectionSerializer.new(sidebar_section)
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
render_json_error(e.record.errors.full_messages.first)
|
||||||
|
rescue Discourse::InvalidAccess
|
||||||
|
render json: failed_json, status: 403
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
sidebar_section = SidebarSection.find_by(id: section_params["id"])
|
||||||
|
@guardian.ensure_can_delete!(sidebar_section)
|
||||||
|
sidebar_section.destroy!
|
||||||
|
render json: SidebarSectionSerializer.new(sidebar_section)
|
||||||
|
rescue Discourse::InvalidAccess
|
||||||
|
render json: failed_json, status: 403
|
||||||
|
end
|
||||||
|
|
||||||
|
def section_params
|
||||||
|
params.permit(:id, :title)
|
||||||
|
end
|
||||||
|
|
||||||
|
def links_params
|
||||||
|
params.permit(links: %i[name value id _destroy])["links"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_if_member_of_group
|
||||||
|
### TODO remove when enable_custom_sidebar_sections SiteSetting is removed
|
||||||
|
if !SiteSetting.enable_custom_sidebar_sections.present? ||
|
||||||
|
!current_user.in_any_groups?(SiteSetting.enable_custom_sidebar_sections_map)
|
||||||
|
raise Discourse::InvalidAccess
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SidebarSection < ActiveRecord::Base
|
||||||
|
belongs_to :user
|
||||||
|
has_many :sidebar_section_links, dependent: :destroy
|
||||||
|
has_many :sidebar_urls,
|
||||||
|
through: :sidebar_section_links,
|
||||||
|
source: :linkable,
|
||||||
|
source_type: "SidebarUrl"
|
||||||
|
|
||||||
|
accepts_nested_attributes_for :sidebar_urls, allow_destroy: true
|
||||||
|
|
||||||
|
validates :title, presence: true, uniqueness: { scope: %i[user_id] }
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: sidebar_sections
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# user_id :integer not null
|
||||||
|
# title :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_sidebar_sections_on_user_id_and_title (user_id,title) UNIQUE
|
||||||
|
#
|
|
@ -3,13 +3,17 @@
|
||||||
class SidebarSectionLink < ActiveRecord::Base
|
class SidebarSectionLink < ActiveRecord::Base
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :linkable, polymorphic: true
|
belongs_to :linkable, polymorphic: true
|
||||||
|
belongs_to :sidebar_section
|
||||||
|
|
||||||
validates :user_id, presence: true, uniqueness: { scope: %i[linkable_type linkable_id] }
|
validates :user_id, presence: true, uniqueness: { scope: %i[linkable_type linkable_id] }
|
||||||
validates :linkable_id, presence: true
|
validates :linkable_id, presence: true
|
||||||
validates :linkable_type, presence: true
|
validates :linkable_type, presence: true
|
||||||
validate :ensure_supported_linkable_type, if: :will_save_change_to_linkable_type?
|
validate :ensure_supported_linkable_type, if: :will_save_change_to_linkable_type?
|
||||||
|
|
||||||
SUPPORTED_LINKABLE_TYPES = %w[Category Tag]
|
SUPPORTED_LINKABLE_TYPES = %w[Category Tag SidebarUrl]
|
||||||
|
|
||||||
|
before_validation { self.user_id ||= self.sidebar_section&.user_id }
|
||||||
|
after_destroy { self.linkable.destroy! if self.linkable_type == "SidebarUrl" }
|
||||||
|
|
||||||
private def ensure_supported_linkable_type
|
private def ensure_supported_linkable_type
|
||||||
if (!SUPPORTED_LINKABLE_TYPES.include?(self.linkable_type)) ||
|
if (!SUPPORTED_LINKABLE_TYPES.include?(self.linkable_type)) ||
|
||||||
|
@ -32,6 +36,7 @@ end
|
||||||
# linkable_type :string not null
|
# linkable_type :string not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
|
# sidebar_section_id :integer
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SidebarUrl < ActiveRecord::Base
|
||||||
|
validates :name, presence: true
|
||||||
|
validates :value, presence: true
|
||||||
|
validate :path_validator
|
||||||
|
|
||||||
|
def path_validator
|
||||||
|
Rails.application.routes.recognize_path(value)
|
||||||
|
rescue ActionController::RoutingError
|
||||||
|
errors.add(
|
||||||
|
:value,
|
||||||
|
I18n.t("activerecord.errors.models.sidebar_section_link.attributes.linkable_type.invalid"),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: sidebar_urls
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# name :string not null
|
||||||
|
# value :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
|
@ -82,6 +82,7 @@ class User < ActiveRecord::Base
|
||||||
has_many :muted_user_records, class_name: "MutedUser", dependent: :delete_all
|
has_many :muted_user_records, class_name: "MutedUser", dependent: :delete_all
|
||||||
has_many :ignored_user_records, class_name: "IgnoredUser", dependent: :delete_all
|
has_many :ignored_user_records, class_name: "IgnoredUser", dependent: :delete_all
|
||||||
has_many :do_not_disturb_timings, dependent: :delete_all
|
has_many :do_not_disturb_timings, dependent: :delete_all
|
||||||
|
has_many :sidebar_sections, dependent: :destroy
|
||||||
has_one :user_status, dependent: :destroy
|
has_one :user_status, dependent: :destroy
|
||||||
|
|
||||||
# dependent deleting handled via before_destroy (special cases)
|
# dependent deleting handled via before_destroy (special cases)
|
||||||
|
|
|
@ -44,6 +44,18 @@ module UserSidebarMixin
|
||||||
sidebar_navigation_menu?
|
sidebar_navigation_menu?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sidebar_sections
|
||||||
|
object
|
||||||
|
.sidebar_sections
|
||||||
|
.order(created_at: :asc)
|
||||||
|
.includes(sidebar_section_links: :linkable)
|
||||||
|
.map { |section| SidebarSectionSerializer.new(section, root: false) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_sidebar_sections?
|
||||||
|
sidebar_navigation_menu?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def sidebar_navigation_menu?
|
def sidebar_navigation_menu?
|
||||||
|
|
|
@ -69,7 +69,9 @@ class CurrentUserSerializer < BasicUserSerializer
|
||||||
:display_sidebar_tags,
|
:display_sidebar_tags,
|
||||||
:sidebar_tags,
|
:sidebar_tags,
|
||||||
:sidebar_category_ids,
|
:sidebar_category_ids,
|
||||||
:sidebar_list_destination
|
:sidebar_list_destination,
|
||||||
|
:sidebar_sections,
|
||||||
|
:custom_sidebar_sections_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
|
||||||
|
@ -309,4 +311,11 @@ class CurrentUserSerializer < BasicUserSerializer
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
def custom_sidebar_sections_enabled
|
||||||
|
if SiteSetting.enable_custom_sidebar_sections.present?
|
||||||
|
object.in_any_groups?(SiteSetting.enable_custom_sidebar_sections_map)
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SidebarSectionSerializer < ApplicationSerializer
|
||||||
|
attributes :id, :title, :links, :slug
|
||||||
|
|
||||||
|
def links
|
||||||
|
object.sidebar_section_links.map(&:linkable)
|
||||||
|
end
|
||||||
|
|
||||||
|
def slug
|
||||||
|
object.title.parameterize
|
||||||
|
end
|
||||||
|
end
|
|
@ -4362,6 +4362,18 @@ en:
|
||||||
all_categories: "All categories"
|
all_categories: "All categories"
|
||||||
all_tags: "All tags"
|
all_tags: "All tags"
|
||||||
sections:
|
sections:
|
||||||
|
custom:
|
||||||
|
add: "Add custom section"
|
||||||
|
edit: "Edit custom section"
|
||||||
|
name: "Section title"
|
||||||
|
save: "Save"
|
||||||
|
delete: "Delete"
|
||||||
|
delete_confirm: "Are you sure you want to delete this section?"
|
||||||
|
links:
|
||||||
|
name: "Link name"
|
||||||
|
points_to: "Points to"
|
||||||
|
add: "Add link"
|
||||||
|
delete: "Delete link"
|
||||||
about:
|
about:
|
||||||
header_link_text: "About"
|
header_link_text: "About"
|
||||||
messages:
|
messages:
|
||||||
|
|
|
@ -1588,6 +1588,8 @@ Discourse::Application.routes.draw do
|
||||||
put "user-status" => "user_status#set"
|
put "user-status" => "user_status#set"
|
||||||
delete "user-status" => "user_status#clear"
|
delete "user-status" => "user_status#clear"
|
||||||
|
|
||||||
|
resources :sidebar_sections, only: %i[create update destroy]
|
||||||
|
|
||||||
get "*url", to: "permalinks#show", constraints: PermalinkConstraint.new
|
get "*url", to: "permalinks#show", constraints: PermalinkConstraint.new
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2118,6 +2118,14 @@ navigation:
|
||||||
enable_new_notifications_menu:
|
enable_new_notifications_menu:
|
||||||
default: false
|
default: false
|
||||||
validator: "EnableNewNotificationsMenuValidator"
|
validator: "EnableNewNotificationsMenuValidator"
|
||||||
|
enable_custom_sidebar_sections:
|
||||||
|
client: true
|
||||||
|
type: group_list
|
||||||
|
list_type: compact
|
||||||
|
default: ""
|
||||||
|
allow_any: false
|
||||||
|
refresh: true
|
||||||
|
hidden: true
|
||||||
|
|
||||||
embedding:
|
embedding:
|
||||||
embed_by_username:
|
embed_by_username:
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateSidebarSections < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
create_table :sidebar_sections do |t|
|
||||||
|
t.integer :user_id, null: false
|
||||||
|
t.string :title, null: false
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :sidebar_sections, %i[user_id title], unique: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateSidebarUrl < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
create_table :sidebar_urls do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.string :value, null: false
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddSidebarSectionIdToSidebarSectionLinks < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
add_column :sidebar_section_links, :sidebar_section_id, :integer, index: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,26 +1,28 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "guardian/bookmark_guardian"
|
||||||
require "guardian/category_guardian"
|
require "guardian/category_guardian"
|
||||||
require "guardian/ensure_magic"
|
require "guardian/ensure_magic"
|
||||||
|
require "guardian/group_guardian"
|
||||||
require "guardian/post_guardian"
|
require "guardian/post_guardian"
|
||||||
require "guardian/bookmark_guardian"
|
require "guardian/post_revision_guardian"
|
||||||
|
require "guardian/sidebar_guardian"
|
||||||
|
require "guardian/tag_guardian"
|
||||||
require "guardian/topic_guardian"
|
require "guardian/topic_guardian"
|
||||||
require "guardian/user_guardian"
|
require "guardian/user_guardian"
|
||||||
require "guardian/post_revision_guardian"
|
|
||||||
require "guardian/group_guardian"
|
|
||||||
require "guardian/tag_guardian"
|
|
||||||
|
|
||||||
# The guardian is responsible for confirming access to various site resources and operations
|
# The guardian is responsible for confirming access to various site resources and operations
|
||||||
class Guardian
|
class Guardian
|
||||||
include EnsureMagic
|
|
||||||
include CategoryGuardian
|
|
||||||
include PostGuardian
|
|
||||||
include BookmarkGuardian
|
include BookmarkGuardian
|
||||||
|
include CategoryGuardian
|
||||||
|
include EnsureMagic
|
||||||
|
include GroupGuardian
|
||||||
|
include PostGuardian
|
||||||
|
include PostRevisionGuardian
|
||||||
|
include SidebarGuardian
|
||||||
|
include TagGuardian
|
||||||
include TopicGuardian
|
include TopicGuardian
|
||||||
include UserGuardian
|
include UserGuardian
|
||||||
include PostRevisionGuardian
|
|
||||||
include GroupGuardian
|
|
||||||
include TagGuardian
|
|
||||||
|
|
||||||
class AnonymousUser
|
class AnonymousUser
|
||||||
def blank?
|
def blank?
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module SidebarGuardian
|
||||||
|
def can_edit_sidebar_section?(sidebar_section)
|
||||||
|
is_my_own?(sidebar_section)
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_delete_sidebar_section?(sidebar_section)
|
||||||
|
is_my_own?(sidebar_section)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,6 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator(:sidebar_section) do
|
||||||
|
title "Sidebar section"
|
||||||
|
user
|
||||||
|
end
|
|
@ -0,0 +1,6 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator(:sidebar_url) do
|
||||||
|
name "tags"
|
||||||
|
value "/tags"
|
||||||
|
end
|
|
@ -0,0 +1,8 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe SidebarUrl do
|
||||||
|
it "validates path" do
|
||||||
|
expect(SidebarUrl.new(name: "categories", value: "/categories").valid?).to eq(true)
|
||||||
|
expect(SidebarUrl.new(name: "categories", value: "/invalid_path").valid?).to eq(false)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,138 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe SidebarSectionsController do
|
||||||
|
fab!(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
### TODO remove when enable_custom_sidebar_sections SiteSetting is removed
|
||||||
|
group = Fabricate(:group)
|
||||||
|
Fabricate(:group_user, group: group, user: user)
|
||||||
|
SiteSetting.enable_custom_sidebar_sections = group.id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#create" do
|
||||||
|
it "is not available for anonymous" do
|
||||||
|
post "/sidebar_sections.json",
|
||||||
|
params: {
|
||||||
|
title: "custom section",
|
||||||
|
links: [
|
||||||
|
{ name: "categories", value: "/categories" },
|
||||||
|
{ name: "tags", value: "/tags" },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response.status).to eq(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates custom section for user" do
|
||||||
|
sign_in(user)
|
||||||
|
post "/sidebar_sections.json",
|
||||||
|
params: {
|
||||||
|
title: "custom section",
|
||||||
|
links: [
|
||||||
|
{ name: "categories", value: "/categories" },
|
||||||
|
{ name: "tags", value: "/tags" },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
|
||||||
|
expect(SidebarSection.count).to eq(1)
|
||||||
|
sidebar_section = SidebarSection.last
|
||||||
|
|
||||||
|
expect(sidebar_section.title).to eq("custom section")
|
||||||
|
expect(sidebar_section.user).to eq(user)
|
||||||
|
expect(sidebar_section.sidebar_urls.count).to eq(2)
|
||||||
|
expect(sidebar_section.sidebar_urls.first.name).to eq("categories")
|
||||||
|
expect(sidebar_section.sidebar_urls.first.value).to eq("/categories")
|
||||||
|
expect(sidebar_section.sidebar_urls.second.name).to eq("tags")
|
||||||
|
expect(sidebar_section.sidebar_urls.second.value).to eq("/tags")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#update" do
|
||||||
|
fab!(:sidebar_section) { Fabricate(:sidebar_section, user: user) }
|
||||||
|
fab!(:sidebar_url_1) { Fabricate(:sidebar_url, name: "tags", value: "/tags") }
|
||||||
|
fab!(:sidebar_url_2) { Fabricate(:sidebar_url, name: "categories", value: "/categories") }
|
||||||
|
fab!(:section_link_1) do
|
||||||
|
Fabricate(:sidebar_section_link, sidebar_section: sidebar_section, linkable: sidebar_url_1)
|
||||||
|
end
|
||||||
|
fab!(:section_link_2) do
|
||||||
|
Fabricate(:sidebar_section_link, sidebar_section: sidebar_section, linkable: sidebar_url_2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows user to update their own section and links" do
|
||||||
|
sign_in(user)
|
||||||
|
put "/sidebar_sections/#{sidebar_section.id}.json",
|
||||||
|
params: {
|
||||||
|
title: "custom section edited",
|
||||||
|
links: [
|
||||||
|
{ id: sidebar_url_1.id, name: "latest", value: "/latest" },
|
||||||
|
{ id: sidebar_url_2.id, name: "tags", value: "/tags", _destroy: "1" },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
|
||||||
|
expect(sidebar_section.reload.title).to eq("custom section edited")
|
||||||
|
expect(sidebar_url_1.reload.name).to eq("latest")
|
||||||
|
expect(sidebar_url_1.value).to eq("/latest")
|
||||||
|
expect { section_link_2.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
expect { sidebar_url_2.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't allow to edit other's sections" do
|
||||||
|
sidebar_section_2 = Fabricate(:sidebar_section)
|
||||||
|
sidebar_url_3 = Fabricate(:sidebar_url, name: "other_tags", value: "/tags")
|
||||||
|
Fabricate(:sidebar_section_link, sidebar_section: sidebar_section_2, linkable: sidebar_url_3)
|
||||||
|
sign_in(user)
|
||||||
|
put "/sidebar_sections/#{sidebar_section_2.id}.json",
|
||||||
|
params: {
|
||||||
|
title: "custom section edited",
|
||||||
|
links: [{ id: sidebar_url_3.id, name: "takeover", value: "/categories" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response.status).to eq(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't allow to edit other's links" do
|
||||||
|
sidebar_url_3 = Fabricate(:sidebar_url, name: "other_tags", value: "/tags")
|
||||||
|
Fabricate(
|
||||||
|
:sidebar_section_link,
|
||||||
|
sidebar_section: Fabricate(:sidebar_section),
|
||||||
|
linkable: sidebar_url_3,
|
||||||
|
)
|
||||||
|
sign_in(user)
|
||||||
|
put "/sidebar_sections/#{sidebar_section.id}.json",
|
||||||
|
params: {
|
||||||
|
title: "custom section edited",
|
||||||
|
links: [{ id: sidebar_url_3.id, name: "takeover", value: "/categories" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response.status).to eq(404)
|
||||||
|
|
||||||
|
expect(sidebar_url_3.reload.name).to eq("other_tags")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#destroy" do
|
||||||
|
fab!(:sidebar_section) { Fabricate(:sidebar_section, user: user) }
|
||||||
|
|
||||||
|
it "allows user to delete their own section" do
|
||||||
|
sign_in(user)
|
||||||
|
delete "/sidebar_sections/#{sidebar_section.id}.json"
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
|
||||||
|
expect { sidebar_section.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't allow to delete other's sidebar section" do
|
||||||
|
sidebar_section_2 = Fabricate(:sidebar_section)
|
||||||
|
sign_in(user)
|
||||||
|
delete "/sidebar_sections/#{sidebar_section_2.id}.json"
|
||||||
|
|
||||||
|
expect(response.status).to eq(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,69 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
describe "Custom sidebar sections", type: :system, js: true do
|
||||||
|
fab!(:user) { Fabricate(:user) }
|
||||||
|
let(:section_modal) { PageObjects::Modals::SidebarSectionForm.new }
|
||||||
|
let(:sidebar) { PageObjects::Components::Sidebar.new }
|
||||||
|
|
||||||
|
before do
|
||||||
|
### TODO remove when enable_custom_sidebar_sections SiteSetting is removed
|
||||||
|
group = Fabricate(:group)
|
||||||
|
Fabricate(:group_user, group: group, user: user)
|
||||||
|
SiteSetting.enable_custom_sidebar_sections = group.id.to_s
|
||||||
|
sign_in user
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows the user to create custom section" do
|
||||||
|
visit("/latest")
|
||||||
|
sidebar.open_new_custom_section
|
||||||
|
|
||||||
|
expect(section_modal).to be_visible
|
||||||
|
expect(section_modal).to have_disabled_save
|
||||||
|
|
||||||
|
section_modal.fill_name("My section")
|
||||||
|
|
||||||
|
section_modal.fill_link("Sidebar Tags", "/tags")
|
||||||
|
expect(section_modal).to have_enabled_save
|
||||||
|
|
||||||
|
section_modal.save
|
||||||
|
|
||||||
|
expect(page).to have_button("My section")
|
||||||
|
expect(page).to have_link("Sidebar Tags")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows the user to edit custom section" do
|
||||||
|
sidebar_section = Fabricate(:sidebar_section, title: "My section", user: user)
|
||||||
|
sidebar_url_1 = Fabricate(:sidebar_url, name: "Sidebar Tags", value: "/tags")
|
||||||
|
Fabricate(:sidebar_section_link, sidebar_section: sidebar_section, linkable: sidebar_url_1)
|
||||||
|
sidebar_url_2 = Fabricate(:sidebar_url, name: "Sidebar Categories", value: "/categories")
|
||||||
|
Fabricate(:sidebar_section_link, sidebar_section: sidebar_section, linkable: sidebar_url_2)
|
||||||
|
|
||||||
|
visit("/latest")
|
||||||
|
|
||||||
|
sidebar.edit_custom_section("My section")
|
||||||
|
section_modal.fill_name("Edited section")
|
||||||
|
section_modal.fill_link("Edited Tags", "/tags")
|
||||||
|
section_modal.remove_last_link
|
||||||
|
|
||||||
|
section_modal.save
|
||||||
|
|
||||||
|
expect(page).to have_button("Edited section")
|
||||||
|
expect(page).to have_link("Edited Tags")
|
||||||
|
expect(page).not_to have_link("Sidebar Categories")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows the user to delete custom section" do
|
||||||
|
sidebar_section = Fabricate(:sidebar_section, title: "My section", user: user)
|
||||||
|
sidebar_url_1 = Fabricate(:sidebar_url, name: "tags", value: "/tags")
|
||||||
|
Fabricate(:sidebar_section_link, sidebar_section: sidebar_section, linkable: sidebar_url_1)
|
||||||
|
|
||||||
|
visit("/latest")
|
||||||
|
|
||||||
|
sidebar.edit_custom_section("My section")
|
||||||
|
|
||||||
|
section_modal.delete
|
||||||
|
section_modal.confirm_delete
|
||||||
|
|
||||||
|
expect(page).not_to have_button("My section")
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,6 +10,15 @@ module PageObjects
|
||||||
def has_category_section_link?(category)
|
def has_category_section_link?(category)
|
||||||
page.has_link?(category.name, class: "sidebar-section-link")
|
page.has_link?(category.name, class: "sidebar-section-link")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def open_new_custom_section
|
||||||
|
find("button.add-section").click
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit_custom_section(name)
|
||||||
|
find(".sidebar-section-#{name.parameterize}").hover
|
||||||
|
find(".sidebar-section-#{name.parameterize} button.sidebar-section-header-button").click
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module PageObjects
|
||||||
|
module Modals
|
||||||
|
class SidebarSectionForm < PageObjects::Modals::Base
|
||||||
|
def fill_name(name)
|
||||||
|
fill_in "section-name", with: name
|
||||||
|
end
|
||||||
|
|
||||||
|
def fill_link(name, url)
|
||||||
|
fill_in "link-name", with: name, match: :first
|
||||||
|
fill_in "link-url", with: url, match: :first
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_last_link
|
||||||
|
all(".delete-link").last.click
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete
|
||||||
|
find("#delete-section").click
|
||||||
|
end
|
||||||
|
|
||||||
|
def confirm_delete
|
||||||
|
find(".dialog-container .btn-primary").click
|
||||||
|
end
|
||||||
|
|
||||||
|
def save
|
||||||
|
find("#save-section").click
|
||||||
|
end
|
||||||
|
|
||||||
|
def visible?
|
||||||
|
page.has_css?(".sidebar-section-form-modal")
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_disabled_save?
|
||||||
|
find_button("Save", disabled: true)
|
||||||
|
end
|
||||||
|
def has_enabled_save?
|
||||||
|
find_button("Save", disabled: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue