diff --git a/app/components/admin/custom_fields/custom_field_projects/row_component.rb b/app/components/admin/custom_fields/custom_field_projects/row_component.rb index 51ce950bbac0..d7b6ac1d62ee 100644 --- a/app/components/admin/custom_fields/custom_field_projects/row_component.rb +++ b/app/components/admin/custom_fields/custom_field_projects/row_component.rb @@ -57,11 +57,10 @@ def more_menu_detach_project def detach_from_project_url url_helpers.custom_field_project_path( custom_field_id: @table.params[:custom_field].id, - custom_fields_project: { project_id: project.id } + custom_fields_project: { project_id: project.id }, + page: current_page ) end - - def project = model.first end end end diff --git a/app/components/admin/custom_fields/custom_field_projects/table_component.rb b/app/components/admin/custom_fields/custom_field_projects/table_component.rb index 9ef7650dab1a..5eb0f19a4cd5 100644 --- a/app/components/admin/custom_fields/custom_field_projects/table_component.rb +++ b/app/components/admin/custom_fields/custom_field_projects/table_component.rb @@ -30,7 +30,7 @@ module Admin module CustomFields module CustomFieldProjects class TableComponent < Projects::TableComponent - include OpTurbo::Streamable + include ::Projects::Concerns::TableComponent::StreamablePaginationLinksConstraints def columns @columns ||= query.selects.reject { |select| select.is_a?(Queries::Selects::NotExistingSelect) } @@ -39,23 +39,6 @@ def columns def sortable? false end - - # @override optional_pagination_options are passed to the pagination_options - # which are passed to #pagination_links_full in pagination_helper.rb - # - # In Turbo streamable components, we need to be able to specify the url_for(action:) so that links are - # generated in the context of the component index action, instead of any turbo stream actions performing - # partial updates on the page. - # - # params[:url_for_action] is passed to the pagination_options making it's way down to any pagination links - # that are generated via link_to which calls url_for which uses the params[:url_for_action] to specify - # the controller action that link_to should use. - # - def optional_pagination_options - return super unless params[:url_for_action] - - super.merge(params: { action: params[:url_for_action] }) - end end end end diff --git a/app/components/projects/concerns/table_component/streamable_pagination_links_constraints.rb b/app/components/projects/concerns/table_component/streamable_pagination_links_constraints.rb new file mode 100644 index 000000000000..ddaedeaa11a7 --- /dev/null +++ b/app/components/projects/concerns/table_component/streamable_pagination_links_constraints.rb @@ -0,0 +1,55 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Projects + module Concerns + module TableComponent + module StreamablePaginationLinksConstraints + # @override optional_pagination_options are passed to the pagination_options + # which are passed to #pagination_links_full in pagination_helper.rb + # + # In Turbo streamable components, we need to be able to specify the url_for(action:) so that links are + # generated in the context of the component index action, instead of any turbo stream actions performing + # partial updates on the page. + # + # params[:url_for_action] is passed to the pagination_options making it's way down to any pagination links + # that are generated via link_to which calls url_for which uses the params[:url_for_action] to specify + # the controller action that link_to should use. + # + # data-turbo-action="advance" is added to the pagination links ensure links pushState to the browser + # + def optional_pagination_options + super.tap do |options| + options[:params] = { action: params[:url_for_action] } if params[:url_for_action] + options[:turbo_action] = "advance" + end + end + end + end + end +end diff --git a/app/components/projects/row_component.rb b/app/components/projects/row_component.rb index 70adc2566d7d..3f540eac0ecd 100644 --- a/app/components/projects/row_component.rb +++ b/app/components/projects/row_component.rb @@ -373,5 +373,9 @@ def user_can_view_project? def custom_field_column?(column) column.is_a?(::Queries::Projects::Selects::CustomField) end + + def current_page + table.model.current_page.to_s + end end end diff --git a/app/components/settings/project_custom_fields/project_custom_field_mapping/row_component.rb b/app/components/settings/project_custom_fields/project_custom_field_mapping/row_component.rb index f4645d2a23ff..5ee09b6baf3e 100644 --- a/app/components/settings/project_custom_fields/project_custom_field_mapping/row_component.rb +++ b/app/components/settings/project_custom_fields/project_custom_field_mapping/row_component.rb @@ -35,7 +35,8 @@ class RowComponent < Admin::CustomFields::CustomFieldProjects::RowComponent def detach_from_project_url url_helpers.unlink_admin_settings_project_custom_field_path( id: @table.params[:custom_field].id, - project_custom_field_project_mapping: { project_id: project.id } + project_custom_field_project_mapping: { project_id: project.id }, + page: current_page ) end end diff --git a/app/controllers/admin/custom_fields/custom_field_projects_controller.rb b/app/controllers/admin/custom_fields/custom_field_projects_controller.rb index 8f69e323e74a..e21966dd67b9 100644 --- a/app/controllers/admin/custom_fields/custom_field_projects_controller.rb +++ b/app/controllers/admin/custom_fields/custom_field_projects_controller.rb @@ -103,7 +103,7 @@ def render_project_list(url_for_action: action_name) update_via_turbo_stream( component: Admin::CustomFields::CustomFieldProjects::TableComponent.new( query: available_custom_fields_projects_query, - params: { custom_field: @custom_field, url_for_action: } + params: params.merge({ custom_field: @custom_field, url_for_action: }) ) ) end diff --git a/app/controllers/admin/settings/project_custom_fields_controller.rb b/app/controllers/admin/settings/project_custom_fields_controller.rb index b532962b12f5..5bccb8501fc7 100644 --- a/app/controllers/admin/settings/project_custom_fields_controller.rb +++ b/app/controllers/admin/settings/project_custom_fields_controller.rb @@ -158,7 +158,7 @@ def render_project_list(url_for_action: action_name) update_via_turbo_stream( component: Settings::ProjectCustomFields::ProjectCustomFieldMapping::TableComponent.new( query: project_custom_field_mappings_query, - params: { custom_field: @custom_field, url_for_action: } + params: params.merge({ custom_field: @custom_field, url_for_action: }) ) ) end diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb index c8a53aaadc42..28e9f79f3c80 100644 --- a/app/helpers/pagination_helper.rb +++ b/app/helpers/pagination_helper.rb @@ -202,6 +202,7 @@ def previous_or_next_page(page, text, class_suffix) def link(text, target, attributes) new_attributes = attributes.dup new_attributes["data-turbo-stream"] = true if turbo? + new_attributes["data-turbo-action"] = turbo_action if turbo_action.present? super(text, target, new_attributes) end @@ -210,6 +211,20 @@ def allowed_params @options[:allowed_params] # rubocop:disable Rails/HelperInstanceVariable end + # Customize the Turbo visit action for pagination links. Can be set to "advance" or "replace". + # "advance" - push a new entry onto the history stack. + # "replace" - replace the current history entry. + # See: https://turbo.hotwired.dev/reference/attributes + # + # Example: Promoting a Frame Navigation to a Page Visit + # By default navigation within a turbo frame does not change the rest of the browser's state, + # but you can promote a frame navigation a "Visit" by setting the turbo-action attribute to "advance". + # See: https://turbo.hotwired.dev/handbook/frames#promoting-a-frame-navigation-to-a-page-visit + # + def turbo_action + @options[:turbo_action] # rubocop:disable Rails/HelperInstanceVariable + end + def turbo? @options[:turbo] # rubocop:disable Rails/HelperInstanceVariable end diff --git a/app/seeders/oauth_applications_seeder.rb b/app/seeders/oauth_applications_seeder.rb index b8f88606db18..3dae4ae5db4d 100644 --- a/app/seeders/oauth_applications_seeder.rb +++ b/app/seeders/oauth_applications_seeder.rb @@ -52,7 +52,7 @@ def create_app OAuth::Applications::CreateService .new(user: User.system) .call( - enabled: true, + enabled: false, name: "OpenProject Mobile App", redirect_uri: "openprojectapp://oauth-callback", builtin: true, diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index c41256b40582..b7dab8db5892 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -11,7 +11,6 @@ <% content_for :content_body do %> <%= angular_component_tag "opce-notification-center" %> -dd <% end %> <% content_for :content_body_right do %> diff --git a/app/views/users/_consent.html.erb b/app/views/users/_consent.html.erb index 38901662394d..d7ac96b638b8 100644 --- a/app/views/users/_consent.html.erb +++ b/app/views/users/_consent.html.erb @@ -1,10 +1,12 @@

