Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation/59721 add setedit reminder buttondialog to workpackage page #17341

1 change: 1 addition & 0 deletions app/components/_index.sass
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
@import "work_packages/activities_tab/journals/item_component/add_reactions"
@import "work_packages/activities_tab/journals/item_component/reactions"
@import "shares/modal_body_component"
@import "work_packages/reminder/modal_body_component"
@import "shares/invite_user_form_component"
@import "work_packages/details/tab_component"
@import "work_packages/progress/modal_body_component"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<%= component_wrapper(tag: "turbo-frame") do %>
<%= primer_form_with(
model: reminder,
url: submit_path,
id: FORM_ID
) do |f| %>
<%= flex_layout(classes: "reminder-modal-body--form-flex-container") do |form_flex_container| %>
<%= form_flex_container.with_row do %>
<%= render(WorkPackages::Reminder::RemindAtDate.new(f, initial_value: remind_at_date_initial_value)) %>
<% end %>
<%= form_flex_container.with_row do %>
<%= render(WorkPackages::Reminder::RemindAtTime.new(f, initial_value: remind_at_time_initial_value)) %>
<% end %>
<%= form_flex_container.with_row do %>
<%= render(WorkPackages::Reminder::Note.new(f)) %>
<% end %>
<% if reminder.persisted? %>
<%= form_flex_container.with_row(flex_layout: true,
justify_content: :space_between) do |actions_row| %>
<%= actions_row.with_column do %>
<%= render(Primer::Beta::Button.new(
scheme: :danger,
type: :submit,
formaction: work_package_reminder_path(remindable, reminder),
formmethod: :delete
)) { I18n.t(:button_remove_reminder) } %>
<% end %>
<%= actions_row.with_column(flex_layout: true) do |actions_sub_row| %>
<%= actions_sub_row.with_column(mr: 2) do %>
<%= render(Primer::Beta::Button.new(**cancel_button_props)) { I18n.t(:button_cancel) } %>
<% end %>
<%= actions_sub_row.with_column do %>
<%= render(Primer::Beta::Button.new(scheme: :primary,
type: :submit)) { I18n.t(:button_save) } %>
<% end %>
<% end%>
<% end %>
<% else%>
<%= form_flex_container.with_row(flex_layout: true,
justify_content: :flex_end) do |actions_row| %>
<%= actions_row.with_column(mr: 2) do %>
<%= render(Primer::Beta::Button.new(**cancel_button_props)) { I18n.t(:button_cancel) } %>
<% end %>
<%= actions_row.with_column do %>
<%= render(Primer::Beta::Button.new(scheme: :primary,
type: :submit)) { I18n.t(:button_set_reminder) } %>
<% end %>
<% end %>
<% end%>
<% end %>
<% end %>
<% end %>
93 changes: 93 additions & 0 deletions app/components/work_packages/reminder/modal_body_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# frozen_string_literal: true

#-- 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 WorkPackages
module Reminder
class ModalBodyComponent < ApplicationComponent
include ApplicationHelper
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers

FORM_ID = "reminder-form"

attr_reader :remindable, :reminder, :errors

def initialize(remindable:, reminder:, errors: nil)
super

@remindable = remindable
@reminder = reminder
@errors = errors
end

class << self
def wrapper_key
"reminder_modal_body"
end
end

def submit_path
if @reminder.persisted?
work_package_reminder_path(@remindable, @reminder)
else
work_package_reminders_path(@remindable)
end
end

def submit_button_text
if @reminder.persisted?
I18n.t(:button_save)
else
I18n.t(:button_set_reminder)
end
end

def cancel_button_props
{
scheme: :secondary,
data: {
controller: "primer-to-angular-modal",
application_target: "dynamic",
action: "click->primer-to-angular-modal#close",
test_selector: "op-reminder-modal-close-button"
}
}
end

def remind_at_date_initial_value
format_time_as_date(@reminder.remind_at, format: "%Y-%m-%d")
end

def remind_at_time_initial_value
format_time(@reminder.remind_at, include_date: false, format: "%H:%M")
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.reminder-modal-body
&--form-flex-container
gap: 1rem
5 changes: 5 additions & 0 deletions app/contracts/reminders/base_contract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class BaseContract < ::ModelContract
validate :validate_acting_user
validate :validate_remindable_exists
validate :validate_manage_reminders_permissions
validate :validate_remind_at_present
validate :validate_remind_at_is_in_future
validate :validate_note_length

Expand All @@ -59,6 +60,10 @@ def validate_remindable_exists
errors.add :remindable, :not_found if model.remindable.blank?
end

def validate_remind_at_present
akabiru marked this conversation as resolved.
Show resolved Hide resolved
errors.add :remind_at, :blank if model.remind_at.blank?
end

def validate_remind_at_is_in_future
if model.remind_at.present? && model.remind_at < Time.current
errors.add :remind_at, :datetime_must_be_in_future
Expand Down
213 changes: 213 additions & 0 deletions app/controllers/work_packages/reminders_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# frozen_string_literal: true

