Skip to content
12 changes: 10 additions & 2 deletions modules/backlogs/app/components/backlogs/sprint_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,16 @@ See COPYRIGHT and LICENSE files for more details.
)
) do |list|
%>
<% list.with_header(title: sprint.name) do |header| %>
<% list.with_header(title: sprint.name, title_arguments:) do |header| %>
<% header.with_description(display: :flex, direction: :column, classes: "row-gap-2") do %>
<% if goal_text.present? %>
<%= render(Primer::Beta::Text.new(id: sprint_goal_id, color: :muted, mr: 2)) do %>
<%= render(OpPrimer::ExpandableTextComponent.new(truncate: :multi_line, lines: 3)) do %>
<%= goal_text %>
<% end %>
<% end %>
<% end %>

<%= render(Primer::Alpha::Stack.new(direction: :horizontal, align: :center)) do %>
<%= render(Backlogs::SprintStatusBadgeComponent.new(sprint:)) %>

Expand Down Expand Up @@ -84,7 +92,7 @@ See COPYRIGHT and LICENSE files for more details.

header.with_menu(button_aria_label: t(".label_actions")) do |menu|
with_item_group(menu) do
if user_allowed?(:create_sprints)
if can_open_edit_dialog?
menu.with_item(
id: dom_target(sprint, :menu, :edit_sprint),
label: t(".action_menu.edit_sprint"),
Expand Down
24 changes: 24 additions & 0 deletions modules/backlogs/app/components/backlogs/sprint_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,22 @@ def finish_sprint_button_arguments
}
end

def goal_text
return @goal_text if defined?(@goal_text)

@goal_text = sprint.goal_text_for(project)
end
Comment thread
myabc marked this conversation as resolved.

def sprint_goal_id
dom_target(sprint, :goal)
end

def title_arguments
return {} if goal_text.blank?

{ aria: { describedby: sprint_goal_id } }
Comment thread
myabc marked this conversation as resolved.
end

def story_points_total
work_packages.filter_map(&:story_points).sum
end
Expand Down Expand Up @@ -120,5 +136,13 @@ def show_task_board_link?
def show_burndown_link?
sprint.active?
end

def can_open_edit_dialog?
if sprint.owned_by?(project)
user_allowed?(:create_sprints)
else
user_allowed?(:create_sprints) || user_allowed?(:create_sprints, project: sprint.project)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ See COPYRIGHT and LICENSE files for more details.
d.with_header(variant: :large)

d.with_body do
render(Backlogs::SprintFormComponent.new(sprint: @sprint))
render(Backlogs::SprintFormComponent.new(sprint: @sprint, project: @project))
end

d.with_footer do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,15 @@ class SprintDialogComponent < ApplicationComponent
STATE_DEFAULT = :create
STATE_OPTIONS = [STATE_DEFAULT, :edit].freeze

attr_reader :sprint, :state
attr_reader :sprint, :project, :state

delegate :create?, :edit?, to: :state

def initialize(sprint:, state: STATE_DEFAULT)
def initialize(sprint:, project:, state: STATE_DEFAULT)
super

@sprint = sprint
@project = project
@state = ActiveSupport::StringInquirer.new(fetch_or_fallback(STATE_OPTIONS, state, STATE_DEFAULT).to_s)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,35 @@ See COPYRIGHT and LICENSE files for more details.
) do |f|
flex_layout(mb: 2) do |flex|
if base_errors&.any?
flex.with_row do
render(Primer::Alpha::Banner.new(mb: 3, icon: :stop, scheme: :danger)) { base_errors.join("\n") }
flex.with_row(mb: 3) do
render(Primer::Alpha::Banner.new(scheme: :danger)) { base_errors.join("\n") }
end
end
if shared_sprint?
flex.with_row(mb: 3) do
render(Primer::Alpha::Banner.new(scheme: banner_scheme)) { banner_text }
end
end
flex.with_row(mb: 3) do
render Backlogs::Sprints::DetailsForm.new(f)
render Backlogs::Sprints::DetailsForm.new(f, disabled: !can_edit_sprint?)
end
flex.with_row(mb: 3, classes: "FormControl-horizontalGroup--sm-vertical") do
render Backlogs::Sprints::DatesForm.new(f, disabled: !can_edit_sprint?)
end
flex.with_row(classes: "FormControl-horizontalGroup--sm-vertical") do
render Backlogs::Sprints::DatesForm.new(f)
if shared_sprint?
flex.with_row(mb: 3) do
render(Primer::Forms::Separator.new)
end
end
flex.with_row do
f.fields_for(:goal, goal) do |goal_fields|
render Backlogs::Sprints::GoalForm.new(
goal_fields,
label: goal_label,
caption: goal_caption,
disabled: !can_edit_goal?
)
end
end
end
end
Expand Down
58 changes: 52 additions & 6 deletions modules/backlogs/app/components/backlogs/sprint_form_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,61 @@ class SprintFormComponent < ApplicationComponent