<%=t('consent.title')%>

+ <% consent_link = admin_settings_users_path(anchor: 'consent_settings') %> + <%= render ::Components::OnOffStatusComponent.new({ is_on: @user.consented_at.present?, on_text: format_time(@user.consented_at), - on_description: t('consent.user_has_consented'), + on_description: link_translate('consent.user_has_consented', links: { consent_settings: consent_link }), off_text: t(:label_never), - off_description: t('consent.not_yet_consented') + off_description: link_translate('consent.not_yet_consented', links: { consent_settings: consent_link }) }) %>
diff --git a/config/locales/en.yml b/config/locales/en.yml index 1ccb1f147eb4..9e02a20d6207 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1504,8 +1504,8 @@ en: failure_message: Consent failed, cannot proceed. title: User Consent decline_warning_message: You have declined to consent and have been logged out. - user_has_consented: User has consented to your configured statement at the given time. - not_yet_consented: User has not consented yet, will be requested upon next login. + user_has_consented: The user gave their consent to your [configured consent information text](consent_settings). + not_yet_consented: The user has not yet given their consent to your [configured consent information text](consent_settings). They will be reminded the next time they log in. contact_mail_instructions: Define the mail address that users can reach a data controller to perform data change or removal requests. contact_your_administrator: Please contact your administrator if you want to have your account deleted. contact_this_mail_address: Please contact %{mail_address} if you want to have your account deleted. @@ -3380,7 +3380,7 @@ en: setting_total_percent_complete_mode_work_weighted_average: "Weighted by work" setting_total_percent_complete_mode_work_weighted_average_caption_html: >- The total % Complete will be weighted against the Work of each work package in the hierarchy. - Work packages without Work will be ignored (current behaviour). + Work packages without Work will be ignored. setting_total_percent_complete_mode_simple_average: "Simple average" setting_total_percent_complete_mode_simple_average_caption_html: >- Work is ignored and the total % Complete will be a simple average of % Complete values of work packages in the hierarchy. diff --git a/frontend/src/stimulus/controllers/poll-for-changes.controller.ts b/frontend/src/stimulus/controllers/poll-for-changes.controller.ts index 811dc91926fd..ba6712479da3 100644 --- a/frontend/src/stimulus/controllers/poll-for-changes.controller.ts +++ b/frontend/src/stimulus/controllers/poll-for-changes.controller.ts @@ -39,9 +39,11 @@ export default class PollForChangesController extends ApplicationController { autoscrollEnabled: Boolean, }; - static targets = ['reloadButton']; + static targets = ['reloadButton', 'reference']; declare reloadButtonTarget:HTMLLinkElement; + declare referenceTarget:HTMLElement; + declare readonly hasReferenceTarget:boolean; declare referenceValue:string; declare urlValue:string; @@ -69,12 +71,20 @@ export default class PollForChangesController extends ApplicationController { clearInterval(this.interval); } + buildReference():string { + if (this.hasReferenceTarget) { + return this.referenceTarget.dataset.referenceValue as string; + } + + return this.referenceValue; + } + reloadButtonTargetConnected() { this.reloadButtonTarget.addEventListener('click', this.rememberCurrentScrollPosition.bind(this)); } triggerTurboStream() { - void fetch(`${this.urlValue}?reference=${this.referenceValue}`, { + void fetch(`${this.urlValue}?reference=${this.buildReference()}`, { headers: { Accept: 'text/vnd.turbo-stream.html', }, diff --git a/modules/meeting/app/components/meetings/header_component.html.erb b/modules/meeting/app/components/meetings/header_component.html.erb index c27700bbe91c..1d327152d6a9 100644 --- a/modules/meeting/app/components/meetings/header_component.html.erb +++ b/modules/meeting/app/components/meetings/header_component.html.erb @@ -1,6 +1,5 @@ <%= helpers.content_controller "poll-for-changes", - poll_for_changes_reference_value: @meeting.changed_hash, poll_for_changes_url_value: check_for_updates_meeting_path(@meeting), poll_for_changes_interval_value: check_for_updates_interval, poll_for_changes_autoscroll_enabled_value: true @@ -8,7 +7,11 @@ component_wrapper do render(Primer::OpenProject::PageHeader.new( test_selector: "meeting-page-header", - state: @state + state: @state, + data: { + poll_for_changes_target: "reference", + reference_value: @meeting.changed_hash + } )) do |header| header.with_title do |title| title.with_editable_form(model: @meeting, diff --git a/modules/storages/app/components/storages/project_storages/destroy_confirmation_dialog_component.html.erb b/modules/storages/app/components/storages/project_storages/destroy_confirmation_dialog_component.html.erb index 956475512f2a..cef571b12e07 100644 --- a/modules/storages/app/components/storages/project_storages/destroy_confirmation_dialog_component.html.erb +++ b/modules/storages/app/components/storages/project_storages/destroy_confirmation_dialog_component.html.erb @@ -31,7 +31,7 @@ See COPYRIGHT and LICENSE files for more details. component_wrapper do primer_form_with( model: @project_storage, - url: admin_settings_storage_project_storage_path(id: @project_storage), + url: admin_settings_storage_project_storage_path(id: @project_storage, page: current_page), data: { turbo: true, controller: 'disable-when-checked', diff --git a/modules/storages/app/components/storages/project_storages/destroy_confirmation_dialog_component.rb b/modules/storages/app/components/storages/project_storages/destroy_confirmation_dialog_component.rb index 228e6125fb47..ecd4b986e0d4 100644 --- a/modules/storages/app/components/storages/project_storages/destroy_confirmation_dialog_component.rb +++ b/modules/storages/app/components/storages/project_storages/destroy_confirmation_dialog_component.rb @@ -32,11 +32,12 @@ class DestroyConfirmationDialogComponent < ApplicationComponent include OpTurbo::Streamable include OpPrimer::ComponentHelpers - def initialize(storage:, project_storage:) + def initialize(storage:, project_storage:, params: {}) super @storage = storage @project_storage = project_storage + @params = params end def id @@ -56,6 +57,10 @@ def text text end + def current_page + @params[:page] + end + def confirmation_text I18n.t("project_storages.remove_project.dialog.confirmation_text") end diff --git a/modules/storages/app/components/storages/project_storages/projects/row_component.rb b/modules/storages/app/components/storages/project_storages/projects/row_component.rb index b144b519edd1..e1995faaeec4 100644 --- a/modules/storages/app/components/storages/project_storages/projects/row_component.rb +++ b/modules/storages/app/components/storages/project_storages/projects/row_component.rb @@ -68,7 +68,8 @@ def more_menu_detach_project icon: :trash, label: I18n.t("project_storages.remove_project.label"), href: destroy_confirmation_dialog_admin_settings_storage_project_storage_path( - id: project_storage.id + id: project_storage.id, + page: current_page ), data: { controller: "async-dialog" diff --git a/modules/storages/app/components/storages/project_storages/projects/table_component.rb b/modules/storages/app/components/storages/project_storages/projects/table_component.rb index 243ccbe46ab1..ea1db72e0516 100644 --- a/modules/storages/app/components/storages/project_storages/projects/table_component.rb +++ b/modules/storages/app/components/storages/project_storages/projects/table_component.rb @@ -32,7 +32,7 @@ # for every "column" defined below. module Storages::ProjectStorages::Projects class TableComponent < Projects::TableComponent - include OpTurbo::Streamable + include ::Projects::Concerns::TableComponent::StreamablePaginationLinksConstraints options :storage diff --git a/modules/storages/app/controllers/storages/admin/storages/project_storages_controller.rb b/modules/storages/app/controllers/storages/admin/storages/project_storages_controller.rb index 3e842e26be0f..f83ffe11d00b 100644 --- a/modules/storages/app/controllers/storages/admin/storages/project_storages_controller.rb +++ b/modules/storages/app/controllers/storages/admin/storages/project_storages_controller.rb @@ -117,7 +117,8 @@ def update def destroy_confirmation_dialog respond_with_dialog Storages::ProjectStorages::DestroyConfirmationDialogComponent.new( storage: @storage, - project_storage: @project_storage + project_storage: @project_storage, + params: ) end @@ -186,7 +187,7 @@ def update_project_list_via_turbo_stream(url_for_action: action_name) component: Storages::ProjectStorages::Projects::TableComponent.new( query: storage_projects_query, storage: @storage, - params: { url_for_action: } + params: params.merge({ url_for_action: }) ) ) end diff --git a/modules/storages/spec/features/storages/admin/project_storages_spec.rb b/modules/storages/spec/features/storages/admin/project_storages_spec.rb index f4ae5ee03f8c..9057b15def07 100644 --- a/modules/storages/spec/features/storages/admin/project_storages_spec.rb +++ b/modules/storages/spec/features/storages/admin/project_storages_spec.rb @@ -228,6 +228,10 @@ expect(page).to have_text(project.name) expect(page).to have_text(subproject.name) + + aggregate_failures "pagination links maintain the correct url" do + project_storages_index_page.expect_page_sizes(model: storage) + end end context "when the user does not select a folder" do @@ -297,6 +301,10 @@ project_storages_index_page.within_the_table_row_containing(project.name) do expect(page).to have_text("No specific folder") end + + aggregate_failures "pagination links maintain the correct url" do + project_storages_index_page.expect_page_sizes(model: storage) + end end context "when oauth access has not been granted and manual selection" do @@ -363,8 +371,15 @@ def call expect(page).to have_text(project.name) end - it "is possible to remove the project after checking the confirmation checkbox in the dialog" do - expect(page).to have_text(project.name) + it "is possible to remove the project after checking the confirmation checkbox in the dialog", + with_settings: { per_page_options: "2,5" } do + projects = create_list(:project, 4) + projects.each { |project| create(:project_storage, storage:, project:) } + + current_page = 3 + visit admin_settings_storage_project_storages_path(storage, page: current_page) + + project = project_storages_index_page.project_in_first_row(column_text_separator: "\t") project_storages_index_page.click_menu_item_of("Remove project", project) # The original DeleteService would try to remove actual files from actual storages, @@ -388,6 +403,10 @@ def call expect(page).to have_no_selector("dialog") expect(page).to have_text("Successful deletion.") expect(page).to have_no_text(project.name) + + aggregate_failures "pagination links maintain the correct url" do + project_storages_index_page.expect_page_links(model: storage, current_page:) + end end end end diff --git a/modules/storages/spec/support/pages/admin/storages/project_storages/project_storages_index.rb b/modules/storages/spec/support/pages/admin/storages/project_storages/project_storages_index.rb index b1008cb411ab..ebd99db68b2d 100644 --- a/modules/storages/spec/support/pages/admin/storages/project_storages/project_storages_index.rb +++ b/modules/storages/spec/support/pages/admin/storages/project_storages/project_storages_index.rb @@ -33,7 +33,7 @@ module Admin module Storages module ProjectStorages class Index < ::Pages::Projects::Index - def path + def path(storage) "/admin/settings/storages/#{storage.id}/project_storages" end diff --git a/spec/features/admin/custom_fields/custom_fields_project_spec.rb b/spec/features/admin/custom_fields/custom_fields_project_spec.rb index 8081ae4c1534..7726caf0d324 100644 --- a/spec/features/admin/custom_fields/custom_fields_project_spec.rb +++ b/spec/features/admin/custom_fields/custom_fields_project_spec.rb @@ -115,38 +115,25 @@ expect(page).to have_text(subproject.name) aggregate_failures "pagination links maintain the correct url" do - within ".op-pagination" do - pagination_links = page.all(".op-pagination--item-link") - expect(pagination_links.size).to be_positive - - pagination_links.each do |pagination_link| - uri = URI.parse(pagination_link["href"]) - expect(uri.path).to eq(custom_field_projects_path(custom_field)) - end - end + custom_field_projects_page.expect_page_sizes(model: custom_field) end end - it "allows unlinking a project from a custom field" do - project = create(:project) - create(:custom_fields_project, custom_field:, project:) + it "allows unlinking a project from a custom field", with_settings: { per_page_options: "2,5" } do + projects = create_list(:project, 4) + projects.each { |project| create(:custom_fields_project, custom_field:, project:) } - visit custom_field_projects_path(custom_field) + current_page = 3 + visit custom_field_projects_path(custom_field, page: current_page) + project = custom_field_projects_page.project_in_first_row custom_field_projects_page.click_menu_item_of("Remove from project", project) expect(page).to have_no_text(project.name) aggregate_failures "pagination links maintain the correct url after unlinking is done" do - within ".op-pagination" do - pagination_links = page.all(".op-pagination--item-link") - expect(pagination_links.size).to be_positive - - pagination_links.each do |pagination_link| - uri = URI.parse(pagination_link["href"]) - expect(uri.path).to eq(custom_field_projects_path(custom_field)) - end - end + custom_field_projects_page.expect_page_links(model: custom_field, current_page:) + custom_field_projects_page.expect_current_page_number(current_page) end end diff --git a/spec/features/admin/oauth/oauth_applications_management_spec.rb b/spec/features/admin/oauth/oauth_applications_management_spec.rb index fd7c4bbbb1c6..7d52221b373e 100644 --- a/spec/features/admin/oauth/oauth_applications_management_spec.rb +++ b/spec/features/admin/oauth/oauth_applications_management_spec.rb @@ -106,23 +106,23 @@ within_test_selector("op-admin-oauth--built-in-applications") do expect(page).to have_test_selector("op-admin-oauth--application", count: 1) expect(page).to have_link(text: "OpenProject Mobile App") - expect(page).to have_test_selector("op-admin-oauth--application-enabled-toggle-switch", text: "On") + expect(page).to have_test_selector("op-admin-oauth--application-enabled-toggle-switch", text: "Off") find_test_selector("op-admin-oauth--application-enabled-toggle-switch").click expect(page).not_to have_test_selector("op-admin-oauth--application-enabled-toggle-switch", text: "Loading") - expect(page).to have_test_selector("op-admin-oauth--application-enabled-toggle-switch", text: "Off") + expect(page).to have_test_selector("op-admin-oauth--application-enabled-toggle-switch", text: "On") app.reload expect(app).to be_builtin - expect(app).not_to be_enabled + expect(app).to be_enabled find_test_selector("op-admin-oauth--application-enabled-toggle-switch").click expect(page).not_to have_test_selector("op-admin-oauth--application-enabled-toggle-switch", text: "Loading") - expect(page).to have_test_selector("op-admin-oauth--application-enabled-toggle-switch", text: "On") + expect(page).to have_test_selector("op-admin-oauth--application-enabled-toggle-switch", text: "Off") app.reload expect(app).to be_builtin - expect(app).to be_enabled + expect(app).not_to be_enabled click_on "OpenProject Mobile App" end diff --git a/spec/features/admin/project_custom_fields/project_mappings_spec.rb b/spec/features/admin/project_custom_fields/project_mappings_spec.rb index ea0eee6e21e6..97769b0e80a9 100644 --- a/spec/features/admin/project_custom_fields/project_mappings_spec.rb +++ b/spec/features/admin/project_custom_fields/project_mappings_spec.rb @@ -119,38 +119,25 @@ expect(page).to have_text(subproject.name) aggregate_failures "pagination links maintain the correct url" do - within ".op-pagination" do - pagination_links = page.all(".op-pagination--item-link") - expect(pagination_links.size).to be_positive - - pagination_links.each do |pagination_link| - uri = URI.parse(pagination_link["href"]) - expect(uri.path).to eq(project_mappings_admin_settings_project_custom_field_path(project_custom_field)) - end - end + project_custom_field_mappings_page.expect_page_sizes(model: project_custom_field) end end - it "allows unlinking a project from a custom field" do - project = create(:project) - create(:project_custom_field_project_mapping, project_custom_field:, project:) + it "allows unlinking a project from a custom field", with_settings: { per_page_options: "2,5" } do + projects = create_list(:project, 4) + projects.each { |project| create(:project_custom_field_project_mapping, project_custom_field:, project:) } - visit project_mappings_admin_settings_project_custom_field_path(project_custom_field) + current_page = 3 + visit project_mappings_admin_settings_project_custom_field_path(project_custom_field, page: current_page) + project = project_custom_field_mappings_page.project_in_first_row project_custom_field_mappings_page.click_menu_item_of("Remove from project", project) expect(page).to have_no_text(project.name) aggregate_failures "pagination links maintain the correct url after unlinking is done" do - within ".op-pagination" do - pagination_links = page.all(".op-pagination--item-link") - expect(pagination_links.size).to be_positive - - pagination_links.each do |pagination_link| - uri = URI.parse(pagination_link["href"]) - expect(uri.path).to eq(project_mappings_admin_settings_project_custom_field_path(project_custom_field)) - end - end + project_custom_field_mappings_page.expect_page_links(model: project_custom_field, current_page:) + project_custom_field_mappings_page.expect_current_page_number(current_page) end end diff --git a/spec/support/pages/admin/custom_fields/custom_fields_projects/custom_field_projects_index.rb b/spec/support/pages/admin/custom_fields/custom_fields_projects/custom_field_projects_index.rb index a170b17e58ef..78fb409497e0 100644 --- a/spec/support/pages/admin/custom_fields/custom_fields_projects/custom_field_projects_index.rb +++ b/spec/support/pages/admin/custom_fields/custom_fields_projects/custom_field_projects_index.rb @@ -33,17 +33,19 @@ module Admin module CustomFields module CustomFieldsProjects class CustomFieldProjectsIndex < ::Pages::Projects::Index - def path + def path(custom_field) "/custom_fields/#{custom_field.id}/projects" end def within_row(project) - row = page.find("#admin-custom-fields-custom-field-projects-row-component-project-#{project.id}") + row = page.find("#{row_id_prefix}-#{project.id}") row.hover within row do yield row end end + + def row_id_prefix = "#admin-custom-fields-custom-field-projects-row-component-project" end end end diff --git a/spec/support/pages/admin/settings/project_custom_fields/project_custom_field_mappings_index.rb b/spec/support/pages/admin/settings/project_custom_fields/project_custom_field_mappings_index.rb index ab41fabafb8f..136762591a4c 100644 --- a/spec/support/pages/admin/settings/project_custom_fields/project_custom_field_mappings_index.rb +++ b/spec/support/pages/admin/settings/project_custom_fields/project_custom_field_mappings_index.rb @@ -32,18 +32,12 @@ module Pages module Admin module Settings module ProjectCustomFields - class ProjectCustomFieldMappingsIndex < ::Pages::Projects::Index - def path + class ProjectCustomFieldMappingsIndex < ::Pages::Admin::CustomFields::CustomFieldsProjects::CustomFieldProjectsIndex + def path(project_custom_field) "/admin/settings/project_custom_fields/#{project_custom_field.id}/project_mappings" end - def within_row(project) - row = page.find("#settings-project-custom-fields-project-custom-field-mapping-row-component-project-#{project.id}") - row.hover - within row do - yield row - end - end + def row_id_prefix = "#settings-project-custom-fields-project-custom-field-mapping-row-component-project" end end end diff --git a/spec/support/pages/projects/index.rb b/spec/support/pages/projects/index.rb index 66810eb38d40..d83f6f04ac43 100644 --- a/spec/support/pages/projects/index.rb +++ b/spec/support/pages/projects/index.rb @@ -33,7 +33,7 @@ module Projects class Index < ::Pages::Page include ::Components::Autocompleter::NgSelectAutocompleteHelpers - def path + def path(*) "/projects" end @@ -127,6 +127,39 @@ def expect_page_link(text) end end + def expect_page_links(model:, current_page: 1) + within ".op-pagination--pages" do + pagination_links = page.all(".op-pagination--item-link") + expect(pagination_links.size).to be_positive + + page_number_links = pagination_links.reject { |link| link.text =~ /previous|next/i } + page_number_links.each.with_index(1) do |pagination_link, page_number| + uri = URI.parse(pagination_link["href"]) + expect(uri.path).to eq(path(model)) + expect(uri.query).to include("page=#{page_number}") + end + + if current_page > 1 + expect(page).to have_link("Previous", href: "#{path(model)}?#{{ page: current_page - 1 }.to_query}") + else + expect(page).to have_link("Next", href: "#{path(model)}?#{{ page: current_page + 1 }.to_query}") + end + end + end + + def expect_page_sizes(model:) + within ".op-pagination--options" do + pagination_links = page.all(".op-pagination--item-link") + expect(pagination_links.size).to be_positive + expect(page).to have_css(".op-pagination--item_current") + + pagination_links.each do |pagination_link| + uri = URI.parse(pagination_link["href"]) + expect(uri.path).to eq(path(model)) + end + end + end + def expect_filters_container_toggled expect(page).to have_css(".op-filters-form") end @@ -531,6 +564,11 @@ def within_table(&) within "#project-table", & end + def project_in_first_row(column_text_separator: "\n") + first_row = within("#projects-table") { find(".op-project-row-component", match: :first) } + Project.find_by!(name: first_row.text.split(column_text_separator).first) + end + def within_row(project) row = page.find("#project-#{project.id}") row.hover