Skip to content

Commit

Permalink
Extend meeting#new form to allow creating "global" meetings
Browse files Browse the repository at this point in the history
The `meeting#new` form toggles as necessary between the project-scoped version and
the global version.

**TODO**:

When a project is selected, I believe because @project is present, that
the sidebar and top-menu project dropdown get populated with the project data as
if it were now scoped.

Some work needs to go in to perhaps modifying the instance variable name
so that this doesn't get toggled but I'll ask if there's a pattern for this
already in mind before going down that rabbit-hole.
  • Loading branch information
aaron-contreras committed Jun 7, 2023
1 parent 2bcc35a commit 8d5382f
Show file tree
Hide file tree
Showing 25 changed files with 767 additions and 217 deletions.
4 changes: 2 additions & 2 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,11 @@ def authorize_global
# * a parameter-like Hash (eg. { controller: '/projects', action: 'edit' })
# * a permission Symbol (eg. :edit_project)
def do_authorize(action, global: false)
context = @project || @projects
context = @current_project || @project || @projects
is_authorized = User.current.allowed_to?(action, context, global:)

unless is_authorized
if @project&.archived?
if (@current_project || @project)&.archived?
render_403 message: :notice_not_authorized_archived_project
else
deny_access
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,42 @@ export default class RefreshOnFormChangesController extends ApplicationControlle

static values = {
refreshUrl: String,
preserveParams: Boolean,
};

declare readonly formTarget:HTMLFormElement;

declare refreshUrlValue:string;
declare preserveParamsValue:boolean;

triggerReload():void {
// without the cast to undefined, the URLSearchParams constructor will
// not accept the FormData object.
const formData = new FormData(this.formTarget) as unknown as undefined;
const serializedFormData = new URLSearchParams(formData).toString();
const formParams = new URLSearchParams(formData);
const currentParams = new URLSearchParams(window.location.search);
const mergedParams = this.mergeQueryParams(currentParams, formParams);

const serializedFormData = mergedParams.toString();

window.location.href = `${this.refreshUrlValue}?${serializedFormData}`;
}

