Commit 06cef29a authored by Mike Greiling's avatar Mike Greiling

Merge branch '44984-use-serializer-for-issuable-sidebar' into 'master'

Resolve "Create a serializer to render issuables sidebar"

Closes #44984

See merge request gitlab-org/gitlab-ce!23379
parents 88094c44 95aae95a
...@@ -79,11 +79,12 @@ Sidebar.prototype.sidebarToggleClicked = function(e, triggered) { ...@@ -79,11 +79,12 @@ Sidebar.prototype.sidebarToggleClicked = function(e, triggered) {
Sidebar.prototype.toggleTodo = function(e) { Sidebar.prototype.toggleTodo = function(e) {
var $btnText, $this, $todoLoading, ajaxType, url; var $btnText, $this, $todoLoading, ajaxType, url;
$this = $(e.currentTarget); $this = $(e.currentTarget);
ajaxType = $this.attr('data-delete-path') ? 'delete' : 'post'; ajaxType = $this.data('deletePath') ? 'delete' : 'post';
if ($this.attr('data-delete-path')) {
url = '' + $this.attr('data-delete-path'); if ($this.data('deletePath')) {
url = '' + $this.data('deletePath');
} else { } else {
url = '' + $this.data('url'); url = '' + $this.data('createPath');
} }
$this.tooltip('hide'); $this.tooltip('hide');
...@@ -119,14 +120,14 @@ Sidebar.prototype.todoUpdateDone = function(data) { ...@@ -119,14 +120,14 @@ Sidebar.prototype.todoUpdateDone = function(data) {
.removeClass('is-loading') .removeClass('is-loading')
.enable() .enable()
.attr('aria-label', $el.data(`${attrPrefix}Text`)) .attr('aria-label', $el.data(`${attrPrefix}Text`))
.attr('data-delete-path', deletePath) .attr('title', $el.data(`${attrPrefix}Text`))
.attr('title', $el.data(`${attrPrefix}Text`)); .data('deletePath', deletePath);
if ($el.hasClass('has-tooltip')) { if ($el.hasClass('has-tooltip')) {
$el.tooltip('_fixTitle'); $el.tooltip('_fixTitle');
} }
if ($el.data(`${attrPrefix}Icon`)) { if (typeof $el.data('isCollapsed') !== 'undefined') {
$elText.html($el.data(`${attrPrefix}Icon`)); $elText.html($el.data(`${attrPrefix}Icon`));
} else { } else {
$elText.text($el.data(`${attrPrefix}Text`)); $elText.text($el.data(`${attrPrefix}Text`));
......
...@@ -71,7 +71,7 @@ export default class SidebarStore { ...@@ -71,7 +71,7 @@ export default class SidebarStore {
} }
findAssignee(findAssignee) { findAssignee(findAssignee) {
return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0]; return this.assignees.find(assignee => assignee.id === findAssignee.id);
} }
removeAssignee(removeAssignee) { removeAssignee(removeAssignee) {
......
...@@ -5,7 +5,6 @@ module IssuableActions ...@@ -5,7 +5,6 @@ module IssuableActions
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
included do included do
before_action :labels, only: [:show, :new, :edit]
before_action :authorize_destroy_issuable!, only: :destroy before_action :authorize_destroy_issuable!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update before_action :authorize_admin_issuable!, only: :bulk_update
end end
...@@ -25,7 +24,10 @@ module IssuableActions ...@@ -25,7 +24,10 @@ module IssuableActions
def show def show
respond_to do |format| respond_to do |format|
format.html format.html do
@issuable_sidebar = serializer.represent(issuable, serializer: 'sidebar') # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
format.json do format.json do
render json: serializer.represent(issuable, serializer: params[:serializer]) render json: serializer.represent(issuable, serializer: params[:serializer])
end end
...@@ -168,10 +170,6 @@ module IssuableActions ...@@ -168,10 +170,6 @@ module IssuableActions
end end
end end
def labels
@labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def authorize_destroy_issuable! def authorize_destroy_issuable!
unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable) unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable)
return access_denied! return access_denied!
......
...@@ -8,7 +8,7 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap ...@@ -8,7 +8,7 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do
labels @issuable_sidebar = serializer.represent(@merge_request, serializer: 'sidebar')
end end
format.json do format.json do
...@@ -60,9 +60,15 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap ...@@ -60,9 +60,15 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap
end end
end end
private
def authorize_can_resolve_conflicts! def authorize_can_resolve_conflicts!
@conflicts_list = ::MergeRequests::Conflicts::ListService.new(@merge_request) @conflicts_list = ::MergeRequests::Conflicts::ListService.new(@merge_request)
return render_404 unless @conflicts_list.can_be_resolved_by?(current_user) return render_404 unless @conflicts_list.can_be_resolved_by?(current_user)
end end
def serializer
MergeRequestSerializer.new(current_user: current_user, project: project)
end
end end
...@@ -22,8 +22,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -22,8 +22,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
format.html format.html
format.json do format.json do
render json: { render json: {
html: view_to_html_string("projects/merge_requests/_merge_requests"), html: view_to_html_string("projects/merge_requests/_merge_requests")
labels: @labels.as_json(methods: :text_color)
} }
end end
end end
...@@ -43,8 +42,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -43,8 +42,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@noteable = @merge_request @noteable = @merge_request
@commits_count = @merge_request.commits_count @commits_count = @merge_request.commits_count
@issuable_sidebar = serializer.represent(@merge_request, serializer: 'sidebar')
labels
set_pipeline_variables set_pipeline_variables
......
...@@ -23,30 +23,41 @@ module IssuablesHelper ...@@ -23,30 +23,41 @@ module IssuablesHelper
end end
end end
def sidebar_due_date_tooltip_label(issuable) def sidebar_milestone_tooltip_label(milestone)
if issuable.due_date return _('Milestone') unless milestone.present?
"#{_('Due date')}<br />#{due_date_remaining_days(issuable)}"
else [milestone[:title], sidebar_milestone_remaining_days(milestone) || _('Milestone')].join('<br/>')
_('Due date') end
end
def sidebar_milestone_remaining_days(milestone)
due_date_with_remaining_days(milestone[:due_date], milestone[:start_date])
end
def sidebar_due_date_tooltip_label(due_date)
[_('Due date'), due_date_with_remaining_days(due_date)].compact.join('<br/>')
end end
def due_date_remaining_days(issuable) def due_date_with_remaining_days(due_date, start_date = nil)
remaining_days_in_words = remaining_days_in_words(issuable) return unless due_date
"#{issuable.due_date.to_s(:medium)} (#{remaining_days_in_words})" "#{due_date.to_s(:medium)} (#{remaining_days_in_words(due_date, start_date)})"
end
def sidebar_label_filter_path(base_path, label_name)
query_params = { label_name: [label_name] }.to_query
"#{base_path}?#{query_params}"
end end
def multi_label_name(current_labels, default_label) def multi_label_name(current_labels, default_label)
if current_labels && current_labels.any? return default_label if current_labels.blank?
title = current_labels.first.try(:title)
if current_labels.size > 1 title = current_labels.first.try(:title) || current_labels.first[:title]
"#{title} +#{current_labels.size - 1} more"
else if current_labels.size > 1
title "#{title} +#{current_labels.size - 1} more"
end
else else
default_label title
end end
end end
...@@ -197,19 +208,11 @@ module IssuablesHelper ...@@ -197,19 +208,11 @@ module IssuablesHelper
output.join.html_safe output.join.html_safe
end end
# rubocop: disable CodeReuse/ActiveRecord
def issuable_todo(issuable)
if current_user
current_user.todos.find_by(target: issuable, state: :pending)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def issuable_labels_tooltip(labels, limit: 5) def issuable_labels_tooltip(labels, limit: 5)
first, last = labels.partition.with_index { |_, i| i < limit } first, last = labels.partition.with_index { |_, i| i < limit }
if labels && labels.any? if labels && labels.any?
label_names = first.collect(&:name) label_names = first.collect { |label| label.fetch(:title) }
label_names << "and #{last.size} more" unless last.empty? label_names << "and #{last.size} more" unless last.empty?
label_names.join(', ') label_names.join(', ')
...@@ -356,12 +359,6 @@ module IssuablesHelper ...@@ -356,12 +359,6 @@ module IssuablesHelper
issuable.model_name.human.downcase issuable.model_name.human.downcase
end end
def selected_labels
Array(params[:label_name]).map do |label_name|
Label.new(title: label_name)
end
end
def has_filter_bar_param? def has_filter_bar_param?
finder.class.scalar_params.any? { |p| params[p].present? } finder.class.scalar_params.any? { |p| params[p].present? }
end end
...@@ -386,19 +383,20 @@ module IssuablesHelper ...@@ -386,19 +383,20 @@ module IssuablesHelper
params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] } params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] }
end end
def issuable_todo_button_data(issuable, todo, is_collapsed) def issuable_todo_button_data(issuable, is_collapsed)
{ {
todo_text: "Add todo", todo_text: _('Add todo'),
mark_text: "Mark todo as done", mark_text: _('Mark todo as done'),
todo_icon: (is_collapsed ? sprite_icon('todo-add') : nil), todo_icon: sprite_icon('todo-add'),
mark_icon: (is_collapsed ? sprite_icon('todo-done', css_class: 'todo-undone') : nil), mark_icon: sprite_icon('todo-done', css_class: 'todo-undone'),
issuable_id: issuable.id, issuable_id: issuable[:id],
issuable_type: issuable.class.name.underscore, issuable_type: issuable[:type],
url: project_todos_path(@project), create_path: issuable[:create_todo_path],
delete_path: (dashboard_todo_path(todo) if todo), delete_path: issuable.dig(:current_user, :todo, :delete_path),
placement: (is_collapsed ? 'left' : nil), placement: is_collapsed ? 'left' : nil,
container: (is_collapsed ? 'body' : nil), container: is_collapsed ? 'body' : nil,
boundary: 'viewport' boundary: 'viewport',
is_collapsed: is_collapsed
} }
end end
...@@ -418,27 +416,20 @@ module IssuablesHelper ...@@ -418,27 +416,20 @@ module IssuablesHelper
end end
end end
def issuable_sidebar_options(issuable, can_edit_issuable) def issuable_sidebar_options(issuable)
{ {
endpoint: "#{issuable_json_path(issuable)}?serializer=sidebar", endpoint: "#{issuable[:issuable_json_path]}?serializer=sidebar_extras",
toggleSubscriptionEndpoint: toggle_subscription_path(issuable), toggleSubscriptionEndpoint: issuable[:toggle_subscription_path],
moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable), moveIssueEndpoint: issuable[:move_issue_path],
projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id), projectsAutocompleteEndpoint: issuable[:projects_autocomplete_path],
editable: can_edit_issuable, editable: issuable.dig(:current_user, :can_edit),
currentUser: UserSerializer.new.represent(current_user), currentUser: issuable[:current_user],
rootPath: root_path, rootPath: root_path,
fullPath: @project.full_path fullPath: issuable[:project_full_path]
} }
end end
def parent def parent
@project || @group @project || @group
end end
def issuable_milestone_tooltip_title(issuable)
if issuable.milestone
milestone_tooltip = milestone_tooltip_title(issuable.milestone)
_('Milestone') + (milestone_tooltip ? ': ' + milestone_tooltip : '')
end
end
end end
...@@ -114,12 +114,6 @@ module MilestonesHelper ...@@ -114,12 +114,6 @@ module MilestonesHelper
end end
end end
def milestone_tooltip_title(milestone)
if milestone
"#{milestone.title}<br />#{milestone_tooltip_due_date(milestone)}"
end
end
def milestone_time_for(date, date_type) def milestone_time_for(date, date_type)
title = date_type == :start ? "Start date" : "End date" title = date_type == :start ? "Start date" : "End date"
...@@ -173,7 +167,7 @@ module MilestonesHelper ...@@ -173,7 +167,7 @@ module MilestonesHelper
def milestone_tooltip_due_date(milestone) def milestone_tooltip_due_date(milestone)
if milestone.due_date if milestone.due_date
"#{milestone.due_date.to_s(:medium)} (#{remaining_days_in_words(milestone)})" "#{milestone.due_date.to_s(:medium)} (#{remaining_days_in_words(milestone.due_date, milestone.start_date)})"
else else
_('Milestone') _('Milestone')
end end
......
...@@ -1422,6 +1422,10 @@ class User < ActiveRecord::Base ...@@ -1422,6 +1422,10 @@ class User < ActiveRecord::Base
todos.where(id: ids) todos.where(id: ids)
end end
def pending_todo_for(target)
todos.find_by(target: target, state: :pending)
end
# @deprecated # @deprecated
alias_method :owned_or_masters_groups, :owned_or_maintainers_groups alias_method :owned_or_masters_groups, :owned_or_maintainers_groups
......
...@@ -44,14 +44,14 @@ module EntityDateHelper ...@@ -44,14 +44,14 @@ module EntityDateHelper
# It returns "Upcoming" for upcoming entities # It returns "Upcoming" for upcoming entities
# If due date is provided, it returns "# days|weeks|months remaining|ago" # If due date is provided, it returns "# days|weeks|months remaining|ago"
# If start date is provided and elapsed, with no due date, it returns "# days elapsed" # If start date is provided and elapsed, with no due date, it returns "# days elapsed"
def remaining_days_in_words(entity) def remaining_days_in_words(due_date, start_date = nil)
if entity.try(:expired?) if due_date&.past?
content_tag(:strong, 'Past due') content_tag(:strong, 'Past due')
elsif entity.try(:upcoming?) elsif start_date&.future?
content_tag(:strong, 'Upcoming') content_tag(:strong, 'Upcoming')
elsif entity.due_date elsif due_date
is_upcoming = (entity.due_date - Date.today).to_i > 0 is_upcoming = (due_date - Date.today).to_i > 0
time_ago = time_ago_in_words(entity.due_date) time_ago = time_ago_in_words(due_date)
# https://gitlab.com/gitlab-org/gitlab-ce/issues/49440 # https://gitlab.com/gitlab-org/gitlab-ce/issues/49440
# #
...@@ -63,8 +63,8 @@ module EntityDateHelper ...@@ -63,8 +63,8 @@ module EntityDateHelper
remaining_or_ago = is_upcoming ? _("remaining") : _("ago") remaining_or_ago = is_upcoming ? _("remaining") : _("ago")
"#{content} #{remaining_or_ago}".html_safe "#{content} #{remaining_or_ago}".html_safe
elsif entity.start_date && entity.start_date.past? elsif start_date&.past?
days = entity.elapsed_days days = (Date.today - start_date).to_i
"#{content_tag(:strong, days)} #{'day'.pluralize(days)} elapsed".html_safe "#{content_tag(:strong, days)} #{'day'.pluralize(days)} elapsed".html_safe
end end
end end
......
# frozen_string_literal: true
class IssuableSidebarBasicEntity < Grape::Entity
include RequestAwareEntity
expose :id
expose :type do |issuable|
issuable.to_ability_name
end
expose :author_id
expose :project_id do |issuable|
issuable.project.id
end
expose :discussion_locked
expose :reference do |issuable|
issuable.to_reference(issuable.project, full: true)
end
expose :milestone, using: ::API::Entities::Milestone
expose :labels, using: LabelEntity
expose :current_user, if: lambda { |_issuable| current_user } do
expose :current_user, merge: true, using: API::Entities::UserBasic
expose :todo, using: IssuableSidebarTodoEntity do |issuable|
current_user.pending_todo_for(issuable)
end
expose :can_edit do |issuable|
can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
end
expose :can_move do |issuable|
issuable.can_move?(current_user)
end
expose :can_admin_label do |issuable|
can?(current_user, :admin_label, issuable.project)
end
end
expose :issuable_json_path do |issuable|
if issuable.is_a?(MergeRequest)
project_merge_request_path(issuable.project, issuable.iid, :json)
else
project_issue_path(issuable.project, issuable.iid, :json)
end
end
expose :namespace_path do |issuable|
issuable.project.namespace.full_path
end
expose :project_path do |issuable|
issuable.project.path
end
expose :project_full_path do |issuable|
issuable.project.full_path
end
expose :project_issuables_path do |issuable|
project = issuable.project
namespace = project.namespace
if issuable.is_a?(MergeRequest)
namespace_project_merge_requests_path(namespace, project)
else
namespace_project_issues_path(namespace, project)
end
end
expose :create_todo_path do |issuable|
project_todos_path(issuable.project)
end
expose :project_milestones_path do |issuable|
project_milestones_path(issuable.project, :json)
end
expose :project_labels_path do |issuable|
project_labels_path(issuable.project, :json, include_ancestor_groups: true)
end
expose :toggle_subscription_path do |issuable|
toggle_subscription_path(issuable)
end
expose :move_issue_path do |issuable|
move_namespace_project_issue_path(
namespace_id: issuable.project.namespace.to_param,
project_id: issuable.project,
id: issuable
)
end
expose :projects_autocomplete_path do |issuable|
autocomplete_projects_path(project_id: issuable.project.id)
end
private
def current_user
request.current_user
end
end
# frozen_string_literal: true # frozen_string_literal: true
class IssuableSidebarEntity < Grape::Entity class IssuableSidebarExtrasEntity < Grape::Entity
include TimeTrackableEntity
include RequestAwareEntity include RequestAwareEntity
include TimeTrackableEntity
expose :participants, using: ::API::Entities::UserBasic do |issuable| expose :participants, using: ::API::Entities::UserBasic do |issuable|
issuable.participants(request.current_user) issuable.participants(request.current_user)
......
# frozen_string_literal: true
class IssuableSidebarTodoEntity < Grape::Entity
include Gitlab::Routing
expose :id
expose :delete_path do |todo|
dashboard_todo_path(todo) if todo
end
end
...@@ -37,7 +37,7 @@ class IssueBoardEntity < Grape::Entity ...@@ -37,7 +37,7 @@ class IssueBoardEntity < Grape::Entity
end end
expose :issue_sidebar_endpoint, if: -> (issue) { issue.project } do |issue| expose :issue_sidebar_endpoint, if: -> (issue) { issue.project } do |issue|
project_issue_path(issue.project, issue, format: :json, serializer: 'sidebar') project_issue_path(issue.project, issue, format: :json, serializer: 'sidebar_extras')
end end
expose :toggle_subscription_endpoint, if: -> (issue) { issue.project } do |issue| expose :toggle_subscription_endpoint, if: -> (issue) { issue.project } do |issue|
......
...@@ -2,13 +2,15 @@ ...@@ -2,13 +2,15 @@
class IssueSerializer < BaseSerializer class IssueSerializer < BaseSerializer
# This overrided method takes care of which entity should be used # This overrided method takes care of which entity should be used
# to serialize the `issue` based on `basic` key in `opts` param. # to serialize the `issue` based on `serializer` key in `opts` param.
# Hence, `entity` doesn't need to be declared on the class scope. # Hence, `entity` doesn't need to be declared on the class scope.
def represent(issue, opts = {}) def represent(issue, opts = {})
entity = entity =
case opts[:serializer] case opts[:serializer]
when 'sidebar' when 'sidebar'
IssueSidebarEntity IssueSidebarBasicEntity
when 'sidebar_extras'
IssueSidebarExtrasEntity
when 'board' when 'board'
IssueBoardEntity IssueBoardEntity
else else
......
# frozen_string_literal: true
class IssueSidebarBasicEntity < IssuableSidebarBasicEntity
expose :due_date
expose :confidential
end
# frozen_string_literal: true # frozen_string_literal: true
class IssueSidebarEntity < IssuableSidebarEntity class IssueSidebarExtrasEntity < IssuableSidebarExtrasEntity
expose :assignees, using: API::Entities::UserBasic expose :assignees, using: API::Entities::UserBasic
end end
# frozen_string_literal: true # frozen_string_literal: true
class MergeRequestBasicEntity < IssuableSidebarEntity class MergeRequestBasicEntity < Grape::Entity
expose :assignee_id expose :assignee_id
expose :merge_status expose :merge_status
expose :merge_error expose :merge_error
......
# frozen_string_literal: true
class MergeRequestBasicSerializer < BaseSerializer
entity MergeRequestBasicEntity
end
...@@ -7,9 +7,14 @@ class MergeRequestSerializer < BaseSerializer ...@@ -7,9 +7,14 @@ class MergeRequestSerializer < BaseSerializer
def represent(merge_request, opts = {}) def represent(merge_request, opts = {})
entity = entity =
case opts[:serializer] case opts[:serializer]
when 'basic', 'sidebar' when 'sidebar'
MergeRequestSidebarBasicEntity
when 'sidebar_extras'
IssuableSidebarExtrasEntity
when 'basic'
MergeRequestBasicEntity MergeRequestBasicEntity
else # It's 'widget' else
# fallback to widget for old poll requests without `serializer` set
MergeRequestWidgetEntity MergeRequestWidgetEntity
end end
......
# frozen_string_literal: true
class MergeRequestSidebarBasicEntity < IssuableSidebarBasicEntity
expose :assignee, if: lambda { |issuable| issuable.assignee } do
expose :assignee, merge: true, using: API::Entities::UserBasic
expose :can_merge do |issuable|
issuable.can_be_merged_by?(issuable.assignee)
end
end
end
...@@ -88,4 +88,4 @@ ...@@ -88,4 +88,4 @@
%section.issuable-discussion %section.issuable-discussion
= render 'projects/issues/discussion' = render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable: @issue = render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @issue.assignees
- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
= render "projects/merge_requests/mr_title"
.merge-request-details.issuable-details
= render "projects/merge_requests/mr_box"
= render 'shared/issuable/sidebar', issuable: @merge_request
#conflicts{ "v-cloak" => "true", data: { conflicts_path: conflicts_project_merge_request_path(@merge_request.project, @merge_request, format: :json),
resolve_conflicts_path: resolve_conflicts_project_merge_request_path(@merge_request.project, @merge_request) } }
.loading{ "v-if" => "isLoading" }
%i.fa.fa-spinner.fa-spin
.nothing-here-block{ "v-if" => "hasError" }
{{conflictsData.errorMessage}}
= render partial: "projects/merge_requests/conflicts/commit_stats"
.files-wrapper{ "v-if" => "!isLoading && !hasError" }
.files
.diff-file.file-holder.conflict{ "v-for" => "file in conflictsData.files" }
.js-file-title.file-title
%i.fa.fa-fw{ ":class" => "file.iconClass" }
%strong {{file.filePath}}
= render partial: 'projects/merge_requests/conflicts/file_actions'
.diff-content.diff-wrap-lines
.diff-wrap-lines.code.file-content.js-syntax-highlight{ "v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
= render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines"
.diff-wrap-lines.code.file-content.js-syntax-highlight{ "v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
%parallel-conflict-lines{ ":file" => "file" }
%div{ "v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'" }
= render partial: "projects/merge_requests/conflicts/components/diff_file_editor"
= render partial: "projects/merge_requests/conflicts/submit_form"
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
.merge-request-details.issuable-details .merge-request-details.issuable-details
= render "projects/merge_requests/mr_box" = render "projects/merge_requests/mr_box"
= render 'shared/issuable/sidebar', issuable: @merge_request = render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees
#conflicts{ "v-cloak" => "true", data: { conflicts_path: conflicts_project_merge_request_path(@merge_request.project, @merge_request, format: :json), #conflicts{ "v-cloak" => "true", data: { conflicts_path: conflicts_project_merge_request_path(@merge_request.project, @merge_request, format: :json),
resolve_conflicts_path: resolve_conflicts_project_merge_request_path(@merge_request.project, @merge_request) } } resolve_conflicts_path: resolve_conflicts_project_merge_request_path(@merge_request.project, @merge_request) } }
......
...@@ -86,7 +86,8 @@ ...@@ -86,7 +86,8 @@
.mr-loading-status .mr-loading-status
= spinner = spinner
= render 'shared/issuable/sidebar', issuable: @merge_request = render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees
- if @merge_request.can_be_reverted?(current_user) - if @merge_request.can_be_reverted?(current_user)
= render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title = render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title
- if @merge_request.can_be_cherry_picked? - if @merge_request.can_be_cherry_picked?
......
- todo = issuable_todo(issuable) -# `assignees` is being passed in for populating selected assignee values in the select box and rendering the assignee link
This should be removed when this sidebar is converted to Vue since assignee data is also available in the `issuable_sidebar` hash
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } - issuable_type = issuable_sidebar[:type]
.issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } } - signed_in = !!issuable_sidebar.dig(:current_user, :id)
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar
.block.issuable-sidebar-header .block.issuable-sidebar-header
- if current_user - if signed_in
%span.issuable-header-text.hide-collapsed.float-left %span.issuable-header-text.hide-collapsed.float-left
= _('Todo') = _('Todo')
%a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } } %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
= sidebar_gutter_toggle_icon = sidebar_gutter_toggle_icon
- if current_user - if signed_in
= render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f| = form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
- if current_user - if signed_in
.block.todo.hide-expanded .block.todo.hide-expanded
= render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar, is_collapsed: true
.block.assignee.qa-assignee-block .block.assignee.qa-assignee-block
= render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present? = render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees
= render_if_exists 'shared/issuable/sidebar_item_epic', issuable: issuable = render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar
- milestone = issuable_sidebar[:milestone] || {}
.block.milestone .block.milestone
.sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } .sidebar-collapsed-icon.has-tooltip{ title: sidebar_milestone_tooltip_label(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
= icon('clock-o', 'aria-hidden': 'true') = icon('clock-o', 'aria-hidden': 'true')
%span.milestone-title.collapse-truncated-title %span.milestone-title.collapse-truncated-title
- if issuable.milestone - if milestone.present?
= issuable.milestone.title = milestone[:title]
- else - else
= _('None') = _('None')
.title.hide-collapsed .title.hide-collapsed
...@@ -35,49 +40,50 @@ ...@@ -35,49 +40,50 @@
- if can_edit_issuable - if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed .value.hide-collapsed
- if issuable.milestone - if milestone.present?
= link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_due_date(issuable.milestone), data: { container: "body", html: 'true', boundary: 'viewport' } = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport' }
- else - else
%span.no-value %span.no-value
= _('None') = _('None')
.selectbox.hide-collapsed .selectbox.hide-collapsed
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil = f.hidden_field 'milestone_id', value: milestone[:id], id: nil
= dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: project_milestones_path(@project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true, default_no: true, selected: (issuable.milestone.name if issuable.milestone), null_default: true, display: 'static' }}) = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], milestones: issuable_sidebar[:project_milestones_path], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }})
- if issuable.has_attribute?(:time_estimate)
#issuable-time-tracker.block #issuable-time-tracker.block
// Fallback while content is loading // Fallback while content is loading
.title.hide-collapsed .title.hide-collapsed
= _('Time tracking') = _('Time tracking')
= icon('spinner spin', 'aria-hidden': 'true') = icon('spinner spin', 'aria-hidden': 'true')
- if issuable.has_attribute?(:due_date)
- if issuable_sidebar.has_key?(:due_date)
.block.due_date .block.due_date
.sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable) } .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) }
= icon('calendar', 'aria-hidden': 'true') = icon('calendar', 'aria-hidden': 'true')
%span.js-due-date-sidebar-value %span.js-due-date-sidebar-value
= issuable.due_date.try(:to_s, :medium) || 'None' = issuable_sidebar[:due_date].try(:to_s, :medium) || 'None'
.title.hide-collapsed .title.hide-collapsed
= _('Due date') = _('Due date')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed .value.hide-collapsed
%span.value-content %span.value-content
- if issuable.due_date - if issuable_sidebar[:due_date]
%span.bold= issuable.due_date.to_s(:medium) %span.bold= issuable_sidebar[:due_date].to_s(:medium)
- else - else
%span.no-value %span.no-value
= _('No due date') = _('No due date')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - if can_edit_issuable
%span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable.due_date.nil?) } %span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable_sidebar[:due_date].nil?) }
\- \-
%a.js-remove-due-date{ href: "#", role: "button" } %a.js-remove-due-date{ href: "#", role: "button" }
= _('remove due date') = _('remove due date')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - if can_edit_issuable
.selectbox.hide-collapsed .selectbox.hide-collapsed
= f.hidden_field :due_date, value: issuable.due_date.try(:strftime, 'yy-mm-dd') = f.hidden_field :due_date, value: issuable_sidebar[:due_date].try(:strftime, 'yy-mm-dd')
.dropdown .dropdown
%button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), display: 'static' } } %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable_type}[due_date]", ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], display: 'static' } }
%span.dropdown-toggle-text %span.dropdown-toggle-text
= _('Due date') = _('Due date')
= icon('chevron-down', 'aria-hidden': 'true') = icon('chevron-down', 'aria-hidden': 'true')
...@@ -86,56 +92,56 @@ ...@@ -86,56 +92,56 @@
= dropdown_content do = dropdown_content do
.js-due-date-calendar .js-due-date-calendar
- if @labels - selected_labels = issuable_sidebar[:labels]
- selected_labels = issuable.labels .block.labels
.block.labels .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } }
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body", boundary: 'viewport' } } = icon('tags', 'aria-hidden': 'true')
= icon('tags', 'aria-hidden': 'true') %span
%span = selected_labels.size
= selected_labels.size .title.hide-collapsed
.title.hide-collapsed = _('Labels')
= _('Labels') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable
- if can_edit_issuable = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' .value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) }
.value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) } - if selected_labels.any?
- if selected_labels.any?
- selected_labels.each do |label|
= link_to_label(label, subject: issuable.project, type: issuable.to_ability_name)
- else
%span.no-value
= _('None')
.selectbox.hide-collapsed
- selected_labels.each do |label| - selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil = link_to sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label[:title]) do
.dropdown %span.badge.color-label.has-tooltip{ style: "background-color: #{label[:color]}; color: #{label[:text_color]}", title: label[:description], data: { container: "body" } }
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path_with_defaults if @project), display: 'static' } } = label[:title]
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } - else
= multi_label_name(selected_labels, "Labels") %span.no-value
= icon('chevron-down', 'aria-hidden': 'true') = _('None')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable .selectbox.hide-collapsed
= render partial: "shared/issuable/label_page_default" - selected_labels.each do |label|
- if can? current_user, :admin_label, @project and @project = hidden_field_tag "#{issuable_type}[label_names][]", label[:id], id: nil
= render partial: "shared/issuable/label_page_create" .dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable_type}[label_names][]", ability_name: issuable_type, show_no: "true", show_any: "true", namespace_path: issuable_sidebar[:namespace_path], project_path: issuable_sidebar[:project_path], issue_update: issuable_sidebar[:issuable_json_path], labels: issuable_sidebar[:project_labels_path], display: 'static' } }
= render_if_exists 'shared/issuable/sidebar_weight', issuable: issuable %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
- if issuable.has_attribute?(:confidential) = icon('chevron-down', 'aria-hidden': 'true')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default"
- if issuable_sidebar.dig(:current_user, :can_admin_label)
= render partial: "shared/issuable/label_page_create"
= render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar
- if issuable_sidebar.has_key?(:confidential)
-# haml-lint:disable InlineJavaScript -# haml-lint:disable InlineJavaScript
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: @issue.confidential, is_editable: can_edit_issuable }.to_json.html_safe %script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe
#js-confidential-entry-point #js-confidential-entry-point
- if issuable.has_attribute?(:discussion_locked) -# haml-lint:disable InlineJavaScript
-# haml-lint:disable InlineJavaScript %script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe
%script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe #js-lock-entry-point
#js-lock-entry-point
.js-sidebar-participants-entry-point .js-sidebar-participants-entry-point
- if current_user - if signed_in
.js-sidebar-subscriptions-entry-point .js-sidebar-subscriptions-entry-point
- project_ref = cross_project_reference(@project, issuable) - project_ref = issuable_sidebar[:reference]
.block.project-reference .block.project-reference
.sidebar-collapsed-icon.dont-change-state .sidebar-collapsed-icon.dont-change-state
= clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left", boundary: 'viewport') = clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left", boundary: 'viewport')
...@@ -145,7 +151,8 @@ ...@@ -145,7 +151,8 @@
%cite{ title: project_ref } %cite{ title: project_ref }
= project_ref = project_ref
= clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left", boundary: 'viewport') = clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left", boundary: 'viewport')
- if current_user && issuable.can_move?(current_user)
- if issuable_sidebar.dig(:current_user, :can_move)
.block.js-sidebar-move-issue-block .block.js-sidebar-move-issue-block
.sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') } .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') }
= custom_icon('icon_arrow_right') = custom_icon('icon_arrow_right')
...@@ -164,4 +171,4 @@ ...@@ -164,4 +171,4 @@
= icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon') = icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
-# haml-lint:disable InlineJavaScript -# haml-lint:disable InlineJavaScript
%script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable, can_edit_issuable).to_json.html_safe %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable_sidebar).to_json.html_safe
- if issuable.is_a?(Issue) - issuable_type = issuable_sidebar[:type]
#js-vue-sidebar-assignees{ data: { field: "#{issuable.to_ability_name}[assignee_ids]", signed_in: signed_in } } - signed_in = !!issuable_sidebar.dig(:current_user, :id)
- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
- if issuable_type == "issue"
#js-vue-sidebar-assignees{ data: { field: "#{issuable_type}[assignee_ids]", signed_in: signed_in } }
.title.hide-collapsed .title.hide-collapsed
= _('Assignee') = _('Assignee')
= icon('spinner spin') = icon('spinner spin')
- else - else
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body", boundary: 'viewport' }, title: sidebar_assignee_tooltip_label(issuable) } - assignee = assignees.first
- if issuable.assignee .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body", boundary: 'viewport' }, title: (issuable_sidebar.dig(:assignee, :name) || _('Assignee')) }
= link_to_member(@project, issuable.assignee, size: 24) - if issuable_sidebar[:assignee]
= link_to_member(@project, assignee, size: 24)
- else - else
= icon('user', 'aria-hidden': 'true') = icon('user', 'aria-hidden': 'true')
.title.hide-collapsed .title.hide-collapsed
...@@ -18,13 +23,13 @@ ...@@ -18,13 +23,13 @@
%a.gutter-toggle.float-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => _('Toggle sidebar') } %a.gutter-toggle.float-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => _('Toggle sidebar') }
= sidebar_gutter_toggle_icon = sidebar_gutter_toggle_icon
.value.hide-collapsed .value.hide-collapsed
- if issuable.assignee - if issuable_sidebar[:assignee]
= link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do = link_to_member(@project, assignee, size: 32, extra_class: 'bold') do
- if !issuable.can_be_merged_by?(issuable.assignee) - if issuable_sidebar[:assignee][:can_merge]
%span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: _('Not allowed to merge') } %span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: _('Not allowed to merge') }
= icon('exclamation-triangle', 'aria-hidden': 'true') = icon('exclamation-triangle', 'aria-hidden': 'true')
%span.username %span.username
= issuable.assignee.to_reference @#{issuable_sidebar[:assignee][:username]}
- else - else
%span.assign-yourself.no-value %span.assign-yourself.no-value
= _('No assignee') = _('No assignee')
...@@ -34,19 +39,33 @@ ...@@ -34,19 +39,33 @@
= _('assign yourself') = _('assign yourself')
.selectbox.hide-collapsed .selectbox.hide-collapsed
- issuable.assignees.each do |assignee| - if assignees.none?
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username } = hidden_field_tag "#{issuable_type}[assignee_ids][]", 0, id: nil
- else
- assignees.each do |assignee|
= hidden_field_tag "#{issuable_type}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }
- options = { toggle_class: 'js-user-search js-author-search', title: _('Assign to'), filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: _('Search users'), data: { first_user: current_user&.username, current_user: true, project_id: @project&.id, author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true, display: 'static' } } - options = { toggle_class: 'js-user-search js-author-search',
title: _('Assign to'),
filter: true,
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author',
placeholder: _('Search users'),
data: { first_user: issuable_sidebar.dig(:current_user, :username),
current_user: true,
project_id: issuable_sidebar[:project_id],
author_id: issuable_sidebar[:author_id],
field_name: "#{issuable_type}[assignee_ids][]",
issue_update: issuable_sidebar[:issuable_json_path],
ability_name: issuable_type,
null_user: true,
display: 'static' } }
- title = _('Select assignee') - title = _('Select assignee')
- if issuable.is_a?(Issue) - if issuable_type == "issue"
- unless issuable.assignees.any?
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil
- dropdown_options = issue_assignees_dropdown_options - dropdown_options = issue_assignees_dropdown_options
- title = dropdown_options[:title] - title = dropdown_options[:title]
- options[:toggle_class] += ' js-multiselect js-save-user-data' - options[:toggle_class] += ' js-multiselect js-save-user-data'
- data = { field_name: "#{issuable.to_ability_name}[assignee_ids][]" } - data = { field_name: "#{issuable_type}[assignee_ids][]" }
- data[:multi_select] = true - data[:multi_select] = true
- data['dropdown-title'] = title - data['dropdown-title'] = title
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header'] - data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
......
- is_collapsed = local_assigns.fetch(:is_collapsed, false) - is_collapsed = local_assigns.fetch(:is_collapsed, false)
- mark_content = is_collapsed ? sprite_icon('todo-done', css_class: 'todo-undone') : _('Mark todo as done') - has_todo = !!issuable_sidebar.dig(:current_user, :todo, :id)
- todo_content = is_collapsed ? sprite_icon('todo-add') : _('Add todo')
- todo_button_data = issuable_todo_button_data(issuable_sidebar, is_collapsed)
- button_title = has_todo ? todo_button_data[:mark_text] : todo_button_data[:todo_text]
- button_icon = has_todo ? todo_button_data[:mark_icon] : todo_button_data[:todo_icon]
%button.issuable-todo-btn.js-issuable-todo{ type: 'button', %button.issuable-todo-btn.js-issuable-todo{ type: 'button',
class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn float-right'), class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn float-right'),
title: (todo.nil? ? _('Add todo') : _('Mark todo as done')), title: button_title,
'aria-label' => (todo.nil? ? _('Add todo') : _('Mark todo as done')), 'aria-label' => button_title,
data: issuable_todo_button_data(issuable, todo, is_collapsed) } data: todo_button_data }
%span.issuable-todo-inner.js-issuable-todo-inner< %span.issuable-todo-inner.js-issuable-todo-inner<
- if todo = is_collapsed ? button_icon : button_title
= mark_content
- else
= todo_content
= icon('spin spinner', 'aria-hidden': 'true') = icon('spin spinner', 'aria-hidden': 'true')
...@@ -19,10 +19,9 @@ ...@@ -19,10 +19,9 @@
.issuable-form-select-holder .issuable-form-select-holder
= render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "qa-issuable-milestone-dropdown js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "qa-issuable-milestone-dropdown js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
.form-group.row .form-group.row
- has_labels = @labels && @labels.any?
= form.label :label_ids, "Labels", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}" = form.label :label_ids, "Labels", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
= form.hidden_field :label_ids, multiple: true, value: '' = form.hidden_field :label_ids, multiple: true, value: ''
.col-sm-10{ class: "#{"col-md-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } .col-sm-10{ class: "#{"col-md-8" if has_due_date}" }
.issuable-form-select-holder .issuable-form-select-holder
= render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label" = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label"
......
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
%span.bold= milestone.due_date.to_s(:medium) %span.bold= milestone.due_date.to_s(:medium)
- else - else
%span.no-value No due date %span.no-value No due date
- remaining_days = remaining_days_in_words(milestone) - remaining_days = remaining_days_in_words(milestone.due_date, milestone.start_date)
- if remaining_days.present? - if remaining_days.present?
= surround '(', ')' do = surround '(', ')' do
%span.remaining-days= remaining_days %span.remaining-days= remaining_days
......
---
title: Refactor issuable sidebar to use serializer
merge_request: 23379
author:
type: other
...@@ -30,19 +30,6 @@ describe Projects::MergeRequestsController do ...@@ -30,19 +30,6 @@ describe Projects::MergeRequestsController do
end end
end end
shared_examples "loads labels" do |action|
it "loads labels into the @labels variable" do
get action,
params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.iid
},
format: 'html'
expect(assigns(:labels)).not_to be_nil
end
end
describe "GET show" do describe "GET show" do
def go(extra_params = {}) def go(extra_params = {})
params = { params = {
...@@ -54,8 +41,6 @@ describe Projects::MergeRequestsController do ...@@ -54,8 +41,6 @@ describe Projects::MergeRequestsController do
get :show, params: params.merge(extra_params) get :show, params: params.merge(extra_params)
end end
it_behaves_like "loads labels", :show
describe 'as html' do describe 'as html' do
context 'when diff files were cleaned' do context 'when diff files were cleaned' do
render_views render_views
......
{
"type": ["object", "null"],
"properties" : {
"id": { "type": "integer" },
"delete_path": { "type": "string" }
},
"additionalProperties": false
}
...@@ -2,20 +2,46 @@ ...@@ -2,20 +2,46 @@
"type": "object", "type": "object",
"properties" : { "properties" : {
"id": { "type": "integer" }, "id": { "type": "integer" },
"iid": { "type": "integer" }, "type": { "type": "string" },
"subscribed": { "type": "boolean" }, "author_id": { "type": "integer" },
"time_estimate": { "type": "integer" }, "project_id": { "type": "integer" },
"total_time_spent": { "type": "integer" }, "discussion_locked": { "type": ["boolean", "null"] },
"human_time_estimate": { "type": ["integer", "null"] }, "due_date": { "type": "date" },
"human_total_time_spent": { "type": ["integer", "null"] }, "confidential": { "type": "boolean" },
"participants": { "reference": { "type": "string" },
"type": "array", "current_user": {
"items": { "$ref": "../public_api/v4/user/basic.json" } "allOf": [
{ "$ref": "../public_api/v4/user/basic.json" },
{ "type": "object",
"properties" : {
"todo": { "$ref": "issuable_sidebar_todo.json" },
"can_edit": { "type": "boolean" },
"can_move": { "type": "boolean" },
"can_admin_label": { "type": "boolean" }
}
}
]
},
"milestone": {
"oneOf": [
{ "type": "null" },
{ "$ref": "../public_api/v4/milestone.json" }
]
}, },
"assignees": { "labels": {
"type": "array", "type": "array",
"items": { "$ref": "../public_api/v4/user/basic.json" } "items": { "$ref": "label.json" }
} },
}, "issuable_json_path": { "type": "string" },
"additionalProperties": false "namespace_path": { "type": "string" },
"project_path": { "type": "string" },
"project_full_path": { "type": "string" },
"project_issuables_path": { "type": "string" },
"create_todo_path": { "type": "string" },
"project_milestones_path": { "type": "string" },
"project_labels_path": { "type": "string" },
"toggle_subscription_path": { "type": "string" },
"move_issue_path": { "type": "string" },
"projects_autocomplete_path": { "type": "string" }
}
} }
{
"type": "object",
"properties" : {
"subscribed": { "type": "boolean" },
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["integer", "null"] },
"human_total_time_spent": { "type": ["integer", "null"] },
"participants": {
"type": "array",
"items": { "$ref": "../public_api/v4/user/basic.json" }
},
"assignees": {
"type": "array",
"items": { "$ref": "../public_api/v4/user/basic.json" }
}
}
}
...@@ -4,15 +4,9 @@ ...@@ -4,15 +4,9 @@
"state": { "type": "string" }, "state": { "type": "string" },
"merge_status": { "type": "string" }, "merge_status": { "type": "string" },
"source_branch_exists": { "type": "boolean" }, "source_branch_exists": { "type": "boolean" },
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["string", "null"] },
"human_total_time_spent": { "type": ["string", "null"] },
"merge_error": { "type": ["string", "null"] }, "merge_error": { "type": ["string", "null"] },
"rebase_in_progress": { "type": "boolean" }, "rebase_in_progress": { "type": "boolean" },
"assignee_id": { "type": ["integer", "null"] }, "assignee_id": { "type": ["integer", "null"] },
"subscribed": { "type": ["boolean", "null"] },
"participants": { "type": "array" },
"allow_collaboration": { "type": "boolean"}, "allow_collaboration": { "type": "boolean"},
"allow_maintainer_to_push": { "type": "boolean"}, "allow_maintainer_to_push": { "type": "boolean"},
"assignee": { "assignee": {
......
{
"type": "object",
"properties" : {
"id": { "type": "integer" },
"type": { "type": "string" },
"author_id": { "type": "integer" },
"project_id": { "type": "integer" },
"discussion_locked": { "type": ["boolean", "null"] },
"reference": { "type": "string" },
"current_user": {
"allOf": [
{ "$ref": "../public_api/v4/user/basic.json" },
{ "type": "object",
"properties" : {
"todo": { "$ref": "issuable_sidebar_todo.json" },
"can_edit": { "type": "boolean" },
"can_move": { "type": "boolean" },
"can_admin_label": { "type": "boolean" }
}
}
]
},
"milestone": {
"oneOf": [
{ "type": "null" },
{ "$ref": "../public_api/v4/milestones.json" }
]
},
"labels": {
"type": "array",
"items": { "$ref": "label.json" }
},
"assignee": {
"allOf": [
{ "$ref": "../public_api/v4/user/basic.json" },
{ "type": "object",
"properties" : {
"can_merge": { "type": "boolean" }
}
}
]
},
"issuable_json_path": { "type": "string" },
"namespace_path": { "type": "string" },
"project_path": { "type": "string" },
"project_full_path": { "type": "string" },
"project_issuables_path": { "type": "string" },
"create_todo_path": { "type": "string" },
"project_milestones_path": { "type": "string" },
"project_labels_path": { "type": "string" },
"toggle_subscription_path": { "type": "string" },
"move_issue_path": { "type": "string" },
"projects_autocomplete_path": { "type": "string" }
},
"additionalProperties": false
}
{
"type": "object",
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"subscribed": { "type": "boolean" },
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["integer", "null"] },
"human_total_time_spent": { "type": ["integer", "null"] },
"participants": {
"type": "array",
"items": { "$ref": "../public_api/v4/user/basic.json" }
},
"assignees": {
"type": "array",
"items": { "$ref": "../public_api/v4/user/basic.json" }
}
},
"additionalProperties": false
}
{
"type": "object",
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": ["integer", "null"] },
"group_id": { "type": ["integer", "null"] },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"start_date": { "type": "date" },
"due_date": { "type": "date" },
"web_url": { "type": "string" }
},
"required": [
"id", "iid", "title", "description", "state",
"state", "created_at", "updated_at", "start_date", "due_date"
],
"additionalProperties": false
}
{ {
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "$ref": "./milestone.json"
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": ["integer", "null"] },
"group_id": { "type": ["integer", "null"] },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"start_date": { "type": "date" },
"due_date": { "type": "date" },
"web_url": { "type": "string" }
},
"required": [
"id", "iid", "title", "description", "state",
"state", "created_at", "updated_at", "start_date", "due_date"
],
"additionalProperties": false
} }
} }
...@@ -43,16 +43,19 @@ describe IssuablesHelper do ...@@ -43,16 +43,19 @@ describe IssuablesHelper do
end end
describe '#issuable_labels_tooltip' do describe '#issuable_labels_tooltip' do
let(:label_entity) { LabelEntity.represent(label).as_json }
let(:label2_entity) { LabelEntity.represent(label2).as_json }
it 'returns label text with no labels' do it 'returns label text with no labels' do
expect(issuable_labels_tooltip([])).to eq("Labels") expect(issuable_labels_tooltip([])).to eq("Labels")
end end
it 'returns label text with labels within max limit' do it 'returns label text with labels within max limit' do
expect(issuable_labels_tooltip([label])).to eq(label.title) expect(issuable_labels_tooltip([label_entity])).to eq(label[:title])
end end
it 'returns label text with labels exceeding max limit' do it 'returns label text with labels exceeding max limit' do
expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more") expect(issuable_labels_tooltip([label_entity, label2_entity], limit: 1)).to eq("#{label[:title]}, and 1 more")
end end
end end
...@@ -197,33 +200,4 @@ describe IssuablesHelper do ...@@ -197,33 +200,4 @@ describe IssuablesHelper do
expect(helper.issuable_initial_data(issue)).to eq(expected_data) expect(helper.issuable_initial_data(issue)).to eq(expected_data)
end end
end end
describe '#selected_labels' do
context 'if label_name param is a string' do
it 'returns a new label with title' do
allow(helper).to receive(:params)
.and_return(ActionController::Parameters.new(label_name: 'test label'))
labels = helper.selected_labels
expect(labels).to be_an(Array)
expect(labels.size).to eq(1)
expect(labels.first.title).to eq('test label')
end
end
context 'if label_name param is an array' do
it 'returns a new label with title for each element' do
allow(helper).to receive(:params)
.and_return(ActionController::Parameters.new(label_name: ['test label 1', 'test label 2']))
labels = helper.selected_labels
expect(labels).to be_an(Array)
expect(labels.size).to eq(2)
expect(labels.first.title).to eq('test label 1')
expect(labels.second.title).to eq('test label 2')
end
end
end
end end
...@@ -66,7 +66,7 @@ const RESPONSE_MAP = { ...@@ -66,7 +66,7 @@ const RESPONSE_MAP = {
}, },
labels: [], labels: [],
}, },
'/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar': { '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras': {
assignees: [ assignees: [
{ {
name: 'User 0', name: 'User 0',
...@@ -181,7 +181,7 @@ const RESPONSE_MAP = { ...@@ -181,7 +181,7 @@ const RESPONSE_MAP = {
const mockData = { const mockData = {
responseMap: RESPONSE_MAP, responseMap: RESPONSE_MAP,
mediator: { mediator: {
endpoint: '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar', endpoint: '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras',
toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription', toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription',
moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move', moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15', projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
......
...@@ -37,7 +37,7 @@ describe('Sidebar mediator', function() { ...@@ -37,7 +37,7 @@ describe('Sidebar mediator', function() {
it('fetches the data', done => { it('fetches the data', done => {
const mockData = const mockData =
Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar']; Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras'];
spyOn(this.mediator, 'processFetchedData').and.callThrough(); spyOn(this.mediator, 'processFetchedData').and.callThrough();
this.mediator this.mediator
...@@ -51,7 +51,7 @@ describe('Sidebar mediator', function() { ...@@ -51,7 +51,7 @@ describe('Sidebar mediator', function() {
it('processes fetched data', () => { it('processes fetched data', () => {
const mockData = const mockData =
Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar']; Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras'];
this.mediator.processFetchedData(mockData); this.mediator.processFetchedData(mockData);
expect(this.mediator.store.assignees).toEqual(mockData.assignees); expect(this.mediator.store.assignees).toEqual(mockData.assignees);
......
...@@ -50,7 +50,7 @@ describe EntityDateHelper do ...@@ -50,7 +50,7 @@ describe EntityDateHelper do
end end
context 'when less than 31 days remaining' do context 'when less than 31 days remaining' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) } let(:milestone_remaining) { date_helper_class.remaining_days_in_words(12.days.from_now.utc.to_date) }
it 'returns days remaining' do it 'returns days remaining' do
expect(milestone_remaining).to eq("<strong>12</strong> days remaining") expect(milestone_remaining).to eq("<strong>12</strong> days remaining")
...@@ -58,7 +58,7 @@ describe EntityDateHelper do ...@@ -58,7 +58,7 @@ describe EntityDateHelper do
end end
context 'when less than 1 year and more than 30 days remaining' do context 'when less than 1 year and more than 30 days remaining' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) } let(:milestone_remaining) { date_helper_class.remaining_days_in_words(2.months.from_now.utc.to_date) }
it 'returns months remaining' do it 'returns months remaining' do
expect(milestone_remaining).to eq("<strong>2</strong> months remaining") expect(milestone_remaining).to eq("<strong>2</strong> months remaining")
...@@ -66,7 +66,7 @@ describe EntityDateHelper do ...@@ -66,7 +66,7 @@ describe EntityDateHelper do
end end
context 'when more than 1 year remaining' do context 'when more than 1 year remaining' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) } let(:milestone_remaining) { date_helper_class.remaining_days_in_words((1.year.from_now + 2.days).utc.to_date) }
it 'returns years remaining' do it 'returns years remaining' do
expect(milestone_remaining).to eq("<strong>1</strong> year remaining") expect(milestone_remaining).to eq("<strong>1</strong> year remaining")
...@@ -74,7 +74,7 @@ describe EntityDateHelper do ...@@ -74,7 +74,7 @@ describe EntityDateHelper do
end end
context 'when milestone is expired' do context 'when milestone is expired' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 2.days.ago.utc)) } let(:milestone_remaining) { date_helper_class.remaining_days_in_words(2.days.ago.utc.to_date) }
it 'returns "Past due"' do it 'returns "Past due"' do
expect(milestone_remaining).to eq("<strong>Past due</strong>") expect(milestone_remaining).to eq("<strong>Past due</strong>")
...@@ -82,7 +82,7 @@ describe EntityDateHelper do ...@@ -82,7 +82,7 @@ describe EntityDateHelper do
end end
context 'when milestone has start_date in the future' do context 'when milestone has start_date in the future' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) } let(:milestone_remaining) { date_helper_class.remaining_days_in_words(nil, 2.days.from_now.utc.to_date) }
it 'returns "Upcoming"' do it 'returns "Upcoming"' do
expect(milestone_remaining).to eq("<strong>Upcoming</strong>") expect(milestone_remaining).to eq("<strong>Upcoming</strong>")
...@@ -90,7 +90,7 @@ describe EntityDateHelper do ...@@ -90,7 +90,7 @@ describe EntityDateHelper do
end end
context 'when milestone has start_date in the past' do context 'when milestone has start_date in the past' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, start_date: 2.days.ago.utc)) } let(:milestone_remaining) { date_helper_class.remaining_days_in_words(nil, 2.days.ago.utc.to_date) }
it 'returns days elapsed' do it 'returns days elapsed' do
expect(milestone_remaining).to eq("<strong>2</strong> days elapsed") expect(milestone_remaining).to eq("<strong>2</strong> days elapsed")
......
...@@ -20,11 +20,19 @@ describe IssueSerializer do ...@@ -20,11 +20,19 @@ describe IssueSerializer do
context 'sidebar issue serialization' do context 'sidebar issue serialization' do
let(:serializer) { 'sidebar' } let(:serializer) { 'sidebar' }
it 'matches sidebar issue json schema' do it 'matches issue_sidebar json schema' do
expect(json_entity).to match_schema('entities/issue_sidebar') expect(json_entity).to match_schema('entities/issue_sidebar')
end end
end end
context 'sidebar extras issue serialization' do
let(:serializer) { 'sidebar_extras' }
it 'matches issue_sidebar_extras json schema' do
expect(json_entity).to match_schema('entities/issue_sidebar_extras')
end
end
context 'board issue serialization' do context 'board issue serialization' do
let(:serializer) { 'board' } let(:serializer) { 'board' }
......
require 'spec_helper'
describe MergeRequestBasicSerializer do
let(:resource) { create(:merge_request) }
let(:user) { create(:user) }
let(:json_entity) do
described_class.new(current_user: user)
.represent(resource, serializer: 'basic')
.with_indifferent_access
end
it 'matches basic merge request json' do
expect(json_entity).to match_schema('entities/merge_request_basic')
end
end
...@@ -20,8 +20,16 @@ describe MergeRequestSerializer do ...@@ -20,8 +20,16 @@ describe MergeRequestSerializer do
context 'sidebar merge request serialization' do context 'sidebar merge request serialization' do
let(:serializer) { 'sidebar' } let(:serializer) { 'sidebar' }
it 'matches basic merge request json schema' do it 'matches merge_request_sidebar json schema' do
expect(json_entity).to match_schema('entities/merge_request_basic') expect(json_entity).to match_schema('entities/merge_request_sidebar')
end
end
context 'sidebar_extras merge request serialization' do
let(:serializer) { 'sidebar_extras' }
it 'matches merge_request_sidebar_extras json schema' do
expect(json_entity).to match_schema('entities/merge_request_sidebar_extras')
end end
end end
......
...@@ -32,6 +32,11 @@ describe 'projects/merge_requests/show.html.haml' do ...@@ -32,6 +32,11 @@ describe 'projects/merge_requests/show.html.haml' do
assign(:noteable, closed_merge_request) assign(:noteable, closed_merge_request)
assign(:notes, []) assign(:notes, [])
assign(:pipelines, Ci::Pipeline.none) assign(:pipelines, Ci::Pipeline.none)
assign(
:issuable_sidebar,
MergeRequestSerializer.new(current_user: user, project: project)
.represent(closed_merge_request, serializer: 'sidebar')
)
preload_view_requirements preload_view_requirements
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment