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/59288 add stages and gates to the project overview page #17223

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5885b03
wip
dombesz Nov 18, 2024
96d1f2e
Display life cycles in the sidebar
dombesz Nov 18, 2024
479b9c8
Update variable names
dombesz Nov 25, 2024
f0c3df9
Add Stages and Gates permissions
dombesz Nov 26, 2024
a115cc5
Allow instantiation of Project::LifeCycleStepDefinition in order to l…
dombesz Nov 26, 2024
13ce90b
Rename overview page spec helper methods
dombesz Nov 26, 2024
9b82812
Add sidebar specs for displaying life cycles
dombesz Nov 26, 2024
b60407e
Add view permission specs
dombesz Nov 26, 2024
bfe6a7f
Add edit permission specs
dombesz Nov 26, 2024
002fdf3
Add Coloured icons, initial rendering of the form fields
dombesz Nov 28, 2024
56445a9
Add DatePicker component and dsl for single_date_picker and range_dat…
dombesz Dec 2, 2024
ddf938c
Display the datepicker input fields on the life cycle edit form
dombesz Dec 3, 2024
8bb61b7
Add ProjecLifeCycleSteps::BaseContract and UpdateContract.
dombesz Dec 3, 2024
bf65858
Make form validations work for nested project life cycle attributes.
dombesz Dec 5, 2024
667d1d5
Add Project::Stage date_range setter and validation.
dombesz Dec 6, 2024
f29a905
Remove condition for invalid LifeCycleStep initialization, add valida…
dombesz Dec 6, 2024
76d8899
Use date and date_range in the life cycle steps dialog, automatically…
dombesz Dec 6, 2024
6e2f780
Add life cycle step increasing dates validation to the ProjectLifeCyc…
dombesz Dec 9, 2024
851f504
Activate associated validations for project life cycle steps only whe…
dombesz Dec 10, 2024
59bf7cc
Display duration in days caption for Project::Stage entries.
dombesz Dec 10, 2024
8306416
Create form preview to interactively calculate duration of selected d…
dombesz Dec 11, 2024
1eb9eac
Allow empty date and date range for Project::LifeCycleStep
dombesz Dec 12, 2024
4bd144d
exclude OP custom dom elements from being morphed
ulferts Dec 6, 2024
8040262
Enable morphing the Stages and Gates form preview action
dombesz Dec 13, 2024
583a2f3
Add validation error border around the datepicker's input.
dombesz Dec 14, 2024
5ad7378
Add specs for stages and gates edit dialog
dombesz Dec 16, 2024
6ce663b
Addd more Stage and Gates dialog specs wip.
dombesz Dec 16, 2024
b8b0bde
Fix unit specs
dombesz Dec 17, 2024
89de3bb
Attempt to fix flaky specs.
dombesz Dec 17, 2024
7aee3c1
Use the correct Project Lifecycle label
dombesz Dec 17, 2024
a96fdd1
Address CR comments
dombesz Dec 17, 2024
0b89658
Fix specs
dombesz Dec 17, 2024
b488419
Fix single quote character
dombesz Dec 17, 2024
3b7b6c3
Allow retry_block on RSpec::Expectations::ExpectationNotMetError too.
dombesz Dec 17, 2024
f00f051
Fix rubocop issues
dombesz Dec 17, 2024
f0fce0e
Address CR comments
dombesz Dec 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion app/components/concerns/op_turbo/streamable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class MissingComponentWrapper < StandardError; end
# rubocop:enable OpenProject/AddPreviewForViewComponent

INLINE_ACTIONS = %i[dialog flash].freeze
# Turbo allows the response method for these actions only:
ACTIONS_WITH_METHOD = %i[update replace].freeze

extend ActiveSupport::Concern

Expand All @@ -43,7 +45,7 @@ def wrapper_key
end

included do
def render_as_turbo_stream(view_context:, action: :update)
def render_as_turbo_stream(view_context:, action: :update, method: nil)
case action
when :update, *INLINE_ACTIONS
@inner_html_only = true
Expand All @@ -63,8 +65,13 @@ def render_as_turbo_stream(view_context:, action: :update)
"Wrap your component in a `component_wrapper` block in order to use turbo-stream methods"
end

if method && !action.in?(ACTIONS_WITH_METHOD)
raise ArgumentError, "The #{action} action does not supports a method"
end

