From fe01f52ee80f7aa1c776e06188207b0ec0e1278a Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 28 Jun 2024 11:52:21 +0200 Subject: [PATCH] apply query based column to meetings --- app/components/row_component.rb | 8 +- app/services/params_to_query_service.rb | 12 ++- .../app/components/meetings/row_component.rb | 2 +- .../meetings/table_component.html.erb | 71 ++++++++++++++++ .../components/meetings/table_component.rb | 81 +++++++++++++------ .../app/controllers/meetings_controller.rb | 15 ++-- modules/meeting/app/models/meeting.rb | 3 - .../meeting/app/models/queries/meetings.rb | 6 ++ .../models/queries/meetings/orders/default.rb | 35 ++++++++ .../models/queries/meetings/orders/project.rb | 43 ++++++++++ .../queries/meetings/selects/default.rb | 39 +++++++++ .../queries/meetings/selects/project.rb | 39 +++++++++ .../meeting/app/views/meetings/index.html.erb | 7 +- .../controllers/meetings_controller_spec.rb | 4 +- ...ams_to_query_service_meeting_query_spec.rb | 81 +++++++++++++++++++ 15 files changed, 404 insertions(+), 42 deletions(-) create mode 100644 modules/meeting/app/components/meetings/table_component.html.erb create mode 100644 modules/meeting/app/models/queries/meetings/orders/default.rb create mode 100644 modules/meeting/app/models/queries/meetings/orders/project.rb create mode 100644 modules/meeting/app/models/queries/meetings/selects/default.rb create mode 100644 modules/meeting/app/models/queries/meetings/selects/project.rb create mode 100644 modules/meeting/spec/services/params_to_query_service_meeting_query_spec.rb diff --git a/app/components/row_component.rb b/app/components/row_component.rb index 2a17c54a822d..032532e7902e 100644 --- a/app/components/row_component.rb +++ b/app/components/row_component.rb @@ -45,7 +45,11 @@ def row end def column_value(column) - send(column) + if column.respond_to?(:attribute) + send(column.attribute) + else + send(column) + end end def column_css_class(column) @@ -53,7 +57,7 @@ def column_css_class(column) end def column_css_classes - @column_css_classes ||= columns.to_h { |name| [name, name] } + @column_css_classes ||= columns.index_with { |column| column.respond_to?(:attribute) ? column.attribute : column } end def button_links diff --git a/app/services/params_to_query_service.rb b/app/services/params_to_query_service.rb index f117459b7a6f..b0fd6ffb1039 100644 --- a/app/services/params_to_query_service.rb +++ b/app/services/params_to_query_service.rb @@ -64,9 +64,9 @@ def apply_filters(query, params) end def apply_order(query, params) - return query unless params[:sortBy] + return query unless params[:sortBy] || params[:sort] - sort = parse_sorting_from_json(params[:sortBy]) + sort = params[:sortBy] ? parse_sorting_from_json(params[:sortBy]) : parse_sort_helper_params(params[:sort]) hash_sort = sort.each_with_object({}) do |(attribute, direction), hash| hash[attribute.to_sym] = direction.downcase.to_sym @@ -107,6 +107,14 @@ def parse_sorting_from_json(json) end end + def parse_sort_helper_params(sort_params) + sort_params + .to_s + .split(",") + .map { |s| s.split(":")[0..1] } + .map { |attribute, direction| [attribute, direction || "asc"] } + end + def convert_attribute(attribute, append_id: false) ::API::Utilities::PropertyNameConverter.to_ar_name(attribute, context: conversion_model, diff --git a/modules/meeting/app/components/meetings/row_component.rb b/modules/meeting/app/components/meetings/row_component.rb index c3e906479808..fefcd32583d6 100644 --- a/modules/meeting/app/components/meetings/row_component.rb +++ b/modules/meeting/app/components/meetings/row_component.rb @@ -30,7 +30,7 @@ module Meetings class RowComponent < ::RowComponent - def project_name + def project helpers.link_to_project model.project, {}, {}, false end diff --git a/modules/meeting/app/components/meetings/table_component.html.erb b/modules/meeting/app/components/meetings/table_component.html.erb new file mode 100644 index 000000000000..1c4189089467 --- /dev/null +++ b/modules/meeting/app/components/meetings/table_component.html.erb @@ -0,0 +1,71 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2024 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. + +++#%> + +
+
+
+
+ > + + <% (columns.length + 1).times do %> + + <% end %> + + + + <%= render_column_headers %> + + + + + <% if rows.empty? %> + + + + <% end %> + <%= render_rows %> + +
+
+
+
<%= empty_row_message %>
+ + <% if inline_create_link && show_inline_create %> +
+ <%= inline_create_link %> +
+ <% end %> +
+
+
+
+ +<% if paginated? %> + <%= helpers.pagination_links_full rows, pagination_options %> +<% end %> diff --git a/modules/meeting/app/components/meetings/table_component.rb b/modules/meeting/app/components/meetings/table_component.rb index 398e2f1641d4..53594f3b5fc9 100644 --- a/modules/meeting/app/components/meetings/table_component.rb +++ b/modules/meeting/app/components/meetings/table_component.rb @@ -29,45 +29,76 @@ #++ module Meetings - class TableComponent < ::TableComponent - options :current_project # used to determine if displaying the projects column + class TableComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent + options :params # We read collapsed state from params + options :current_user # adds this option to those of the base class + options :query - sortable_columns :title, :project_name, :type, :start_time, :duration, :location - - def initial_sort - %i[start_time asc] + def table_id + "meeting-table" end - def sortable_columns_correlation - super.merge( - project_name: "projects.name", - type: "meetings.type" - ) + def container_class + "generic-table--container_visible-overflow generic-table--container_height-100" end - def initialize_sorted_model - helpers.sort_clear - - super + def rows + @rows ||= query.results.paginate(page: helpers.page_param(params), per_page: helpers.per_page_param(params)) end def paginated? true end - def headers - @headers ||= [ - [:title, { caption: Meeting.human_attribute_name(:title) }], - current_project.blank? ? [:project_name, { caption: Meeting.human_attribute_name(:project) }] : nil, - [:type, { caption: Meeting.human_attribute_name(:type) }], - [:start_time, { caption: Meeting.human_attribute_name(:start_time) }], - [:duration, { caption: Meeting.human_attribute_name(:duration) }], - [:location, { caption: Meeting.human_attribute_name(:location) }] - ].compact + def pagination_options + default_pagination_options.merge(optional_pagination_options) + end + + def default_pagination_options + { allowed_params: %i[query_id filters columns sortBy] } + end + + def optional_pagination_options + {} + end + + def sortable_column?(select) + sortable? && query.known_order?(select.attribute) end def columns - @columns ||= headers.map(&:first) + @columns ||= query.selects.reject { |select| select.is_a?(::Queries::Selects::NotExistingSelect) } + end + + def render_rows + render(self.class.row_class.with_collection(rows, table: self)) + end + + def render_column_headers + # TODO: turn the Projects::ColumnHeaderComponent into generic component + render(Projects::ColumnHeaderComponent.with_collection(columns, query:)) + end + + def inline_create_link + nil + end + + def empty_row_message + I18n.t :no_results_title_text + end + + class << self + def row_class + mod = name.deconstantize + + "#{mod}::RowComponent".constantize + rescue NameError + raise( + NameError, + "#{mod}::RowComponent required by #{mod}::TableComponent not defined. " + + "Expected to be defined in `app/components/#{mod.underscore}/row_component.rb`." + ) + end end end end diff --git a/modules/meeting/app/controllers/meetings_controller.rb b/modules/meeting/app/controllers/meetings_controller.rb index 7e47bd8471a3..0375a030891e 100644 --- a/modules/meeting/app/controllers/meetings_controller.rb +++ b/modules/meeting/app/controllers/meetings_controller.rb @@ -57,7 +57,6 @@ class MeetingsController < ApplicationController def index @query = load_query - @meetings = load_meetings(@query) render "index", locals: { menu_name: project_or_global_menu } end @@ -249,17 +248,23 @@ def notify private def load_query + # TODO: move into Factory query = ParamsToQueryService.new( Meeting, current_user ).call(params) query = apply_default_filter_if_none_given(query) + query = apply_default_order_if_none_given(query) if @project query.where("project_id", "=", @project.id) end + query.select(:title) + query.select(:project) unless @project + query.select(:type, :start_time, :duration, :location) + query end @@ -270,10 +275,10 @@ def apply_default_filter_if_none_given(query) query.where("invited_user_id", "=", [User.current.id.to_s]) end - def load_meetings(query) - query - .results - .paginate(page: page_param, per_page: per_page_param) + def apply_default_order_if_none_given(query) + return query if query.orders.any? + + query.order(start_time: :asc) end def set_time_zone(&) diff --git a/modules/meeting/app/models/meeting.rb b/modules/meeting/app/models/meeting.rb index bba501a767f8..a37924782326 100644 --- a/modules/meeting/app/models/meeting.rb +++ b/modules/meeting/app/models/meeting.rb @@ -46,9 +46,6 @@ class Meeting < ApplicationRecord has_many :sections, dependent: :destroy, class_name: "MeetingSection" has_many :agenda_items, dependent: :destroy, class_name: "MeetingAgendaItem" - default_scope do - order("#{Meeting.table_name}.start_time DESC") - end scope :from_tomorrow, -> { where(["start_time >= ?", Date.tomorrow.beginning_of_day]) } scope :from_today, -> { where(["start_time >= ?", Time.zone.today.beginning_of_day]) } scope :with_users_by_date, -> { diff --git a/modules/meeting/app/models/queries/meetings.rb b/modules/meeting/app/models/queries/meetings.rb index 6054468a835b..b36ddf00b2ab 100644 --- a/modules/meeting/app/models/queries/meetings.rb +++ b/modules/meeting/app/models/queries/meetings.rb @@ -34,5 +34,11 @@ module Queries::Meetings filter Filters::InvitedUserFilter filter Filters::AuthorFilter filter Filters::DatesIntervalFilter + + order Orders::Default + order Orders::Project + + select Selects::Default + select Selects::Project end end diff --git a/modules/meeting/app/models/queries/meetings/orders/default.rb b/modules/meeting/app/models/queries/meetings/orders/default.rb new file mode 100644 index 000000000000..fa2d754a2bd3 --- /dev/null +++ b/modules/meeting/app/models/queries/meetings/orders/default.rb @@ -0,0 +1,35 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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. +# ++ + +class Queries::Meetings::Orders::Default < Queries::Orders::Base + self.model = Meeting + + def self.key + /\A(title|type|start_time|duration|location)\z/ + end +end diff --git a/modules/meeting/app/models/queries/meetings/orders/project.rb b/modules/meeting/app/models/queries/meetings/orders/project.rb new file mode 100644 index 000000000000..5a9e018bd9b5 --- /dev/null +++ b/modules/meeting/app/models/queries/meetings/orders/project.rb @@ -0,0 +1,43 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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. +# ++ + +class Queries::Meetings::Orders::Project < Queries::Orders::Base + self.model = Meeting + + def self.key + :project + end + + def joins + :project + end + + def name + "#{Project.table_name}.name" + end +end diff --git a/modules/meeting/app/models/queries/meetings/selects/default.rb b/modules/meeting/app/models/queries/meetings/selects/default.rb new file mode 100644 index 000000000000..8979f572f1a3 --- /dev/null +++ b/modules/meeting/app/models/queries/meetings/selects/default.rb @@ -0,0 +1,39 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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. +# ++ + +class Queries::Meetings::Selects::Default < Queries::Selects::Base + KEYS = %i[title type start_time duration location].freeze + + def self.key + Regexp.new(KEYS.join("|")) + end + + def self.all_available + KEYS.map { new(_1) } + end +end diff --git a/modules/meeting/app/models/queries/meetings/selects/project.rb b/modules/meeting/app/models/queries/meetings/selects/project.rb new file mode 100644 index 000000000000..4b02ccce50c8 --- /dev/null +++ b/modules/meeting/app/models/queries/meetings/selects/project.rb @@ -0,0 +1,39 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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. +# ++ + +class Queries::Meetings::Selects::Project < Queries::Selects::Base + def self.key + :project + end + + def apply_to(scope) + scope + .joins(:project) + .select("projects.name AS project_name") + end +end diff --git a/modules/meeting/app/views/meetings/index.html.erb b/modules/meeting/app/views/meetings/index.html.erb index 1f1e9233458b..727a1098f497 100644 --- a/modules/meeting/app/views/meetings/index.html.erb +++ b/modules/meeting/app/views/meetings/index.html.erb @@ -31,8 +31,11 @@ See COPYRIGHT and LICENSE files for more details. <%= render(Meetings::IndexPageHeaderComponent.new(project: @project)) %> <%= render(Meetings::IndexSubHeaderComponent.new(query: @query, project: @project)) %> -<% if @meetings.empty? -%> +<% if @query.results.empty? -%> <%= no_results_box %> <% else -%> - <%= render Meetings::TableComponent.new(rows: @meetings, current_project: @project) %> + <%= render Meetings::TableComponent.new( + query: @query, + current_user: current_user, + params:) %> <% end -%> diff --git a/modules/meeting/spec/controllers/meetings_controller_spec.rb b/modules/meeting/spec/controllers/meetings_controller_spec.rb index c4543bf4a062..7703553c78b0 100644 --- a/modules/meeting/spec/controllers/meetings_controller_spec.rb +++ b/modules/meeting/spec/controllers/meetings_controller_spec.rb @@ -52,7 +52,7 @@ end it { expect(response).to be_successful } - it { expect(assigns(:meetings)).to match_array meetings[1..2] } + it { expect(assigns(:query).results).to match_array meetings[1..2] } end context "when requesting meetings scoped to a project ID" do @@ -61,7 +61,7 @@ end it { expect(response).to be_successful } - it { expect(assigns(:meetings)).to match_array meetings[1] } + it { expect(assigns(:query).results).to match_array meetings[1] } end end end diff --git a/modules/meeting/spec/services/params_to_query_service_meeting_query_spec.rb b/modules/meeting/spec/services/params_to_query_service_meeting_query_spec.rb new file mode 100644 index 000000000000..cf5f24a4bb30 --- /dev/null +++ b/modules/meeting/spec/services/params_to_query_service_meeting_query_spec.rb @@ -0,0 +1,81 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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. +# ++ + +require "spec_helper" + +RSpec.describe ParamsToQueryService, "meeting query" do + # This spec does currently not cover the whole functionality. + + let(:user) { build_stubbed(:admin) } + let(:model) { Meeting } + let(:params) { {} } + let(:instance) { described_class.new(model, user) } + let(:service_call) { instance.call(params) } + + context "for a new query" do + context "when sending neither filters nor orders props" do + it "returns a new query" do + expect(service_call) + .to be_a Queries::Meetings::MeetingQuery + end + + it "applies no filter" do + expect(service_call.filters) + .to be_empty + end + + it "does not apply sorting" do + expect(service_call.orders) + .to be_empty + end + end + + context "when sending old style 'sort' orders props" do + let(:params) do + { sort: "start_time:desc,title:asc,type" } + end + + it "returns a new query" do + expect(service_call) + .to be_a Queries::Meetings::MeetingQuery + end + + it "applies no filter" do + expect(service_call.filters) + .to be_empty + end + + it "applies the sorting" do + expect(service_call.orders.map { |o| { attribute: o.attribute, direction: o.direction } }) + .to contain_exactly({ attribute: :start_time, direction: :desc }, + { attribute: :title, direction: :asc }, + { attribute: :type, direction: :asc }) + end + end + end +end