Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 6 additions & 4 deletions app/components/op_primer/status_button_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,12 @@ def readonly?
end

def button_content(button)
button.with_leading_visual_icon(icon: @current_status.icon) if @current_status.icon
button.with_trailing_action_icon(icon: "triangle-down") if !readonly? && @items.any?
if !@button_arguments[:icon_only]
button.with_leading_visual_icon(icon: @current_status.icon) if @current_status.icon
button.with_trailing_action_icon(icon: "triangle-down") if !readonly? && @items.any?

@current_status.name
@current_status.name
end
end

def button_arguments
Expand All @@ -82,7 +84,7 @@ def button_arguments
aria: {
label: title
}
}.compact.deep_merge(@button_arguments)
}.compact.deep_merge(@button_arguments.except(:classes))
end

def highlight_class_name(status, style)
Expand Down
29 changes: 29 additions & 0 deletions app/components/projects/phase_status_button_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<%=
component_wrapper do
flex_layout do |container|
container.with_row do
render(
OpPrimer::StatusButtonComponent.new(
current_status: build_item(@status),
items: build_items,
button_arguments: {
icon_only: true,
classes: class_names(
"op-phase-status-button",

Check failure on line 12 in app/components/projects/phase_status_button_component.html.erb

View workflow job for this annotation

GitHub Actions / ERB lint

[erb_lint] reported by reviewdog 🐶 Style/TrailingCommaInArguments: Avoid comma after the last parameter of a method call. Raw Output: app/components/projects/phase_status_button_component.html.erb:12:40: Style/TrailingCommaInArguments: Avoid comma after the last parameter of a method call.
),
title: project_phase_status_name(@status.code),
size: :medium,
scheme: :invisible,
style: :foreground,
icon: @status.icon,
"aria-label": project_phase_status_name(@status.code)
},
menu_arguments: {
data: { test_selector: "phase-status-action-menu-#{@phase.id}" },

Check failure on line 22 in app/components/projects/phase_status_button_component.html.erb

View workflow job for this annotation

GitHub Actions / ERB lint

[erb_lint] reported by reviewdog 🐶 Style/TrailingCommaInHashLiteral: Avoid comma after the last item of a hash. Raw Output: app/components/projects/phase_status_button_component.html.erb:22:78: Style/TrailingCommaInHashLiteral: Avoid comma after the last item of a hash.
}
)
)
end
end
end
%>
93 changes: 93 additions & 0 deletions app/components/projects/phase_status_button_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, see <https://www.gnu.org/licenses/>.
#
# See COPYRIGHT and LICENSE files for more details.
#++

class Projects::PhaseStatusButtonComponent < ApplicationComponent
include ApplicationHelper
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
include Primer::ClassNameHelper

attr_reader :phase, :user

def initialize(phase:, user:, **system_arguments)
super

@phase = phase
@user = user
@status = Projects::PhaseStatuses.find(phase.status)
@system_arguments = system_arguments
end

def wrapper_uniq_by
phase
end

def render? = @status.present? && edit_enabled?

private

def edit_enabled?
user.allowed_in_project?(:edit_project_phases, phase.project)
end

def build_items
Projects::PhaseStatuses::AVAILABLE.map { build_item(it) }
end

def build_item(status)
OpPrimer::StatusButtonOption.new(
name: project_phase_status_name(status.code),
description: project_phase_status_description(status.code),
color_namespace: :project_phase_status,
color_ref: status.id,
icon: status.icon,
item_id: status.id,
tag: :a,
href: project_phase_status_path(phase, status: status.code),
content_arguments: {
data: { turbo_method: status.value ? :put : :delete },
aria: { current: (true if status == @status) }
}
)
end

##
# @param status_code [String | Symbol | nil] Project Phase Status code
# @return [String] the localized name.
def project_phase_status_name(status_code)
I18n.t(status_code, scope: "projects.phase_status.title")
end