OpTurbo::StreamComponent.new(
action:,
method:,
target: wrapper_key,
template:
).render_in(view_context)
Expand Down
5 changes: 3 additions & 2 deletions app/contracts/model_contract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class ModelContract < BaseContract
# This of course is only true if that contract validates the model and
# if the model has an errors object.
def valid?(context = nil)
model.valid? if validate_model?
model.valid?(context) if validate_model?

contract_valid?(context, clear_errors: !validate_model?)
end
Expand All @@ -61,7 +61,8 @@ def valid?(context = nil)
# Clearing would then be done in the #valid? method by calling model.valid?
# * Checks for readonly attributes being changed
def contract_valid?(context = nil, clear_errors: false)
current_context, self.validation_context = validation_context, context # rubocop:disable Style/ParallelAssignment
current_context = validation_context
self.validation_context = context

errors.clear if clear_errors

Expand Down
79 changes: 79 additions & 0 deletions app/contracts/project_life_cycle_steps/base_contract.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#-- 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 ProjectLifeCycleSteps
class BaseContract < ::ModelContract
validate :select_custom_fields_permission
validate :consecutive_steps_have_increasing_dates

def valid?(context = :saving_life_cycle_steps)
super
end

def select_custom_fields_permission
return if user.allowed_in_project?(:edit_project_stages_and_gates, model)

errors.add :base, :error_unauthorized
end

def consecutive_steps_have_increasing_dates
# Filter out steps with missing dates before proceeding with comparison
filtered_steps = model.available_life_cycle_steps.select(&:start_date)

# Only proceed with comparisons if there are at least 2 valid steps
return if filtered_steps.size < 2

# Compare consecutive steps in pairs
filtered_steps.each_cons(2) do |previous_step, current_step|
if start_date_for(current_step) <= end_date_for(previous_step)
step = previous_step.is_a?(Project::Stage) ? "Stage" : "Gate"
field = current_step.is_a?(Project::Stage) ? :date_range : :date
model.errors.import(
current_step.errors.add(field, :non_continuous_dates, step:),
attribute: :"available_life_cycle_steps.#{field}"
)
end
end
end

private

def start_date_for(step)
step.start_date
end

def end_date_for(step)
case step
when Project::Gate
step.date
when Project::Stage
step.end_date || step.start_date # Use the start_date as fallback for single date stages
end
end
end
end
32 changes: 32 additions & 0 deletions app/contracts/project_life_cycle_steps/update_contract.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#-- 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 ProjectLifeCycleSteps
class UpdateContract < BaseContract
end
end
13 changes: 7 additions & 6 deletions app/controllers/concerns/op_turbo/component_stream.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,24 @@ def respond_to_with_turbo_streams(status: turbo_status, &format_block)

alias_method :respond_with_turbo_streams, :respond_to_with_turbo_streams

def update_via_turbo_stream(component:, status: :ok)
modify_via_turbo_stream(component:, action: :update, status:)
def update_via_turbo_stream(component:, status: :ok, method: nil)
modify_via_turbo_stream(component:, action: :update, status:, method:)
end

def replace_via_turbo_stream(component:, status: :ok)
modify_via_turbo_stream(component:, action: :replace, status:)
def replace_via_turbo_stream(component:, status: :ok, method: nil)
modify_via_turbo_stream(component:, action: :replace, status:, method:)
end

def remove_via_turbo_stream(component:, status: :ok)
modify_via_turbo_stream(component:, action: :remove, status:)
end

def modify_via_turbo_stream(component:, action:, status:)
def modify_via_turbo_stream(component:, action:, status:, method: nil)
@turbo_status = status
turbo_streams << component.render_as_turbo_stream(
view_context:,
action:
action:,
method:
)
end

Expand Down
5 changes: 5 additions & 0 deletions app/forms/application_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ def url_helpers
Rails.application.routes.url_helpers
end

# @return [ActionView::Base] the view helper instance
def helpers
@view_context.helpers
end

# @return [ActiveRecord::Base] the model instance given to the form builder
def model
@builder.object
Expand Down
104 changes: 104 additions & 0 deletions app/forms/projects/life_cycles/form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#-- 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 Projects::LifeCycles
class Form < ApplicationForm
form do |f|
life_cycle_input(f)
end

private