FORM_ID = SprintDialogComponent::FORM_ID

attr_reader :sprint, :base_errors
attr_reader :sprint, :project, :current_user, :base_errors

def initialize(sprint:, base_errors: nil)
def initialize(sprint:, project:, current_user: User.current, base_errors: nil)
super

@sprint = sprint
@project = project
@current_user = current_user
@base_errors = base_errors
end

def shared_sprint?
sprint.persisted? && !sprint.owned_by?(project)
end

def can_edit_sprint?
return true unless shared_sprint?

current_user.allowed_in_project?(:create_sprints, sprint.project)
end

def can_edit_goal?
return true unless shared_sprint?

current_user.allowed_in_project?(:create_sprints, project)
end

def banner_scheme
can_edit_sprint? ? :default : :warning
end

def banner_text
if can_edit_sprint?
t(".shared_sprint_info_banner")
else
t(".shared_sprint_warning_banner")
end
end

def goal
sprint.goals.find_or_initialize_by(project:)
end

def goal_label
if shared_sprint?
I18n.t("backlogs.sprint_form.goal_for_this_project_label", attribute: Sprint.human_attribute_name(:goal))
else
Sprint.human_attribute_name(:goal)
end
end

def goal_caption
I18n.t("backlogs.sprint_form.goal_caption") if shared_sprint?
end

private

def http_verb
Expand All @@ -54,18 +100,18 @@ def http_verb

def form_url
if sprint.new_record?
project_backlogs_sprints_path(sprint.project_id, all_backlogs_params)
project_backlogs_sprints_path(project, all_backlogs_params)
else
project_backlogs_sprint_path(sprint.project_id, sprint.id, all_backlogs_params)
project_backlogs_sprint_path(project, sprint, all_backlogs_params)
end
end

def data_attributes
{
controller: "refresh-on-form-changes",
"refresh-on-form-changes-target": "form",
"refresh-on-form-changes-turbo-stream-url-value": refresh_form_project_backlogs_sprints_path(sprint.project_id,
all_backlogs_params)
"refresh-on-form-changes-turbo-stream-url-value":
refresh_form_project_backlogs_sprints_path(project, all_backlogs_params)
}
end
end
Expand Down
38 changes: 36 additions & 2 deletions modules/backlogs/app/contracts/backlogs/sprints/base_contract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@

module Backlogs::Sprints
class BaseContract < ::ModelContract
validate :user_authorized
SPRINT_ATTRIBUTES = %w[name project_id start_date finish_date].freeze

validate :user_authorized_for_sprint_attributes
validate :user_authorized_for_goal_attributes

def self.model
Sprint
Expand All @@ -40,15 +43,46 @@ def self.model
attribute :project_id
attribute :start_date
attribute :finish_date
attribute :goals_attributes, readable: false

private

def user_authorized
def user_authorized_for_sprint_attributes
return unless model.project
return unless sprint_attributes_changed?

unless user.allowed_in_project?(:create_sprints, model.project)
errors.add :base, :error_unauthorized
end
end

def sprint_attributes_changed?
model.new_record? || model.changed.intersect?(SPRINT_ATTRIBUTES)
end

def user_authorized_for_goal_attributes
changed_goals.each do |goal|
project = goal.project

unless project && sprint_visible_to_goal_project?(project) && user.allowed_in_project?(:create_sprints, project)
errors.add :base, :error_unauthorized
end
end
end
Comment thread
myabc marked this conversation as resolved.

def changed_goals
goals_association = model.association(:goals)
return [] unless goals_association.loaded? || goals_association.target.any?

goals_association.target.select { |goal| goal.changed? || goal.marked_for_destruction? }
end

def sprint_visible_to_goal_project?(project)
if model.new_record?
model.project == project
else
model.visible_to?(project)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def load_project
end

def load_sprint
@sprint_id = params.delete(:sprint_id)
@sprint_id = params[:sprint_id].presence
return unless @sprint_id

@sprint = Sprint.for_project(@project).visible.find(@sprint_id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def load_sprint_data
@sprints = Sprint.for_project(@project)
.not_completed
.order_by_date
.includes(:project, :task_boards)
.includes(:project, :task_boards, :goals)
@active_sprint_ids = @sprints.select(&:active?).map(&:id)

@work_packages_by_sprint_id = WorkPackage
Expand Down
Loading
Loading