From 296787dde07ea43e0632694a80f944e6620569f6 Mon Sep 17 00:00:00 2001 From: Mir Bhatia Date: Thu, 5 Dec 2024 11:02:02 +0100 Subject: [PATCH 1/4] [#59883] Pagination for recurring meetings https://community.openproject.org/work_packages/59883 From 81eaf30d2c5b8ec5a8e8364f5cccaf60cf5093ee Mon Sep 17 00:00:00 2001 From: Mir Bhatia Date: Fri, 6 Dec 2024 12:00:36 +0100 Subject: [PATCH 2/4] Add 'show more' footer --- .../border_box_table_component.html.erb | 6 ++ .../op_primer/border_box_table_component.rb | 8 +++ .../footer_component.html.erb | 56 +++++++++++++++++++ .../recurring_meetings/footer_component.rb | 56 +++++++++++++++++++ .../recurring_meetings/table_component.rb | 28 +++++++++- .../recurring_meetings_controller.rb | 24 ++++---- .../app/helpers/recurring_meetings_helper.rb | 37 ++++++++++++ .../views/recurring_meetings/show.html.erb | 3 +- modules/meeting/config/locales/en.yml | 5 ++ 9 files changed, 209 insertions(+), 14 deletions(-) create mode 100644 modules/meeting/app/components/recurring_meetings/footer_component.html.erb create mode 100644 modules/meeting/app/components/recurring_meetings/footer_component.rb create mode 100644 modules/meeting/app/helpers/recurring_meetings_helper.rb diff --git a/app/components/op_primer/border_box_table_component.html.erb b/app/components/op_primer/border_box_table_component.html.erb index 4c96232aa95f..a026da0f9aa1 100644 --- a/app/components/op_primer/border_box_table_component.html.erb +++ b/app/components/op_primer/border_box_table_component.html.erb @@ -58,6 +58,12 @@ See COPYRIGHT and LICENSE files for more details. end end end + + if has_footer? + component.with_footer(classes: grid_class, color: :muted) do + footer + end + end end %> diff --git a/app/components/op_primer/border_box_table_component.rb b/app/components/op_primer/border_box_table_component.rb index 9b8dcc8135e9..1e1160790966 100644 --- a/app/components/op_primer/border_box_table_component.rb +++ b/app/components/op_primer/border_box_table_component.rb @@ -106,6 +106,10 @@ def has_actions? false end + def has_footer? + false + end + def sortable? false end @@ -133,5 +137,9 @@ def blank_description def blank_icon nil end + + def footer + raise ArgumentError, "Need to provide footer content" + end end end diff --git a/modules/meeting/app/components/recurring_meetings/footer_component.html.erb b/modules/meeting/app/components/recurring_meetings/footer_component.html.erb new file mode 100644 index 000000000000..9defd037db85 --- /dev/null +++ b/modules/meeting/app/components/recurring_meetings/footer_component.html.erb @@ -0,0 +1,56 @@ +<%#-- 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. + +++#%> + +<%= + component_wrapper do + flex_layout(justify_content: :space_between) do |flex| + flex.with_column(classes: "ellipsis") do + render(Primer::BaseComponent.new(tag: :span, color: :muted)) do + concat render(Primer::Beta::Octicon.new(icon: :iterations, mr: 1, ml: 1)) + concat render(Primer::Beta::Text.new(font_weight: :bold)) { label } + end + end + + flex.with_column do + render( + Primer::Beta::Button.new( + scheme: :link, + size: :medium, + tag: :a, + href: polymorphic_path([@project, @meeting], count: @current_count, direction: @direction) + ) + ) do |_c| + I18n.t(:label_recurring_meeting_show_more) + end + end + end + end +%> + + diff --git a/modules/meeting/app/components/recurring_meetings/footer_component.rb b/modules/meeting/app/components/recurring_meetings/footer_component.rb new file mode 100644 index 000000000000..a3666e063930 --- /dev/null +++ b/modules/meeting/app/components/recurring_meetings/footer_component.rb @@ -0,0 +1,56 @@ +#-- 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 RecurringMeetings + class FooterComponent < ApplicationComponent + include ApplicationHelper + include OpTurbo::Streamable + include OpPrimer::ComponentHelpers + + def initialize(meeting:, project:, count:, direction:, max_count:) + super + + @meeting = meeting + @project = project + @current_count = count + @direction = direction + @max_count = max_count + end + + def label + count = @max_count - @current_count + label_suffix = count == 1 ? "_singular" : "" + + if @direction == "past" + I18n.t("label_recurring_meeting_more_past#{label_suffix}", count:) + else + I18n.t("label_recurring_meeting_more#{label_suffix}", count:, schedule: @meeting.schedule_in_words) + end + end + end +end diff --git a/modules/meeting/app/components/recurring_meetings/table_component.rb b/modules/meeting/app/components/recurring_meetings/table_component.rb index cd446823d142..720bab91af3b 100644 --- a/modules/meeting/app/components/recurring_meetings/table_component.rb +++ b/modules/meeting/app/components/recurring_meetings/table_component.rb @@ -30,7 +30,7 @@ module RecurringMeetings class TableComponent < ::OpPrimer::BorderBoxTableComponent - options :current_project + options :current_project, :count, :direction, :max_count columns :start_time, :relative_time, :last_edited, :status, :create @@ -38,6 +38,22 @@ def has_actions? true end + def has_footer? # rubocop:disable Metrics/AbcSize + return false unless recurring_meeting + + options[:max_count] - count > 0 + end + + def footer + render RecurringMeetings::FooterComponent.new( + meeting: recurring_meeting, + project: options[:current_project], + direction: options[:direction], + max_count: options[:max_count], + count: + ) + end + def header_args(column) if column == :title { style: "grid-column: span 2" } @@ -63,5 +79,15 @@ def headers def columns @columns ||= headers.map(&:first) end + + def recurring_meeting + return if model.blank? + + @recurring_meeting ||= model.first.recurring_meeting + end + + def count + @count ||= [options[:count], rows.count].max + end end end diff --git a/modules/meeting/app/controllers/recurring_meetings_controller.rb b/modules/meeting/app/controllers/recurring_meetings_controller.rb index f84f380d548b..9d4024032dc3 100644 --- a/modules/meeting/app/controllers/recurring_meetings_controller.rb +++ b/modules/meeting/app/controllers/recurring_meetings_controller.rb @@ -1,4 +1,5 @@ class RecurringMeetingsController < ApplicationController + include RecurringMeetingsHelper include Layout include PaginationHelper include OpTurbo::ComponentStream @@ -34,17 +35,16 @@ def new @recurring_meeting = RecurringMeeting.new(project: @project) end - def show # rubocop:disable Metrics/AbcSize + def show @direction = params[:direction] - if params[:direction] == "past" - @meetings = @recurring_meeting - .scheduled_instances(upcoming: false) - .page(page_param) - .per_page(per_page_param) - else - @meetings = upcoming_meetings - @total_count = @recurring_meeting.remaining_occurrences.count - @meetings.count - end + @max_count = max_count + @count = [(params[:count].to_i + 5), @max_count].min + + @meetings = if @direction == "past" + @recurring_meeting.scheduled_instances(upcoming: false).limit(@count) + else + upcoming_meetings(count: @count) + end respond_to do |format| format.html do @@ -154,13 +154,13 @@ def delete_scheduled private - def upcoming_meetings + def upcoming_meetings(count:) meetings = @recurring_meeting .scheduled_instances(upcoming: true) .index_by(&:start_time) merged = @recurring_meeting - .scheduled_occurrences(limit: 5) + .scheduled_occurrences(limit: count) .map do |start_time| meetings.delete(start_time) || scheduled_meeting(start_time) end diff --git a/modules/meeting/app/helpers/recurring_meetings_helper.rb b/modules/meeting/app/helpers/recurring_meetings_helper.rb new file mode 100644 index 000000000000..fb36af0f88d8 --- /dev/null +++ b/modules/meeting/app/helpers/recurring_meetings_helper.rb @@ -0,0 +1,37 @@ +#-- 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 RecurringMeetingsHelper + def max_count + if @direction == "past" + @recurring_meeting.scheduled_instances(upcoming: false).count + else + @recurring_meeting.remaining_occurrences.count + end + end +end diff --git a/modules/meeting/app/views/recurring_meetings/show.html.erb b/modules/meeting/app/views/recurring_meetings/show.html.erb index 21fcdc45647c..476c3b1dbdd3 100644 --- a/modules/meeting/app/views/recurring_meetings/show.html.erb +++ b/modules/meeting/app/views/recurring_meetings/show.html.erb @@ -26,6 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> + <% html_title t(:label_recurring_meeting_plural) %> <%= render(RecurringMeetings::ShowPageHeaderComponent.new(project: @project, meeting: @recurring_meeting)) %> @@ -34,5 +35,5 @@ See COPYRIGHT and LICENSE files for more details. <% if @recurring_meeting.nil? -%> <%= no_results_box %> <% else -%> - <%= render RecurringMeetings::TableComponent.new(rows: @meetings, current_project: @project) %> + <%= render RecurringMeetings::TableComponent.new(rows: @meetings, current_project: @project, count: @count, direction: @direction, max_count: @max_count) %> <% end -%> diff --git a/modules/meeting/config/locales/en.yml b/modules/meeting/config/locales/en.yml index 437e2daea45b..cf7cd35fa908 100644 --- a/modules/meeting/config/locales/en.yml +++ b/modules/meeting/config/locales/en.yml @@ -143,6 +143,11 @@ en: Any meeting information not in the template will be lost. Do you want to continue? label_recurring_meeting_restore: "Restore this occurrence" + label_recurring_meeting_more: "There are %{count} more scheduled meetings (%{schedule})." + label_recurring_meeting_more_singular: "There is %{count} more scheduled meeting (%{schedule})." + label_recurring_meeting_more_past: "There are %{count} more past meetings." + label_recurring_meeting_more_past_singular: "There is %{count} more past meeting." + label_recurring_meeting_show_more: "Show more" label_recurring_meeting_series_edit: "Edit meeting series" label_recurring_meeting_series_delete: "Delete meeting series" label_my_meetings: "My meetings" From d29beb07a54833343090afd43e711b89aadf9618 Mon Sep 17 00:00:00 2001 From: Mir Bhatia Date: Mon, 9 Dec 2024 11:25:38 +0100 Subject: [PATCH 3/4] Update documentation --- lookbook/docs/patterns/12-tables.md.erb | 11 +++++++++++ .../components/recurring_meetings/table_component.rb | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lookbook/docs/patterns/12-tables.md.erb b/lookbook/docs/patterns/12-tables.md.erb index cb841e6bf0cf..2cafad9ffdee 100644 --- a/lookbook/docs/patterns/12-tables.md.erb +++ b/lookbook/docs/patterns/12-tables.md.erb @@ -77,8 +77,19 @@ main_column :title ``` Note: Ideally, one one main column will be present for each table. +#### Footer +Set `has_footer?` to true to add a footer to the table, defining the component/content to be rendered with the `footer` method. +```ruby +def has_footer? + true +end + +def footer + render CustomFooterComponent.new(...) +end +``` ## Best practices diff --git a/modules/meeting/app/components/recurring_meetings/table_component.rb b/modules/meeting/app/components/recurring_meetings/table_component.rb index 720bab91af3b..520c2c4e6a63 100644 --- a/modules/meeting/app/components/recurring_meetings/table_component.rb +++ b/modules/meeting/app/components/recurring_meetings/table_component.rb @@ -38,7 +38,7 @@ def has_actions? true end - def has_footer? # rubocop:disable Metrics/AbcSize + def has_footer? return false unless recurring_meeting options[:max_count] - count > 0 From b0f2dda98ec8911a705bd4296bc74cad57c108b9 Mon Sep 17 00:00:00 2001 From: Mir Bhatia Date: Mon, 9 Dec 2024 12:15:15 +0100 Subject: [PATCH 4/4] Add specs --- .../recurring_meetings_show_spec.rb | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/modules/meeting/spec/requests/recurring_meetings/recurring_meetings_show_spec.rb b/modules/meeting/spec/requests/recurring_meetings/recurring_meetings_show_spec.rb index 40717f9bb05f..12ad0130c9df 100644 --- a/modules/meeting/spec/requests/recurring_meetings/recurring_meetings_show_spec.rb +++ b/modules/meeting/spec/requests/recurring_meetings/recurring_meetings_show_spec.rb @@ -155,4 +155,32 @@ expect(response).to have_http_status(:not_found) end end + + describe "Show more" do + context "when there are more than 5 scheduled instances" do + it "shows the footer" do + get recurring_meeting_path(recurring_meeting) + + expect(page).to have_css("#recurring-meetings-footer-component") + end + end + + context "when there are 5 or fewer scheduled instances" do + let(:recurring_meeting) do + create :recurring_meeting, + project:, + author: user, + start_time: Time.zone.today + 1.day, + frequency: "daily", + end_after: "iterations", + iterations: 5 + end + + it "shows no footer" do + get recurring_meeting_path(recurring_meeting) + + expect(page).to have_no_css("#recurring-meetings-footer-component") + end + end + end end