def life_cycle_input(form)
case model
when Project::Stage
multi_value_life_cycle_input(form)
when Project::Gate
single_value_life_cycle_input(form)
else
raise NotImplementedError, "Unknown life cycle definition type #{model.class.name}"
end
end

def qa_field_name
"life-cycle-step-#{model.id}"
end

def base_input_attributes
{
label: "#{icon} #{text}".html_safe, # rubocop:disable Rails/OutputSafety
leading_visual: { icon: :calendar },
datepicker_options: {
inDialog: true,
data: { action: "change->overview--project-life-cycles-form#handleChange" }
},
wrapper_data_attributes: {
"qa-field-name": qa_field_name
}
}
end

def single_value_life_cycle_input(form)
input_attributes = { name: :date, value: model.date }

form.single_date_picker **base_input_attributes, **input_attributes
end

def multi_value_life_cycle_input(form)
value = [model.start_date, model.end_date].compact.join(" - ")

input_attributes = { name: :date_range, value: }
if model.working_days_count
input_attributes[:caption] =
I18n.t("project_stage.working_days_count", count: model.working_days_count)
end

form.range_date_picker **base_input_attributes, **input_attributes
end

def text
model.name
end

def icon
icon_name = case model
when Project::Stage
:"git-commit"
when Project::Gate
:diamond
else
raise NotImplementedError, "Unknown model #{model.class} to render a LifeCycleForm with"
end

render Primer::Beta::Octicon.new(icon: icon_name, classes: icon_color_class)
end

def icon_color_class
helpers.hl_inline_class("life_cycle_step_definition", model.definition)
end
end
end
6 changes: 6 additions & 0 deletions app/models/permitted_params.rb
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,12 @@ def project
whitelist.merge(custom_field_values(:project))
end

def project_life_cycles
params.require(:project).permit(
available_life_cycle_steps_attributes: %i[id date date_range]
)
end

def project_custom_field_project_mapping
params.require(:project_custom_field_project_mapping)
.permit(*self.class.permitted_attributes[:project_custom_field_project_mapping])
Expand Down
8 changes: 8 additions & 0 deletions app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ class Project < ApplicationRecord
has_many :project_storages, dependent: :destroy, class_name: "Storages::ProjectStorage"
has_many :storages, through: :project_storages
has_many :life_cycle_steps, class_name: "Project::LifeCycleStep", dependent: :destroy
has_many :available_life_cycle_steps,
-> { active.eager_load(:definition).order(position: :asc) },
class_name: "Project::LifeCycleStep",
inverse_of: :project,
dependent: :destroy

accepts_nested_attributes_for :available_life_cycle_steps
validates_associated :available_life_cycle_steps, on: :saving_life_cycle_steps

store_attribute :settings, :deactivate_work_package_attachments, :boolean

Expand Down
5 changes: 4 additions & 1 deletion app/models/project/gate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ class Project::Gate < Project::LifeCycleStep

# This ensures the type cannot be changed after initialising the class.
validates :type, inclusion: { in: %w[Project::Gate], message: :must_be_a_gate }
validates :date, presence: true
validate :end_date_not_allowed

def end_date_not_allowed
if end_date.present?
errors.add(:base, :end_date_not_allowed)
end
end

def not_set?
date.blank?
end
end
17 changes: 9 additions & 8 deletions app/models/project/life_cycle_step.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,25 @@
#++

class Project::LifeCycleStep < ApplicationRecord
belongs_to :project, optional: false
belongs_to :project, optional: false, inverse_of: :available_life_cycle_steps
belongs_to :definition,
optional: false,
class_name: "Project::LifeCycleStepDefinition"
has_many :work_packages, inverse_of: :project_life_cycle_step, dependent: :nullify

delegate :name, :position, to: :definition

attr_readonly :definition_id, :type

validates :type, inclusion: { in: %w[Project::Stage Project::Gate], message: :must_be_a_stage_or_gate }
validate :validate_type_and_class_name_are_identical

def initialize(*args)
if instance_of? Project::LifeCycleStep
# Do not allow directly instantiating this class
raise NotImplementedError, "Cannot instantiate the base Project::LifeCycleStep class directly. " \
"Use Project::Stage or Project::Gate instead."
end
scope :active, -> { where(active: true) }

super
def validate_type_and_class_name_are_identical
if type != self.class.name
errors.add(:type, :type_and_class_name_mismatch)
end
end

def column_name
Expand Down
Loading
Loading