2024-12-10 15:03:32 +01:00
|
|
|
# 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::DatePickerController < ApplicationController
|
|
|
|
|
ERROR_PRONE_ATTRIBUTES = %i[start_date
|
|
|
|
|
due_date
|
|
|
|
|
duration].freeze
|
|
|
|
|
|
|
|
|
|
layout false
|
|
|
|
|
|
2025-06-27 18:55:31 +03:00
|
|
|
before_action :find_work_package, except: %i[new create preview]
|
|
|
|
|
authorization_checked! :show, :preview, :update, :edit, :new, :create
|
2024-12-10 15:03:32 +01:00
|
|
|
|
|
|
|
|
attr_accessor :work_package
|
|
|
|
|
|
|
|
|
|
def show
|
2025-03-12 17:13:05 +01:00
|
|
|
set_date_attributes_to_work_package
|
2025-06-23 19:20:04 +03:00
|
|
|
render_form
|
2024-12-10 15:03:32 +01:00
|
|
|
end
|
|
|
|
|
|
2025-01-13 14:41:57 +01:00
|
|
|
def new
|
|
|
|
|
make_fake_initial_work_package
|
|
|
|
|
set_date_attributes_to_work_package
|
2025-06-23 19:20:04 +03:00
|
|
|
render_form
|
2025-01-13 14:41:57 +01:00
|
|
|
end
|
|
|
|
|
|
2024-12-12 12:08:53 +01:00
|
|
|
def edit
|
|
|
|
|
set_date_attributes_to_work_package
|
2025-06-23 19:20:04 +03:00
|
|
|
render_form
|
2024-12-12 12:08:53 +01:00
|
|
|
end
|
|
|
|
|
|
2025-06-27 18:55:31 +03:00
|
|
|
def preview
|
|
|
|
|
if params[:work_package_id]
|
|
|
|
|
find_work_package
|
|
|
|
|
else
|
|
|
|
|
make_fake_initial_work_package
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
set_date_attributes_to_work_package
|
|
|
|
|
render_form(preview: true)
|
|
|
|
|
end
|
|
|
|
|
|
2025-01-13 14:41:57 +01:00
|
|
|
def create
|
|
|
|
|
make_fake_initial_work_package
|
|
|
|
|
service_call = set_date_attributes_to_work_package
|
|
|
|
|
|
|
|
|
|
if service_call.errors
|
|
|
|
|
.map(&:attribute)
|
|
|
|
|
.intersect?(ERROR_PRONE_ATTRIBUTES)
|
2025-06-23 19:20:04 +03:00
|
|
|
render_form(status: :unprocessable_entity)
|
2025-01-13 14:41:57 +01:00
|
|
|
else
|
|
|
|
|
render json: {
|
|
|
|
|
startDate: @work_package.start_date,
|
|
|
|
|
dueDate: @work_package.due_date,
|
|
|
|
|
duration: @work_package.duration,
|
|
|
|
|
scheduleManually: @work_package.schedule_manually,
|
|
|
|
|
includeNonWorkingDays: if @work_package.ignore_non_working_days.nil?
|
|
|
|
|
false
|
|
|
|
|
else
|
|
|
|
|
@work_package.ignore_non_working_days
|
|
|
|
|
end
|
|
|
|
|
}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2024-12-10 15:03:32 +01:00
|
|
|
def update
|
2025-03-24 14:46:44 +01:00
|
|
|
wp_params = work_package_datepicker_params
|
|
|
|
|
wp_params = manage_params_for_automatic_mode(wp_params)
|
|
|
|
|
|
2024-12-10 15:03:32 +01:00
|
|
|
service_call = WorkPackages::UpdateService
|
|
|
|
|
.new(user: current_user,
|
|
|
|
|
model: @work_package)
|
2025-03-24 14:46:44 +01:00
|
|
|
.call(wp_params)
|
2024-12-10 15:03:32 +01:00
|
|
|
|
2025-06-23 19:20:04 +03:00
|
|
|
if service_call.success?
|
|
|
|
|
head :ok
|
|
|
|
|
else
|
|
|
|
|
render_form(status: :unprocessable_entity)
|
2024-12-10 15:03:32 +01:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
2025-06-27 18:55:31 +03:00
|
|
|
def render_form(status: :ok, preview: false)
|
2025-06-23 19:20:04 +03:00
|
|
|
render :show,
|
|
|
|
|
locals: {
|
|
|
|
|
work_package:,
|
|
|
|
|
schedule_manually:,
|
|
|
|
|
focused_field:,
|
|
|
|
|
touched_field_map:,
|
2025-06-27 18:55:31 +03:00
|
|
|
date_mode:,
|
|
|
|
|
preview:
|
2025-06-23 19:20:04 +03:00
|
|
|
},
|
2025-06-25 13:27:58 +03:00
|
|
|
status:
|
2025-05-21 13:08:21 +02:00
|
|
|
end
|
|
|
|
|
|
2024-12-10 15:03:32 +01:00
|
|
|
def focused_field
|
2025-02-20 11:07:13 +01:00
|
|
|
return params[:focused_field] if params[:focused_field].present?
|
|
|
|
|
|
2025-01-20 10:04:33 +01:00
|
|
|
trigger = params[:field]
|
|
|
|
|
|
2025-03-24 14:46:44 +01:00
|
|
|
# For automatic scheduling, we focus the due date initially and do not switch to start date after touching it
|
|
|
|
|
if !ActiveModel::Type::Boolean.new.cast(schedule_manually) && trigger != "duration"
|
|
|
|
|
return :due_date
|
|
|
|
|
end
|
|
|
|
|
|
2025-01-20 10:04:33 +01:00
|
|
|
# Decide which field to focus next
|
|
|
|
|
case trigger
|
2025-03-24 14:46:44 +01:00
|
|
|
when "startDate"
|
|
|
|
|
:start_date
|
2025-01-20 10:04:33 +01:00
|
|
|
when "work_package[start_date]"
|
2025-02-26 09:30:13 +01:00
|
|
|
handle_focus_order_for_fields(:start_date, :due_date)
|
2025-03-24 14:46:44 +01:00
|
|
|
when "work_package[duration]", "duration"
|
2025-01-20 10:04:33 +01:00
|
|
|
:duration
|
2025-03-24 14:46:44 +01:00
|
|
|
when "dueDate"
|
|
|
|
|
:due_date
|
2025-02-26 09:30:13 +01:00
|
|
|
when "work_package[due_date]"
|
|
|
|
|
handle_focus_order_for_fields(:due_date, :start_date)
|
2025-01-20 10:04:33 +01:00
|
|
|
else
|
|
|
|
|
:start_date
|
|
|
|
|
end
|
2024-12-10 15:03:32 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def find_work_package
|
|
|
|
|
@work_package = WorkPackage.visible.find(params[:work_package_id])
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def touched_field_map
|
2024-12-17 10:24:35 +01:00
|
|
|
if params[:work_package]
|
|
|
|
|
params.require(:work_package)
|
|
|
|
|
.slice("schedule_manually_touched",
|
|
|
|
|
"ignore_non_working_days_touched",
|
|
|
|
|
"start_date_touched",
|
|
|
|
|
"due_date_touched",
|
|
|
|
|
"duration_touched")
|
2025-04-16 09:07:42 +02:00
|
|
|
.transform_values { it == "true" }
|
2024-12-17 10:24:35 +01:00
|
|
|
.permit!
|
|
|
|
|
else
|
|
|
|
|
{}
|
|
|
|
|
end
|
2024-12-10 15:03:32 +01:00
|
|
|
end
|
|
|
|
|
|
2024-12-17 10:24:35 +01:00
|
|
|
def schedule_manually
|
2024-12-19 11:00:03 +01:00
|
|
|
find_if_present(params[:schedule_manually]) ||
|
|
|
|
|
find_if_present(params.dig(:work_package, :schedule_manually)) ||
|
2024-12-10 15:03:32 +01:00
|
|
|
work_package.schedule_manually
|
2024-12-19 11:00:03 +01:00
|
|
|
end
|
|
|
|
|
|
2025-02-18 14:56:43 +01:00
|
|
|
def date_mode
|
2025-03-03 15:22:03 +01:00
|
|
|
# Once in range mode, always in range mode
|
|
|
|
|
return params[:date_mode] if params[:date_mode].present? && params[:date_mode] == "range"
|
2025-02-18 14:56:43 +01:00
|
|
|
|
|
|
|
|
if work_package.start_date.nil? || work_package.due_date.nil?
|
|
|
|
|
"single"
|
|
|
|
|
else
|
|
|
|
|
"range"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2024-12-19 11:00:03 +01:00
|
|
|
def find_if_present(value)
|
|
|
|
|
value.presence
|
2024-12-10 15:03:32 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def work_package_datepicker_params
|
2024-12-17 10:24:35 +01:00
|
|
|
if params[:work_package]
|
2024-12-20 12:50:36 +01:00
|
|
|
handle_milestone_dates
|
2025-02-27 08:31:23 +01:00
|
|
|
handle_form_cleared
|
2024-12-20 12:50:36 +01:00
|
|
|
|
2024-12-17 10:24:35 +01:00
|
|
|
params.require(:work_package)
|
|
|
|
|
.slice(*allowed_touched_params)
|
2025-03-14 11:23:46 +01:00
|
|
|
.merge(schedule_manually:, date_mode:, triggering_field: params[:triggering_field])
|
2024-12-17 10:24:35 +01:00
|
|
|
.permit!
|
2025-04-14 11:59:23 +02:00
|
|
|
else
|
|
|
|
|
{}
|
2024-12-17 10:24:35 +01:00
|
|
|
end
|
2024-12-10 15:03:32 +01:00
|
|
|
end
|
|
|
|
|
|
2024-12-12 12:08:53 +01:00
|
|
|
def allowed_touched_params
|
2025-04-16 09:07:42 +02:00
|
|
|
allowed_params.filter { touched?(it) }
|
2024-12-12 12:08:53 +01:00
|
|
|
end
|
2024-12-10 15:03:32 +01:00
|
|
|
|
|
|
|
|
def allowed_params
|
|
|
|
|
%i[schedule_manually ignore_non_working_days start_date due_date duration]
|
|
|
|
|
end
|
|
|
|
|
|
2024-12-12 12:08:53 +01:00
|
|
|
def touched?(field)
|
|
|
|
|
touched_field_map[:"#{field}_touched"]
|
|
|
|
|
end
|
|
|
|
|
|
2025-01-13 14:41:57 +01:00
|
|
|
def make_fake_initial_work_package
|
2025-02-20 16:08:20 +01:00
|
|
|
initial_params = params.require(:work_package)
|
|
|
|
|
.require(:initial)
|
|
|
|
|
.permit(:start_date, :due_date, :duration, :ignore_non_working_days)
|
2025-01-13 14:41:57 +01:00
|
|
|
@work_package = WorkPackage.new(initial_params)
|
|
|
|
|
@work_package.clear_changes_information
|
|
|
|
|
end
|
|
|
|
|
|
2024-12-12 12:08:53 +01:00
|
|
|
def set_date_attributes_to_work_package
|
2024-12-17 10:24:35 +01:00
|
|
|
wp_params = work_package_datepicker_params
|
2025-03-24 14:46:44 +01:00
|
|
|
wp_params = manage_params_for_automatic_mode(wp_params)
|
2024-12-17 10:24:35 +01:00
|
|
|
|
|
|
|
|
if wp_params.present?
|
|
|
|
|
WorkPackages::SetAttributesService
|
|
|
|
|
.new(user: current_user,
|
|
|
|
|
model: @work_package,
|
2025-04-17 11:44:42 +02:00
|
|
|
contract_class:)
|
2024-12-17 10:24:35 +01:00
|
|
|
.call(wp_params)
|
|
|
|
|
end
|
2024-12-12 12:08:53 +01:00
|
|
|
end
|
2024-12-20 12:50:36 +01:00
|
|
|
|
2025-04-17 11:44:42 +02:00
|
|
|
def contract_class
|
|
|
|
|
if @work_package.new_record?
|
|
|
|
|
WorkPackages::CreateContract
|
|
|
|
|
else
|
|
|
|
|
WorkPackages::UpdateContract
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2024-12-20 12:50:36 +01:00
|
|
|
def handle_milestone_dates
|
2025-06-13 18:03:17 +02:00
|
|
|
if work_package.is_milestone? && params.require(:work_package).has_key?(:start_date)
|
2024-12-20 12:50:36 +01:00
|
|
|
# Set the dueDate as the SetAttributesService will otherwise throw an error because the fields do not match
|
|
|
|
|
params.require(:work_package)[:due_date] = params.require(:work_package)[:start_date]
|
|
|
|
|
params.require(:work_package)[:due_date_touched] = "true"
|
|
|
|
|
end
|
|
|
|
|
end
|
2025-02-26 09:30:13 +01:00
|
|
|
|
2025-02-27 08:31:23 +01:00
|
|
|
def handle_form_cleared
|
|
|
|
|
touched_params = params.require(:work_package).slice(*allowed_touched_params)
|
|
|
|
|
|
|
|
|
|
if two_fields_cleared?(touched_params)
|
|
|
|
|
# If two fields are already manually cleared, we assume that the user wants to clear the whole form
|
|
|
|
|
params_array = %i[start_date due_date duration]
|
|
|
|
|
|
|
|
|
|
params_array.each do |param|
|
|
|
|
|
if touched_params[param].nil?
|
|
|
|
|
params.require(:work_package)[param] = ""
|
|
|
|
|
params.require(:work_package)["#{param}_touched"] = "true"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def two_fields_cleared?(wp_params)
|
|
|
|
|
start_date = wp_params[:start_date]
|
|
|
|
|
due_date = wp_params[:due_date]
|
|
|
|
|
duration = wp_params[:duration]
|
|
|
|
|
|
|
|
|
|
# Check which params are set to an empty string
|
|
|
|
|
empty_params = [start_date, due_date, duration].select { |param| param == "" }
|
|
|
|
|
# Check which param was not touched
|
|
|
|
|
missing_param = [start_date, due_date, duration].select(&:nil?)
|
|
|
|
|
|
|
|
|
|
# If two values are deleted and one is untouched, return tru
|
|
|
|
|
empty_params.length == 2 && missing_param.length == 1
|
|
|
|
|
end
|
|
|
|
|
|
2025-02-26 09:30:13 +01:00
|
|
|
def handle_focus_order_for_fields(trigger_field, alternative_field)
|
|
|
|
|
if !!params[:work_package][:"#{trigger_field}_touched"] && params[:work_package][:"#{trigger_field}"].blank?
|
|
|
|
|
# Special case, when deleting a value: we want to keep the focus on that field instead of moving to the next field
|
|
|
|
|
trigger_field
|
|
|
|
|
else
|
|
|
|
|
alternative_field
|
|
|
|
|
end
|
|
|
|
|
end
|
2025-03-24 14:46:44 +01:00
|
|
|
|
|
|
|
|
def manage_params_for_automatic_mode(wp_params)
|
|
|
|
|
return wp_params if wp_params["schedule_manually"] != "false"
|
|
|
|
|
|
2025-10-16 13:51:07 +02:00
|
|
|
# For WP with children the dates and duration are always fixed
|
|
|
|
|
return wp_params.without("start_date", "due_date", "duration") if work_package.children.any?
|
2025-03-24 14:46:44 +01:00
|
|
|
|
|
|
|
|
# the start should be preserved and will thus be send as a parameter
|
|
|
|
|
wp_params["start_date"] = work_package.start_date.to_s
|
|
|
|
|
|
|
|
|
|
wp_params
|
|
|
|
|
end
|
2024-12-10 15:03:32 +01:00
|
|
|
end
|