Repository navigation
[OP-20396] States for project lifecycle phases #26164
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
base: dev
Are you sure you want to change the base?
Changes from all commits
6dca1a4
dbba8e6
b00afef
e7c1ba3
f5afec1
3756b2d
9b193b2
1716736
468d6c2
f239466
65f7cb2
4484e8e
93794a3
ed765a8
ad1e6a8
a30f39c
8cc6b1f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
|
||
| ), | ||
| 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
|
||
| } | ||
| ) | ||
| ) | ||
| end | ||
| end | ||
| end | ||
| %> | ||
| 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 |
| 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 | ||
| 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 } | ||
|
planktonic marked this conversation as resolved.
|
||
| end | ||
| end | ||
| end | ||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| 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 | ||
|
planktonic marked this conversation as resolved.
|
||
| end | ||
|
|
||
| def refresh_buttons | ||
| @project.available_phases.each do |phase| | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 | ||
There was a problem hiding this comment.
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
:nonewhen it'snil.Is there a reason why it must be a string? I think the helper in the component turns it into a string anyways?