// Merge the form's submitted params onto the currently present
// query string parameters on the URL.
//
// This preserves scope-dependent form data that would painfully
// get reset in some reload scenarios when calling triggerReload
// from a dependent form field.
private mergeQueryParams(currentParams:URLSearchParams, newParams:URLSearchParams) {
if (!this.preserveParamsValue) {
return newParams;
}

newParams.forEach((value, key) => {
currentParams.set(key, value);
});

return currentParams;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<li class="<%= li_css_class %>">
<a href="<%= dynamic_path %>"
id="<%= id %>"
title="<%= title %>"
arial-label="<%= aria_label %>"
class="<%= link_css_class %>">
<%= icon %>
<%= label %>
</a>
</li>
78 changes: 78 additions & 0 deletions modules/meeting/app/components/meetings/add_button_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# frozen_string_literal: true

# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2023 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 Meetings
class AddButtonComponent < ::RailsComponent
options :current_project

def render?
if current_project
User.current.allowed_to?(:create_meetings, current_project)
else
User.current.allowed_to_globally?(:create_meetings)
end
end

def li_css_class
'toolbar-item'
end

def dynamic_path
polymorphic_path([:new, current_project, :meeting])
end

def id
'add-meeting-button'
end

def title
I18n.t(:label_meeting_new)
end

def aria_label
I18n.t(:label_meeting_new)
end

def link_css_class
'button -alt-highlight'
end

def label
content_tag(:span,
I18n.t(:label_meeting),
class: 'button--text')
end

def icon
helpers.op_icon('button--icon icon-add')
end
end
end
77 changes: 77 additions & 0 deletions modules/meeting/app/controllers/base_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2023 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 BaseController < ApplicationController
around_action :set_time_zone

helper :watchers
helper :meeting_contents
include WatchersHelper
include PaginationHelper
include SortHelper

private

def set_time_zone(&)
zone = User.current.time_zone
if zone.nil?
localzone = Time.current.utc_offset
localzone -= 3600 if Time.current.dst?
zone = ::ActiveSupport::TimeZone[localzone]
end

Time.use_zone(zone, &)
end

def convert_params
if params.key?(:meeting)
# We do some preprocessing of `meeting_params` that we will store in this
# instance variable.
@converted_params = meeting_params.to_h

@converted_params[:duration] = @converted_params[:duration].to_hours
# Force defaults on participants
@converted_params[:participants_attributes] ||= {}
@converted_params[:participants_attributes].each { |p| p.reverse_merge! attended: false, invited: false }
end
end

def populate_meeting_with_converted_params
@meeting.participants.clear # Start with a clean set of participants
@meeting.participants_attributes = @converted_params.delete(:participants_attributes)
@meeting.attributes = @converted_params
end

def meeting_params
params.require(:meeting).permit(:title, :location, :start_time, :duration, :start_date, :start_time_hour,
participants_attributes: %i[email name invited attended user user_id meeting id])
end
end
86 changes: 16 additions & 70 deletions modules/meeting/app/controllers/meetings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,62 +26,30 @@
# See COPYRIGHT and LICENSE files for more details.
#++

class MeetingsController < ApplicationController
around_action :set_time_zone
before_action :find_optional_project, only: %i[index new create]
class MeetingsController < BaseController
before_action :find_optional_current_project, only: %i[new create]
before_action :convert_params, only: %i[new create update]
before_action :build_meeting, only: %i[new create]
before_action :find_meeting, except: %i[index new create]
before_action :convert_params, only: %i[create update]
before_action :authorize, except: [:index]
before_action :authorize_global, only: :index
before_action :authorize, except: %i[index new]
before_action :authorize_global, only: %i[index new]

helper :watchers
helper :meeting_contents
include WatchersHelper
include PaginationHelper
include SortHelper

menu_item :new_meeting, only: %i[new create]

def index
@meetings = @project ? @project.meetings : global_upcoming_meetings
@meetings = global_upcoming_meetings
end

def show
params[:tab] ||= 'minutes' if @meeting.agenda.present? && @meeting.agenda.locked?
end

def create
@meeting.participants.clear # Start with a clean set of participants
@meeting.participants_attributes = @converted_params.delete(:participants_attributes)
@meeting.attributes = @converted_params
if params[:copied_from_meeting_id].present? && params[:copied_meeting_agenda_text].present?
@meeting.agenda = MeetingAgenda.new(
text: params[:copied_meeting_agenda_text],
journal_notes: I18n.t('meeting.copied', id: params[:copied_from_meeting_id])
)
@meeting.agenda.author = User.current
end
if @meeting.save
text = I18n.t(:notice_successful_create)
if User.current.time_zone.nil?
link = I18n.t(:notice_timezone_missing, zone: Time.zone)
text += " #{view_context.link_to(link, { controller: '/my', action: :account }, class: 'link_to_profile')}"
end
flash[:notice] = text.html_safe

redirect_to action: 'show', id: @meeting
else
render template: 'meetings/new', project_id: @project
end
end

def new; end

current_menu_item :new do
:meetings
end

def copy
params[:copied_from_meeting_id] = @meeting.id
params[:copied_meeting_agenda_text] = @meeting.agenda.text if @meeting.agenda.present?
Expand All @@ -92,7 +60,7 @@ def copy
def destroy
@meeting.destroy
flash[:notice] = I18n.t(:notice_successful_delete)
redirect_to action: 'index', project_id: @project
redirect_to project_meetings_path(@project)
end

def edit; end
Expand All @@ -110,32 +78,26 @@ def update

private

def set_time_zone(&)
zone = User.current.time_zone
if zone.nil?
localzone = Time.current.utc_offset
localzone -= 3600 if Time.current.dst?
zone = ::ActiveSupport::TimeZone[localzone]
end

Time.use_zone(zone, &)
end

def build_meeting
@meeting = Meeting.new
@meeting.project = @project
populate_meeting_with_converted_params if @converted_params
@meeting.project = @current_project
@meeting.author = User.current
end

def find_optional_project
return true unless params[:project_id]
def find_optional_current_project
return true if project_id.blank?

@project = Project.find(params[:project_id])
@current_project = Project.find(project_id)
authorize
rescue ActiveRecord::RecordNotFound
render_404
end

def project_id
@project_id ||= params[:project_id] || params.dig(:meeting, :project_id)
end

def global_upcoming_meetings
projects = Project.allowed_to(User.current, :view_meetings)

Expand All @@ -150,20 +112,4 @@ def find_meeting
rescue ActiveRecord::RecordNotFound
render_404
end

def convert_params
# We do some preprocessing of `meeting_params` that we will store in this
# instance variable.
@converted_params = meeting_params.to_h

@converted_params[:duration] = @converted_params[:duration].to_hours
# Force defaults on participants
@converted_params[:participants_attributes] ||= {}
@converted_params[:participants_attributes].each { |p| p.reverse_merge! attended: false, invited: false }
end

def meeting_params
params.require(:meeting).permit(:title, :location, :start_time, :duration, :start_date, :start_time_hour,
participants_attributes: %i[email name invited attended user user_id meeting id])
end
end
Loading

0 comments on commit 8d5382f

Please sign in to comment.