diff --git a/app/assets/javascripts/discourse/app/components/sidebar/user/custom-sections.hbs b/app/assets/javascripts/discourse/app/components/sidebar/user/custom-sections.hbs index ecb0854d51f..a0536bc6462 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/user/custom-sections.hbs +++ b/app/assets/javascripts/discourse/app/components/sidebar/user/custom-sections.hbs @@ -2,14 +2,9 @@ {{#each this.sections as |section|}} {{#each section.links as |link|}} diff --git a/app/assets/javascripts/discourse/app/components/sidebar/user/custom-sections.js b/app/assets/javascripts/discourse/app/components/sidebar/user/custom-sections.js index c2421eec208..1d3f943bbc5 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/user/custom-sections.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/user/custom-sections.js @@ -1,15 +1,43 @@ import Component from "@glimmer/component"; -import { action } from "@ember/object"; import showModal from "discourse/lib/show-modal"; import { inject as service } from "@ember/service"; import RouteInfoHelper from "discourse/lib/sidebar/route-info-helper"; +import I18n from "I18n"; +import { ajax } from "discourse/lib/ajax"; +import { iconHTML } from "discourse-common/lib/icon-library"; +import { htmlSafe } from "@ember/template"; +import { bind } from "discourse-common/utils/decorators"; export default class SidebarUserCustomSections extends Component { @service currentUser; @service router; + @service messageBus; + + constructor() { + super(...arguments); + this.messageBus.subscribe("/refresh-sidebar-sections", this._refresh); + } + + willDestroy() { + this.messageBus.unsubscribe("/refresh-sidebar-sections"); + } get sections() { this.currentUser.sidebarSections.forEach((section) => { + if (!section.public || this.currentUser.staff) { + section.headerActions = [ + { + action: () => { + return showModal("sidebar-section-form", { model: section }); + }, + title: I18n.t("sidebar.sections.custom.edit"), + }, + ]; + } + section.decoratedTitle = + section.public && this.currentUser.staff + ? htmlSafe(`${iconHTML("globe")} ${section.title}`) + : section.title; section.links.forEach((link) => { const routeInfoHelper = new RouteInfoHelper(this.router, link.value); link.route = routeInfoHelper.route; @@ -20,8 +48,10 @@ export default class SidebarUserCustomSections extends Component { return this.currentUser.sidebarSections; } - @action - editSection(section) { - showModal("sidebar-section-form", { model: section }); + @bind + _refresh() { + return ajax("/sidebar_sections.json", {}).then((json) => { + this.currentUser.set("sidebar_sections", json.sidebar_sections); + }); } } 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 33623e16e7a..ec2c8605cbd 100644 --- a/app/assets/javascripts/discourse/app/controllers/sidebar-section-form.js +++ b/app/assets/javascripts/discourse/app/controllers/sidebar-section-form.js @@ -13,8 +13,9 @@ class Section { @tracked title; @tracked links; - constructor({ title, links, id }) { + constructor({ title, links, id, publicSection }) { this.title = title; + this.public = publicSection; this.links = links; this.id = id; } @@ -112,6 +113,7 @@ export default Controller.extend(ModalFunctionality, { if (this.model) { return new Section({ title: this.model.title, + publicSection: this.model.public, links: A( this.model.links.map( (link) => @@ -140,6 +142,7 @@ export default Controller.extend(ModalFunctionality, { dataType: "json", data: JSON.stringify({ title: this.model.title, + public: this.model.public, links: this.model.links.map((link) => { return { icon: link.icon, @@ -168,6 +171,7 @@ export default Controller.extend(ModalFunctionality, { dataType: "json", data: JSON.stringify({ title: this.model.title, + public: this.model.public, links: this.model.links.map((link) => { return { id: link.id, 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 3e21bd08d3e..7c9962eedb7 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 @@ -69,6 +69,18 @@ @icon="plus" @label="sidebar.sections.custom.links.add" /> + {{#if this.currentUser.staff}} +
+ +
+ {{/if}} diff --git a/app/assets/stylesheets/common/base/sidebar.scss b/app/assets/stylesheets/common/base/sidebar.scss index c4dd9c8cc74..3c06599a647 100644 --- a/app/assets/stylesheets/common/base/sidebar.scss +++ b/app/assets/stylesheets/common/base/sidebar.scss @@ -130,6 +130,14 @@ .sidebar-section-wrapper { padding-bottom: 0; } + .d-icon-globe { + position: absolute; + left: 0.5em; + height: 0.75em; + width: 0.75em; + margin-top: 0.15em; + align-items: center; + } } .sidebar-section-form-modal { .modal-inner-container { @@ -138,7 +146,7 @@ form { margin-bottom: 0; } - input { + .input-group input { width: 100%; } input.warning { diff --git a/app/controllers/sidebar_sections_controller.rb b/app/controllers/sidebar_sections_controller.rb index 42c083119e8..d5c10d3860a 100644 --- a/app/controllers/sidebar_sections_controller.rb +++ b/app/controllers/sidebar_sections_controller.rb @@ -3,6 +3,16 @@ class SidebarSectionsController < ApplicationController requires_login before_action :check_if_member_of_group + before_action :check_access_if_public + + def index + sections = + SidebarSection + .where("public OR user_id = ?", current_user.id) + .order("(public IS TRUE) DESC") + .map { |section| SidebarSectionSerializer.new(section, root: false) } + render json: sections + end def create sidebar_section = @@ -10,6 +20,15 @@ class SidebarSectionsController < ApplicationController section_params.merge(user: current_user, sidebar_urls_attributes: links_params), ) + if sidebar_section.public? + StaffActionLogger.new(current_user).log_create_public_sidebar_section(sidebar_section) + MessageBus.publish( + "/refresh-sidebar-sections", + nil, + group_ids: SiteSetting.enable_custom_sidebar_sections_map, + ) + end + render json: SidebarSectionSerializer.new(sidebar_section) rescue ActiveRecord::RecordInvalid => e render_json_error(e.record.errors.full_messages.first) @@ -21,6 +40,15 @@ class SidebarSectionsController < ApplicationController sidebar_section.update!(section_params.merge(sidebar_urls_attributes: links_params)) + if sidebar_section.public? + StaffActionLogger.new(current_user).log_update_public_sidebar_section(sidebar_section) + MessageBus.publish( + "/refresh-sidebar-sections", + nil, + group_ids: SiteSetting.enable_custom_sidebar_sections_map, + ) + end + render json: SidebarSectionSerializer.new(sidebar_section) rescue ActiveRecord::RecordInvalid => e render_json_error(e.record.errors.full_messages.first) @@ -32,13 +60,22 @@ class SidebarSectionsController < ApplicationController sidebar_section = SidebarSection.find_by(id: section_params["id"]) @guardian.ensure_can_delete!(sidebar_section) sidebar_section.destroy! + + if sidebar_section.public? + StaffActionLogger.new(current_user).log_destroy_public_sidebar_section(sidebar_section) + MessageBus.publish( + "/refresh-sidebar-sections", + nil, + group_ids: SiteSetting.enable_custom_sidebar_sections_map, + ) + end render json: SidebarSectionSerializer.new(sidebar_section) rescue Discourse::InvalidAccess render json: failed_json, status: 403 end def section_params - params.permit(:id, :title) + params.permit(:id, :title, :public) end def links_params @@ -52,4 +89,11 @@ class SidebarSectionsController < ApplicationController raise Discourse::InvalidAccess end end + + private + + def check_access_if_public + return true if !params[:public] + raise Discourse::InvalidAccess.new if !guardian.can_create_public_sidebar_section? + end end diff --git a/app/models/sidebar_section.rb b/app/models/sidebar_section.rb index 65fdc0091cf..8b83a6d984e 100644 --- a/app/models/sidebar_section.rb +++ b/app/models/sidebar_section.rb @@ -22,6 +22,7 @@ end # title :string(30) not null # created_at :datetime not null # updated_at :datetime not null +# public :boolean default(FALSE), not null # # Indexes # diff --git a/app/models/user_history.rb b/app/models/user_history.rb index 53cae312490..6a3918b482f 100644 --- a/app/models/user_history.rb +++ b/app/models/user_history.rb @@ -120,6 +120,9 @@ class UserHistory < ActiveRecord::Base watched_word_destroy: 98, delete_group: 99, permanently_delete_post_revisions: 100, + create_public_sidebar_section: 101, + update_public_sidebar_section: 102, + destroy_public_sidebar_section: 103, ) end @@ -215,6 +218,9 @@ class UserHistory < ActiveRecord::Base watched_word_destroy delete_group permanently_delete_post_revisions + create_public_sidebar_section + update_public_sidebar_section + destroy_public_sidebar_section ] end diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index bf0cdcb67cd..8a440570f9d 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -77,6 +77,13 @@ class CurrentUserSerializer < BasicUserSerializer has_one :user_option, embed: :object, serializer: CurrentUserOptionSerializer + def sidebar_sections + SidebarSection + .where("public OR user_id = ?", object.id) + .order("(public IS TRUE) DESC") + .map { |section| SidebarSectionSerializer.new(section, root: false) } + end + def groups owned_group_ids = GroupUser.where(user_id: id, owner: true).pluck(:group_id).to_set diff --git a/app/serializers/sidebar_section_serializer.rb b/app/serializers/sidebar_section_serializer.rb index 3f4c2828d72..27b16251ba2 100644 --- a/app/serializers/sidebar_section_serializer.rb +++ b/app/serializers/sidebar_section_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class SidebarSectionSerializer < ApplicationSerializer - attributes :id, :title, :links, :slug + attributes :id, :title, :links, :slug, :public def links object.sidebar_section_links.map(&:linkable) diff --git a/app/services/staff_action_logger.rb b/app/services/staff_action_logger.rb index 9de363d855a..ac16718bf9a 100644 --- a/app/services/staff_action_logger.rb +++ b/app/services/staff_action_logger.rb @@ -964,6 +964,32 @@ class StaffActionLogger ) end + def log_create_public_sidebar_section(section) + UserHistory.create!( + action: UserHistory.actions[:create_public_sidebar_section], + acting_user_id: @admin.id, + subject: section.title, + details: custom_section_details(section), + ) + end + + def log_update_public_sidebar_section(section) + UserHistory.create!( + action: UserHistory.actions[:update_public_sidebar_section], + acting_user_id: @admin.id, + subject: section.title, + details: custom_section_details(section), + ) + end + + def log_destroy_public_sidebar_section(section) + UserHistory.create!( + action: UserHistory.actions[:destroy_public_sidebar_section], + acting_user_id: @admin.id, + subject: section.title, + ) + end + private def get_changes(changes) @@ -990,4 +1016,9 @@ class StaffActionLogger def validate_category(category) raise Discourse::InvalidParameters.new(:category) unless category && category.is_a?(Category) end + + def custom_section_details(section) + urls = section.sidebar_urls.map { |url| "#{url.name} - #{url.value}" } + "links: #{urls.join(", ")}" + end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index f1c8d8c88bb..2cc1cbd7731 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -4373,6 +4373,7 @@ en: save: "Save" delete: "Delete" delete_confirm: "Are you sure you want to delete this section?" + public: "Make this section public and visible to everyone" links: icon: "Icon" name: "Name" @@ -5426,6 +5427,9 @@ en: delete_group: "delete group" watched_word_create: "add watched word" watched_word_destroy: "delete watched word" + create_public_sidebar_section: "create public sidebar section" + update_public_sidebar_section: "update public sidebar section" + destroy_public_sidebar_section: "destroy public sidebar section" screened_emails: title: "Screened Emails" description: "When someone tries to create a new account, the following email addresses will be checked and the registration will be blocked, or some other action performed." diff --git a/config/routes.rb b/config/routes.rb index eab836fb21b..01a5fe03a33 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1589,7 +1589,7 @@ Discourse::Application.routes.draw do put "user-status" => "user_status#set" delete "user-status" => "user_status#clear" - resources :sidebar_sections, only: %i[create update destroy] + resources :sidebar_sections, only: %i[index create update destroy] get "*url", to: "permalinks#show", constraints: PermalinkConstraint.new end diff --git a/db/migrate/20230214044350_add_public_to_sidebar_sections.rb b/db/migrate/20230214044350_add_public_to_sidebar_sections.rb new file mode 100644 index 00000000000..db7c5387fd3 --- /dev/null +++ b/db/migrate/20230214044350_add_public_to_sidebar_sections.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddPublicToSidebarSections < ActiveRecord::Migration[7.0] + def change + add_column :sidebar_sections, :public, :boolean, null: false, default: false + end +end diff --git a/lib/guardian/sidebar_guardian.rb b/lib/guardian/sidebar_guardian.rb index fc57666399a..5f55d4f465d 100644 --- a/lib/guardian/sidebar_guardian.rb +++ b/lib/guardian/sidebar_guardian.rb @@ -1,11 +1,17 @@ # frozen_string_literal: true module SidebarGuardian + def can_create_public_sidebar_section? + @user.staff? + end + def can_edit_sidebar_section?(sidebar_section) + return @user.staff? if sidebar_section.public? is_my_own?(sidebar_section) end def can_delete_sidebar_section?(sidebar_section) + return @user.staff? if sidebar_section.public? is_my_own?(sidebar_section) end end diff --git a/spec/requests/sidebar_sections_controller_spec.rb b/spec/requests/sidebar_sections_controller_spec.rb index 07d18e516f4..4c8ebf12cf3 100644 --- a/spec/requests/sidebar_sections_controller_spec.rb +++ b/spec/requests/sidebar_sections_controller_spec.rb @@ -2,14 +2,39 @@ RSpec.describe SidebarSectionsController do fab!(:user) { Fabricate(:user) } + fab!(:admin) { Fabricate(:admin) } before do ### TODO remove when enable_custom_sidebar_sections SiteSetting is removed group = Fabricate(:group) Fabricate(:group_user, group: group, user: user) + Fabricate(:group_user, group: group, user: admin) SiteSetting.enable_custom_sidebar_sections = group.id.to_s end + describe "#index" do + fab!(:sidebar_section) { Fabricate(:sidebar_section, title: "private section", user: user) } + fab!(:sidebar_url_1) { Fabricate(:sidebar_url, name: "tags", value: "/tags") } + fab!(:section_link_1) do + Fabricate(:sidebar_section_link, sidebar_section: sidebar_section, linkable: sidebar_url_1) + end + fab!(:sidebar_section_2) do + Fabricate(:sidebar_section, title: "public section", user: admin, public: true) + end + fab!(:section_link_2) do + Fabricate(:sidebar_section_link, sidebar_section: sidebar_section, linkable: sidebar_url_1) + end + + it "returns public and private sections" do + sign_in(user) + get "/sidebar_sections.json" + expect(response.status).to eq(200) + expect(response.parsed_body["sidebar_sections"].map { |section| section["title"] }).to eq( + ["public section", "private section"], + ) + end + end + describe "#create" do it "is not available for anonymous" do post "/sidebar_sections.json", @@ -20,7 +45,6 @@ RSpec.describe SidebarSectionsController do { icon: "link", name: "tags", value: "/tags" }, ], } - expect(response.status).to eq(403) end @@ -42,6 +66,8 @@ RSpec.describe SidebarSectionsController do expect(sidebar_section.title).to eq("custom section") expect(sidebar_section.user).to eq(user) + expect(sidebar_section.public).to be false + expect(UserHistory.count).to eq(0) expect(sidebar_section.sidebar_urls.count).to eq(2) expect(sidebar_section.sidebar_urls.first.icon).to eq("link") expect(sidebar_section.sidebar_urls.first.name).to eq("categories") @@ -50,6 +76,43 @@ RSpec.describe SidebarSectionsController do expect(sidebar_section.sidebar_urls.second.name).to eq("tags") expect(sidebar_section.sidebar_urls.second.value).to eq("/tags") end + + it "does not allow regular user to create public section" do + sign_in(user) + post "/sidebar_sections.json", + params: { + title: "custom section", + public: true, + links: [ + { icon: "link", name: "categories", value: "/categories" }, + { icon: "address-book", name: "tags", value: "/tags" }, + ], + } + expect(response.status).to eq(403) + end + + it "allows admin to create public section" do + sign_in(admin) + post "/sidebar_sections.json", + params: { + title: "custom section", + public: true, + links: [ + { icon: "link", name: "categories", value: "/categories" }, + { icon: "address-book", name: "tags", value: "/tags" }, + ], + } + expect(response.status).to eq(200) + + sidebar_section = SidebarSection.last + expect(sidebar_section.title).to eq("custom section") + expect(sidebar_section.public).to be true + + user_history = UserHistory.last + expect(user_history.action).to eq(UserHistory.actions[:create_public_sidebar_section]) + expect(user_history.subject).to eq("custom section") + expect(user_history.details).to eq("links: categories - /categories, tags - /tags") + end end describe "#update" do @@ -77,12 +140,39 @@ RSpec.describe SidebarSectionsController do expect(response.status).to eq(200) expect(sidebar_section.reload.title).to eq("custom section edited") + expect(UserHistory.count).to eq(0) 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 "allows admin to update public section and links" do + sign_in(admin) + sidebar_section.update!(user: admin, public: true) + put "/sidebar_sections/#{sidebar_section.id}.json", + params: { + title: "custom section edited", + links: [ + { icon: "link", id: sidebar_url_1.id, name: "latest", value: "/latest" }, + { icon: "link", 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) + + user_history = UserHistory.last + expect(user_history.action).to eq(UserHistory.actions[:update_public_sidebar_section]) + expect(user_history.subject).to eq("custom section edited") + expect(user_history.details).to eq("links: latest - /latest") + 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") @@ -97,6 +187,20 @@ RSpec.describe SidebarSectionsController do expect(response.status).to eq(403) end + it "doesn't allow to edit public sections" do + sign_in(user) + sidebar_section.update!(public: true) + put "/sidebar_sections/#{sidebar_section.id}.json", + params: { + title: "custom section edited", + links: [ + { icon: "link", id: sidebar_url_1.id, name: "latest", value: "/latest" }, + { icon: "link", id: sidebar_url_2.id, name: "tags", value: "/tags", _destroy: "1" }, + ], + } + 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( @@ -127,6 +231,22 @@ RSpec.describe SidebarSectionsController do expect(response.status).to eq(200) expect { sidebar_section.reload }.to raise_error(ActiveRecord::RecordNotFound) + + expect(UserHistory.count).to eq(0) + end + + it "allows admin to delete public section" do + sign_in(admin) + sidebar_section.update!(user: admin, public: true) + delete "/sidebar_sections/#{sidebar_section.id}.json" + + expect(response.status).to eq(200) + + expect { sidebar_section.reload }.to raise_error(ActiveRecord::RecordNotFound) + + user_history = UserHistory.last + expect(user_history.action).to eq(UserHistory.actions[:destroy_public_sidebar_section]) + expect(user_history.subject).to eq("Sidebar section") end it "doesn't allow to delete other's sidebar section" do @@ -136,5 +256,13 @@ RSpec.describe SidebarSectionsController do expect(response.status).to eq(403) end + + it "doesn't allow to delete public sidebar section" do + sign_in(user) + sidebar_section.update!(public: true) + delete "/sidebar_sections/#{sidebar_section.id}.json" + + expect(response.status).to eq(403) + end end end diff --git a/spec/system/custom_sidebar_sections_spec.rb b/spec/system/custom_sidebar_sections_spec.rb index 390a1f2ae0a..4d54a634638 100644 --- a/spec/system/custom_sidebar_sections_spec.rb +++ b/spec/system/custom_sidebar_sections_spec.rb @@ -2,6 +2,7 @@ describe "Custom sidebar sections", type: :system, js: true do fab!(:user) { Fabricate(:user) } + fab!(:admin) { Fabricate(:admin) } let(:section_modal) { PageObjects::Modals::SidebarSectionForm.new } let(:sidebar) { PageObjects::Components::Sidebar.new } @@ -9,6 +10,7 @@ describe "Custom sidebar sections", type: :system, js: true do ### TODO remove when enable_custom_sidebar_sections SiteSetting is removed group = Fabricate(:group) Fabricate(:group_user, group: group, user: user) + Fabricate(:group_user, group: group, user: admin) SiteSetting.enable_custom_sidebar_sections = group.id.to_s sign_in user end @@ -55,6 +57,23 @@ describe "Custom sidebar sections", type: :system, js: true do expect(page).not_to have_link("Sidebar Categories") end + it "does not allow the user to edit public section" do + sidebar_section = Fabricate(:sidebar_section, title: "Public section", user: user, public: true) + 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") + + expect(page).to have_button("Public section") + find(".sidebar-section-public-section").hover + expect(page).not_to have_css( + ".sidebar-section-public-section button.sidebar-section-header-button", + ) + expect(page).not_to have_css(".sidebar-section-public-section .d-icon-globe") + 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") @@ -69,4 +88,31 @@ describe "Custom sidebar sections", type: :system, js: true do expect(page).not_to have_button("My section") end + + it "allows admin to create, edit and delete public section" do + sign_in admin + visit("/latest") + sidebar.open_new_custom_section + + section_modal.fill_name("Public section") + section_modal.fill_link("Sidebar Tags", "/tags") + section_modal.mark_as_public + section_modal.save + + expect(page).to have_button("Public section") + expect(page).to have_link("Sidebar Tags") + expect(page).to have_css(".sidebar-section-public-section .d-icon-globe") + + sidebar.edit_custom_section("Public section") + section_modal.fill_name("Edited public section") + section_modal.save + + expect(page).to have_button("Edited public section") + + sidebar.edit_custom_section("Edited public section") + section_modal.delete + section_modal.confirm_delete + + expect(page).not_to have_button("Edited public section") + end end diff --git a/spec/system/page_objects/modals/sidebar_section_form.rb b/spec/system/page_objects/modals/sidebar_section_form.rb index 44fd273c827..6b789a7f9a7 100644 --- a/spec/system/page_objects/modals/sidebar_section_form.rb +++ b/spec/system/page_objects/modals/sidebar_section_form.rb @@ -12,6 +12,10 @@ module PageObjects fill_in "link-url", with: url, match: :first end + def mark_as_public + find(".modal .mark-public").click + end + def remove_last_link all(".delete-link").last.click end