Repository navigation
[OP-20396] States for project lifecycle phases - #26164
planktonic wants to merge 15 commits into
Conversation
| it "renders label" do | ||
| expect(rendered_component).to have_element :label, text: "Subject", class: "advanced-filters--filter-name" | ||
| def self.find(code) | ||
| AVAILABLE.find { it.code.to_s == code.to_s } |
There was a problem hiding this comment.
why do we need the to_s here? aren't these both the same symbol?
There was a problem hiding this comment.
This is how it’s done in the existing project status too. It seems to be necessary (or clearer) because rails stores the enums as strings.
| module Projects | ||
| module PhaseStatuses | ||
| Status = Data.define(:code, :hexcode, :icon) do | ||
| def id = code&.to_s || :none |
There was a problem hiding this comment.
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 refresh_buttons | ||
| @project.available_phases.each do |phase| | ||
| update_via_turbo_stream( | ||
| component: Projects::PhaseStatusButtonComponent.new(phase: phase.reload) |
There was a problem hiding this comment.
mh the phase.reload is reloading in a loop, no? Why do we need to reload? Maybe we can reload all at once before the loop?
| def cascade_to_other_phases | ||
| project.available_phases.each do |other| | ||
| next if other == model | ||
|
|
||
| subject(:rendered_component) do | ||
| with_controller_class(Reporting::CostReportsController) do | ||
| with_request_url("/reporting/cost_reports") do | ||
| render_component(cost_query, **options) | ||
| target = cascaded_status(earlier: other.position < model.position) | ||
| next if target.nil? || other.status == target | ||
|
|
||
| other.update!(status: target) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| context "when there are no items" do | ||
| it "renders nothing" do | ||
| expect(rendered_component).to have_css ".generic-table--no-results-title", text: "There is currently nothing to display." | ||
| def cascaded_status(earlier:) | ||
| case model.status | ||
| when "open" then earlier ? nil : "open" | ||
| when "in_progress" then earlier ? "completed" : "open" | ||
| when "completed" then earlier ? "completed" : nil | ||
| end | ||
| end |
There was a problem hiding this comment.
here I am wondering too if we couldn't do this all in one step instead of in a loop 🤔
There was a problem hiding this comment.
Same. I guess you coud get all earlier/later phases and to a bulk edit on them
8bf3249 to
a5be46b
Compare
Adds the required fields to the Project Phase model, and adds some constants for the new phase statuses Adds a migration for that
Adds a new Project::PhaseStatusButtom component, mirroring the existing Project::StatusButton component. Registers a new highlighter (so far unused) Adds translations. Adds the option to render only an IconButton in the OpPrimer::StatusButton component.
Updates controllers & add routes to allow changing phase states. Colors of the buttons in the dropdown are now correctly set.
Adds some space between button & text, so when hovered it doesn’t look broken. Adds a story for the new status button component Cleans up some unnecessary code Linting
Adds tests for the new project-phase status button, and the phase-status service. The phase-status-button is now disabled when the user doesn’t have edit rights.
Follows the project status approach of passing in the user model, making lookbook preview work following the gating to admin users.
Adds an override to the icon color in the phase-state-button
2d5d6a3 to
468d6c2
Compare
Updates tests to reflect the user model being passed in. Reverts the unrelated changes made to the Sass file (auto-linting applied by my editor on save) Linting.
50dd5b2 to
f239466
Compare
HDinger
left a comment
There was a problem hiding this comment.
Works already quite nice. 👍 I left some comments.
| end | ||
|
|
||
| def refresh_buttons | ||
| @project.available_phases.each do |phase| |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| super() | ||
|
|
||
| @phase = phase | ||
| @user = user |
There was a problem hiding this comment.
What is the sense of passing the user through to all the components instead of using User.current like before?
ulferts
left a comment
There was a problem hiding this comment.
@planktonic , please consider this a sloppy review. I know nothing about the context of this PR and didn't spent a lot of time on it. That also means that maybe one or the other comment is off.
What I would definitely require though is the specs on the contract and another one on feature level.
| add_column :project_phases, :status, :integer, null: false, default: 0 | ||
| add_column :project_phase_journals, :status, :integer, null: false, default: 0 | ||
|
|
||
| add_index :project_phases, :status |
|
|
||
| module ProjectPhases | ||
| class UpdateStatusContract < BaseContract | ||
| delegate :project, to: :model |
There was a problem hiding this comment.
Why is this required? Only the BaseContract has a reference to project and it should have solved that. Although admittedly, just from looking at it I don't understand how. But if it is necessary, it should find its way into the superclass.
Edit: I see now that e.g. the UpdateContract does the same thing. Suggestion, move the delegation into the superclass, clean up the classes inheriting from it.
|
|
||
| def writable_attributes = %w[status] | ||
| end | ||
| end |
There was a problem hiding this comment.
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.
| end | ||
|
|
||
| def refresh_buttons | ||
| @project.available_phases.each do |phase| |
There was a problem hiding this comment.
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.
- Removes unnecessarily-nested flex layout in rows - Uses test-selector in tests instead of `.op-` classname - StatusButton is not rendered if the user doesn’t have edit access, instead of simply being disabled. Adds a preview for that, and updates tests.
Removes the global helper, moves the helper functions into the component Moves the new translation strings in `config/locales/en.yml`, out of the JS version.
The phase status enum now uses string instead of integers. Updated the migration.
Fixes the StatuButton component clobbering the generated classname attribute (that has the highlight class) when you also pass in a classname. Removes the extra code in the PhaseStatucButton that was getting around that.

Ticket
https://community.openproject.org/wp/OP-20396
What are you trying to accomplish?
Add a status field to project lifecycle phases. Allow that status to be changed (currently fixed to the statuses
open,in_progress, andcomplete).When the status is changed, update the status of other the phases, following the rules on the ticket.
Screenshots
With a menu open:
What approach did you choose and why?
Adds the required fields to the Project Phase model, and adds some constants for the new phase statuses, with a migration for that.
Registers a new highlighter.
Adds a new Project::PhaseStatusButtom component, mirroring the existing Project::StatusButton component. Overrides the color that is set on IconButton, so the phase status color is used instead. Adds lookbook previews to show these changes.
Adds tests.
AI involvement
Collaborative – AI generated a substantial part of the code; I reviewed and understand every line.
Merge checklist