##
# @param status_code [String | Symbol | nil] Project Phase Status code
# @return [String] the localized description.
def project_phase_status_description(status_code)
I18n.t(status_code, scope: "projects.phase_status.description")
end
end
49 changes: 49 additions & 0 deletions app/constants/projects/phase_statuses.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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, see <https://www.gnu.org/licenses/>.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Projects
module PhaseStatuses
Status = Data.define(:code, :hexcode, :icon) do
def id = code&.to_s || :none

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A small thing but this is a string when it's there and a :none when it's nil.
Is there a reason why it must be a string? I think the helper in the component turns it into a string anyways?

def value = code&.to_s

def color = Color.new(hexcode:)
end

OPEN = Status.new(code: :open, hexcode: "#006edb", icon: "issue-opened")
IN_PROGRESS = Status.new(code: :in_progress, hexcode: "#1F883D", icon: "play")
COMPLETED = Status.new(code: :completed, hexcode: "#25292e", icon: "check-circle")

AVAILABLE = [OPEN, IN_PROGRESS, COMPLETED].freeze

def self.find(code)
AVAILABLE.find { it.code.to_s == code.to_s }
Comment thread
planktonic marked this conversation as resolved.
end
end
end
2 changes: 2 additions & 0 deletions app/contracts/project_phases/base_contract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

module ProjectPhases
class BaseContract < ::ModelContract
delegate :project, to: :model

validate :validate_edit_project_phases_permission

def validate_edit_project_phases_permission
Expand Down
2 changes: 0 additions & 2 deletions app/contracts/project_phases/update_contract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ class UpdateContract < BaseContract
validate :validate_finish_date_is_a_working_day
validate :validate_date_format

delegate :project, to: :model

def writable_attributes = %w[start_date finish_date]

def validate_start_after_preceeding_phases
Expand Down
34 changes: 34 additions & 0 deletions app/contracts/project_phases/update_status_contract.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 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, see <https://www.gnu.org/licenses/>.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module ProjectPhases
class UpdateStatusContract < BaseContract
attribute :status
end
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As small as this class looks, contracts are the main line of defence for the models. Controllers protect against access to the controller but contracts control which action is allowed to a user on a model. The controller check is usually coarse, the controller's check can be way more granular.

I am writing all this to justify why I think a unit spec on this class is a must. It should check that the permission is necessary and that only the status can be changed.

Empty file.
68 changes: 68 additions & 0 deletions app/controllers/project_phases/status_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# 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, see <https://www.gnu.org/licenses/>.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module ProjectPhases
class StatusController < ApplicationController
include OpTurbo::ComponentStream
include OpTurbo::FlashStreamHelper

before_action :find_phase
before_action :authorize

def update
call = ProjectPhases::UpdateStatusService
.new(user: current_user, model: @phase)
.call(status: params.require(:status))

if call.success?
refresh_buttons
render_success_flash_message_via_turbo_stream(message: t(:notice_successful_update))
respond_with_turbo_streams
else
message = t(:notice_unsuccessful_update_with_reason, reason: call.message)
respond_with_flash_error(message:)
end
end

private

def find_phase
@phase = Project::Phase.where(active: true).find(params.expect(:project_phase_id))
@project = @phase.project
Comment thread
planktonic marked this conversation as resolved.
end

def refresh_buttons
@project.available_phases.each do |phase|

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess in the future, we might want to make that smarter than just refreshing them all. But for now this is fine for me.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Counterproposal: make it dumber. This already knows that it is called from the project overview page which is why all the project's phases are on the page. Just render the complete turbo frame project-life-cycle-sidebar

Or, throw an event and have the frontend figure out whether it then wants to reload or not. Costs a round trip but offers a way simpler architecture. And the user will not know the difference.

update_via_turbo_stream(
component: Projects::PhaseStatusButtonComponent.new(phase: phase.reload, user: current_user)
)
end
end
end
end
2 changes: 2 additions & 0 deletions app/models/project/phase.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class Project::Phase < ApplicationRecord

attr_readonly :definition_id

enum :status, { open: "open", in_progress: "in_progress", completed: "completed" }, validate: true

scope :active, -> { where(active: true) }
scope :with_timeline_content, -> {
joins(:definition).where(
Expand Down
Loading
Loading