# -- 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.
# ++

class WorkPackages::RemindersController < ApplicationController
include OpTurbo::ComponentStream
layout false
before_action :find_work_package
before_action :build_or_find_reminder, only: %i[modal_body create]
before_action :find_reminder, only: %i[update destroy]

before_action :authorize

def modal_body
render modal_component_class.new(
remindable: @work_package,
reminder: @reminder
)
end

def create
service_result = Reminders::CreateService.new(user: current_user)
.call(reminder_params)

if service_result.success?
update_flash_message_via_turbo_stream(
message: I18n.t("work_package.reminders.success_creation_message"),
scheme: :success
)
respond_with_turbo_streams
else
prepare_errors_from_result(service_result)

replace_via_turbo_stream(
component: modal_component_class.new(
remindable: @work_package,
reminder: @reminder,
errors: @errors
)
)

respond_with_turbo_streams(status: :unprocessable_entity)
end
end

def update
service_result = Reminders::UpdateService.new(user: current_user,
model: @reminder)
.call(reminder_params)

if service_result.success?
update_flash_message_via_turbo_stream(
message: I18n.t("work_package.reminders.success_update_message"),
scheme: :success
)
respond_with_turbo_streams
else
prepare_errors_from_result(service_result)

replace_via_turbo_stream(
component: modal_component_class.new(
remindable: @work_package,
reminder: @reminder,
errors: @errors
)
)

respond_with_turbo_streams(status: :unprocessable_entity)
end
end

def destroy
service_result = Reminders::DeleteService.new(user: current_user,
model: @reminder)
.call

if service_result.success?
update_flash_message_via_turbo_stream(
message: I18n.t("work_package.reminders.success_deletion_message"),
scheme: :success
)
respond_with_turbo_streams
else
update_flash_message_via_turbo_stream(
message: service_result.errors.full_messages,
scheme: :danger
)
respond_with_turbo_streams(status: :unprocessable_entity)
end
end

private

def modal_component_class
WorkPackages::Reminder::ModalBodyComponent
end

def find_work_package
@work_package = WorkPackage.visible.find(params[:work_package_id])
end

# At the form level, we split the date and time into two form fields.
# In order to be a bit more informative of which field is causing
# the remind_at attribute to be in the past/invalid, we need to
# remap the error attribute to the appropriate field.
def prepare_errors_from_result(service_result)
# We set the reminder here for "create" case
# as the record comes from the service.
@reminder = service_result.result
@errors = service_result.errors

case @errors.find { |error| error.attribute == :remind_at }&.type
when :blank
handle_blank_error
when :datetime_must_be_in_future
handle_future_error
end

@errors.delete(:remind_at)
end
Comment on lines +128 to +146
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 I think this method and related calls would do well as an extracted simple PORO that can be unit tested- also reduce the perceived complexity on the controller

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


def handle_blank_error
@errors.add(:remind_at_date, :blank) if remind_at_date.blank?
@errors.add(:remind_at_time, :blank) if remind_at_time.blank?
end

def handle_future_error
@errors.add(:remind_at_date, :datetime_must_be_in_future) if @reminder.remind_at.to_date < today_in_user_time_zone
@errors.add(:remind_at_time, :datetime_must_be_in_future) if @reminder.remind_at < now_in_user_time_zone
end

def now_in_user_time_zone
@now_in_user_time_zone ||= Time.current
.in_time_zone(User.current.time_zone)
end

def today_in_user_time_zone
@today_in_user_time_zone ||= now_in_user_time_zone.to_date
end

# We assume for now that there is only one reminder per work package
def build_or_find_reminder
@reminder = @work_package.reminders
.upcoming_and_visible_to(User.current)
.last || @work_package.reminders.build
end

def find_reminder
@reminder = @work_package.reminders
.upcoming_and_visible_to(User.current)
.find(params[:id])
rescue ActiveRecord::RecordNotFound
update_flash_message_via_turbo_stream(
message: I18n.t(:error_reminder_not_found),
scheme: :danger
)
respond_with_turbo_streams(status: :not_found)
false
end

def reminder_params
params.require(:reminder)
.permit(%i[remind_at_date remind_at_time note])
.tap do |initial_params|
date = initial_params.delete(:remind_at_date)
time = initial_params.delete(:remind_at_time)

initial_params[:remind_at] = build_remind_at_from_params(date, time)
initial_params[:remindable] = @work_package
initial_params[:creator] = User.current
end
end

def build_remind_at_from_params(remind_at_date, remind_at_time)
if remind_at_date.present? && remind_at_time.present?
DateTime.parse("#{remind_at_date} #{User.current.time_zone.parse(remind_at_time)}")
end
end

def remind_at_date
params[:reminder][:remind_at_date]
end

def remind_at_time
params[:reminder][:remind_at_time]
end
end
Loading
Loading