Commit 9ce83d06 authored by Mike Greiling's avatar Mike Greiling

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

EE port of 44984-use-serializer-for-issuable-sidebar

See merge request gitlab-org/gitlab-ee!8651
parents 12665637 464fa0a5
......@@ -79,11 +79,12 @@ Sidebar.prototype.sidebarToggleClicked = function(e, triggered) {
Sidebar.prototype.toggleTodo = function(e) {
var $btnText, $this, $todoLoading, ajaxType, url;
$this = $(e.currentTarget);
ajaxType = $this.attr('data-delete-path') ? 'delete' : 'post';
if ($this.attr('data-delete-path')) {
url = '' + $this.attr('data-delete-path');
ajaxType = $this.data('deletePath') ? 'delete' : 'post';
if ($this.data('deletePath')) {
url = '' + $this.data('deletePath');
} else {
url = '' + $this.data('url');
url = '' + $this.data('createPath');
}
$this.tooltip('hide');
......@@ -119,14 +120,14 @@ Sidebar.prototype.todoUpdateDone = function(data) {
.removeClass('is-loading')
.enable()
.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')) {
$el.tooltip('_fixTitle');
}
if ($el.data(`${attrPrefix}Icon`)) {
if (typeof $el.data('isCollapsed') !== 'undefined') {
$elText.html($el.data(`${attrPrefix}Icon`));
} else {
$elText.text($el.data(`${attrPrefix}Text`));
......
......@@ -71,7 +71,7 @@ export default class SidebarStore {
}
findAssignee(findAssignee) {
return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0];
return this.assignees.find(assignee => assignee.id === findAssignee.id);
}
removeAssignee(removeAssignee) {
......
......@@ -5,7 +5,6 @@ module IssuableActions
include Gitlab::Utils::StrongMemoize
included do
before_action :labels, only: [:show, :new, :edit]
before_action :authorize_destroy_issuable!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update
end
......@@ -25,7 +24,10 @@ module IssuableActions
def show
respond_to do |format|
format.html
format.html do
@issuable_sidebar = serializer.represent(issuable, serializer: 'sidebar') # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
format.json do
render json: serializer.represent(issuable, serializer: params[:serializer])
end
......@@ -168,10 +170,6 @@ module IssuableActions
end
end
def labels
@labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def authorize_destroy_issuable!
unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable)
return access_denied!
......
......@@ -8,7 +8,7 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap
def show
respond_to do |format|
format.html do
labels
@issuable_sidebar = serializer.represent(@merge_request, serializer: 'sidebar')
end
format.json do
......@@ -60,9 +60,15 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap
end
end
private
def authorize_can_resolve_conflicts!
@conflicts_list = ::MergeRequests::Conflicts::ListService.new(@merge_request)
return render_404 unless @conflicts_list.can_be_resolved_by?(current_user)
end
def serializer
MergeRequestSerializer.new(current_user: current_user, project: project)
end
end
......@@ -24,8 +24,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
format.html
format.json do
render json: {
html: view_to_html_string("projects/merge_requests/_merge_requests"),
labels: @labels.as_json(methods: :text_color)
html: view_to_html_string("projects/merge_requests/_merge_requests")
}
end
end
......@@ -45,8 +44,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@noteable = @merge_request
@commits_count = @merge_request.commits_count
labels
@issuable_sidebar = serializer.represent(@merge_request, serializer: 'sidebar')
set_pipeline_variables
......
......@@ -23,31 +23,42 @@ module IssuablesHelper
end
end
def sidebar_due_date_tooltip_label(issuable)
if issuable.due_date
"#{_('Due date')}<br />#{due_date_remaining_days(issuable)}"
else
_('Due date')
def sidebar_milestone_tooltip_label(milestone)
return _('Milestone') unless milestone.present?
[milestone[:title], sidebar_milestone_remaining_days(milestone) || _('Milestone')].join('<br/>')
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
def due_date_with_remaining_days(due_date, start_date = nil)
return unless due_date
"#{due_date.to_s(:medium)} (#{remaining_days_in_words(due_date, start_date)})"
end
def due_date_remaining_days(issuable)
remaining_days_in_words = remaining_days_in_words(issuable)
def sidebar_label_filter_path(base_path, label_name)
query_params = { label_name: [label_name] }.to_query
"#{issuable.due_date.to_s(:medium)} (#{remaining_days_in_words})"
"#{base_path}?#{query_params}"
end
def multi_label_name(current_labels, default_label)
if current_labels && current_labels.any?
title = current_labels.first.try(:title)
return default_label if current_labels.blank?
title = current_labels.first.try(:title) || current_labels.first[:title]
if current_labels.size > 1
"#{title} +#{current_labels.size - 1} more"
else
title
end
else
default_label
end
end
def issuable_json_path(issuable)
......@@ -197,19 +208,11 @@ module IssuablesHelper
output.join.html_safe
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)
first, last = labels.partition.with_index { |_, i| i < limit }
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.join(', ')
......@@ -356,12 +359,6 @@ module IssuablesHelper
issuable.model_name.human.downcase
end
def selected_labels
Array(params[:label_name]).map do |label_name|
Label.new(title: label_name)
end
end
def has_filter_bar_param?
finder.class.scalar_params.any? { |p| params[p].present? }
end
......@@ -386,19 +383,20 @@ module IssuablesHelper
params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] }
end
def issuable_todo_button_data(issuable, todo, is_collapsed)
def issuable_todo_button_data(issuable, is_collapsed)
{
todo_text: "Add todo",
mark_text: "Mark todo as done",
todo_icon: (is_collapsed ? sprite_icon('todo-add') : nil),
mark_icon: (is_collapsed ? sprite_icon('todo-done', css_class: 'todo-undone') : nil),
issuable_id: issuable.id,
issuable_type: issuable.class.name.underscore,
url: project_todos_path(@project),
delete_path: (dashboard_todo_path(todo) if todo),
placement: (is_collapsed ? 'left' : nil),
container: (is_collapsed ? 'body' : nil),
boundary: 'viewport'
todo_text: _('Add todo'),
mark_text: _('Mark todo as done'),
todo_icon: sprite_icon('todo-add'),
mark_icon: sprite_icon('todo-done', css_class: 'todo-undone'),
issuable_id: issuable[:id],
issuable_type: issuable[:type],
create_path: issuable[:create_todo_path],
delete_path: issuable.dig(:current_user, :todo, :delete_path),
placement: is_collapsed ? 'left' : nil,
container: is_collapsed ? 'body' : nil,
boundary: 'viewport',
is_collapsed: is_collapsed
}
end
......@@ -418,29 +416,22 @@ module IssuablesHelper
end
end
def issuable_sidebar_options(issuable, can_edit_issuable)
def issuable_sidebar_options(issuable)
{
endpoint: "#{issuable_json_path(issuable)}?serializer=sidebar",
toggleSubscriptionEndpoint: toggle_subscription_path(issuable),
moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable),
projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id),
editable: can_edit_issuable,
currentUser: UserSerializer.new.represent(current_user),
endpoint: "#{issuable[:issuable_json_path]}?serializer=sidebar_extras",
toggleSubscriptionEndpoint: issuable[:toggle_subscription_path],
moveIssueEndpoint: issuable[:move_issue_path],
projectsAutocompleteEndpoint: issuable[:projects_autocomplete_path],
editable: issuable.dig(:current_user, :can_edit),
currentUser: issuable[:current_user],
rootPath: root_path,
fullPath: @project.full_path
fullPath: issuable[:project_full_path]
}
end
def parent
@project || @group
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
IssuablesHelper.prepend(EE::IssuablesHelper)
......@@ -114,12 +114,6 @@ module MilestonesHelper
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)
title = date_type == :start ? "Start date" : "End date"
......@@ -173,7 +167,7 @@ module MilestonesHelper
def milestone_tooltip_due_date(milestone)
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
_('Milestone')
end
......
......@@ -1422,6 +1422,10 @@ class User < ActiveRecord::Base
todos.where(id: ids)
end
def pending_todo_for(target)
todos.find_by(target: target, state: :pending)
end
# @deprecated
alias_method :owned_or_masters_groups, :owned_or_maintainers_groups
......
......@@ -44,14 +44,14 @@ module EntityDateHelper
# It returns "Upcoming" for upcoming entities
# 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"
def remaining_days_in_words(entity)
if entity.try(:expired?)
def remaining_days_in_words(due_date, start_date = nil)
if due_date&.past?
content_tag(:strong, 'Past due')
elsif entity.try(:upcoming?)
elsif start_date&.future?
content_tag(:strong, 'Upcoming')
elsif entity.due_date
is_upcoming = (entity.due_date - Date.today).to_i > 0
time_ago = time_ago_in_words(entity.due_date)
elsif due_date
is_upcoming = (due_date - Date.today).to_i > 0
time_ago = time_ago_in_words(due_date)
# https://gitlab.com/gitlab-org/gitlab-ce/issues/49440
#
......@@ -63,8 +63,8 @@ module EntityDateHelper
remaining_or_ago = is_upcoming ? _("remaining") : _("ago")
"#{content} #{remaining_or_ago}".html_safe
elsif entity.start_date && entity.start_date.past?
days = entity.elapsed_days
elsif start_date&.past?
days = (Date.today - start_date).to_i
"#{content_tag(:strong, days)} #{'day'.pluralize(days)} elapsed".html_safe
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
class IssuableSidebarEntity < Grape::Entity
include TimeTrackableEntity
class IssuableSidebarExtrasEntity < Grape::Entity
include RequestAwareEntity
prepend ::EE::IssuableSidebarEntity # rubocop: disable Cop/InjectEnterpriseEditionModule
include TimeTrackableEntity
expose :participants, using: ::API::Entities::UserBasic do |issuable|
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
......@@ -38,7 +38,7 @@ class IssueBoardEntity < Grape::Entity
end
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
expose :toggle_subscription_endpoint, if: -> (issue) { issue.project } do |issue|
......
......@@ -2,13 +2,15 @@
class IssueSerializer < BaseSerializer
# 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.
def represent(issue, opts = {})
entity =
case opts[:serializer]
when 'sidebar'
IssueSidebarEntity
IssueSidebarBasicEntity
when 'sidebar_extras'
IssueSidebarExtrasEntity
when 'board'
IssueBoardEntity
else
......
# frozen_string_literal: true
class IssueSidebarBasicEntity < IssuableSidebarBasicEntity
expose :due_date
expose :confidential
end
IssueSidebarBasicEntity.prepend(EE::IssueSidebarBasicEntity)
# frozen_string_literal: true
class IssueSidebarEntity < IssuableSidebarEntity
prepend ::EE::IssueSidebarEntity # rubocop: disable Cop/InjectEnterpriseEditionModule
class IssueSidebarExtrasEntity < IssuableSidebarExtrasEntity
expose :assignees, using: API::Entities::UserBasic
end
IssueSidebarExtrasEntity.prepend(EE::IssueSidebarExtrasEntity)
# frozen_string_literal: true
class MergeRequestBasicEntity < IssuableSidebarEntity
class MergeRequestBasicEntity < Grape::Entity
expose :assignee_id
expose :merge_status
expose :merge_error
......
# frozen_string_literal: true
class MergeRequestBasicSerializer < BaseSerializer
entity MergeRequestBasicEntity
end
......@@ -7,9 +7,14 @@ class MergeRequestSerializer < BaseSerializer
def represent(merge_request, opts = {})
entity =
case opts[:serializer]
when 'basic', 'sidebar'
when 'sidebar'
MergeRequestSidebarBasicEntity
when 'sidebar_extras'
IssuableSidebarExtrasEntity
when 'basic'
MergeRequestBasicEntity
else # It's 'widget'
else
# fallback to widget for old poll requests without `serializer` set
MergeRequestWidgetEntity
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
......@@ -98,4 +98,4 @@
%section.issuable-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 @@
.merge-request-details.issuable-details
= 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),
resolve_conflicts_path: resolve_conflicts_project_merge_request_path(@merge_request.project, @merge_request) } }
......
......@@ -86,7 +86,8 @@
.mr-loading-status
= 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)
= render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title
- 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-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } }
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- issuable_type = issuable_sidebar[:type]
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
- 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
- if current_user
- if signed_in
%span.issuable-header-text.hide-collapsed.float-left
= _('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' } }
= sidebar_gutter_toggle_icon
- if current_user
= render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable
- if signed_in
= 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|
- if current_user
= 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 signed_in
.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
= 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
.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')
%span.milestone-title.collapse-truncated-title
- if issuable.milestone
= issuable.milestone.title
- if milestone.present?
= milestone[:title]
- else
= _('None')
.title.hide-collapsed
......@@ -35,49 +40,50 @@
- if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed
- if issuable.milestone
= 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' }
- if milestone.present?
= 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
%span.no-value
= _('None')
.selectbox.hide-collapsed
= f.hidden_field 'milestone_id', value: issuable.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' }})
- if issuable.has_attribute?(:time_estimate)
= 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_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' }})
#issuable-time-tracker.block
// Fallback while content is loading
.title.hide-collapsed
= _('Time tracking')
= icon('spinner spin', 'aria-hidden': 'true')
- if issuable.has_attribute?(:due_date)
- if issuable_sidebar.has_key?(: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')
%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
= _('Due date')
= 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'
.value.hide-collapsed
%span.value-content
- if issuable.due_date
%span.bold= issuable.due_date.to_s(:medium)
- if issuable_sidebar[:due_date]
%span.bold= issuable_sidebar[:due_date].to_s(:medium)
- else
%span.no-value
= _('No due date')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
%span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable.due_date.nil?) }
- if can_edit_issuable
%span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable_sidebar[:due_date].nil?) }
\-
%a.js-remove-due-date{ href: "#", role: "button" }
= _('remove due date')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- if can_edit_issuable
.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
%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
= _('Due date')
= icon('chevron-down', 'aria-hidden': 'true')
......@@ -86,10 +92,9 @@
= dropdown_content do
.js-due-date-calendar
- if @labels
- selected_labels = issuable.labels
- selected_labels = issuable_sidebar[:labels]
.block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body", boundary: 'viewport' } }
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } }
= icon('tags', 'aria-hidden': 'true')
%span
= selected_labels.size
......@@ -101,41 +106,42 @@
.value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" 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)
= link_to sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label[:title]) do
%span.badge.color-label.has-tooltip{ style: "background-color: #{label[:color]}; color: #{label[:text_color]}", title: label[:description], data: { container: "body" } }
= label[:title]
- else
%span.no-value
= _('None')
.selectbox.hide-collapsed
- selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
= hidden_field_tag "#{issuable_type}[label_names][]", label[:id], id: nil
.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.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' } }
%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' } }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= 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 can? current_user, :admin_label, @project and @project
- if issuable_sidebar.dig(:current_user, :can_admin_label)
= render partial: "shared/issuable/label_page_create"
= render_if_exists 'shared/issuable/sidebar_weight', issuable: issuable
= render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar
- if issuable.has_attribute?(:confidential)
- if issuable_sidebar.has_key?(:confidential)
-# 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
- if issuable.has_attribute?(:discussion_locked)
-# haml-lint:disable InlineJavaScript
%script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe
%script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe
#js-lock-entry-point
.js-sidebar-participants-entry-point
- if current_user
- if signed_in
.js-sidebar-subscriptions-entry-point
- project_ref = cross_project_reference(@project, issuable)
- project_ref = issuable_sidebar[:reference]
.block.project-reference
.sidebar-collapsed-icon.dont-change-state
= clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left", boundary: 'viewport')
......@@ -145,7 +151,8 @@
%cite{ title: project_ref }
= project_ref
= 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
.sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') }
= custom_icon('icon_arrow_right')
......@@ -164,4 +171,4 @@
= icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
-# 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)
#js-vue-sidebar-assignees{ data: { field: "#{issuable.to_ability_name}[assignee_ids]", signed_in: signed_in } }
- issuable_type = issuable_sidebar[:type]
- 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
= _('Assignee')
= icon('spinner spin')
- else
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body", boundary: 'viewport' }, title: sidebar_assignee_tooltip_label(issuable) }
- if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 24)
- assignee = assignees.first
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body", boundary: 'viewport' }, title: (issuable_sidebar.dig(:assignee, :name) || _('Assignee')) }
- if issuable_sidebar[:assignee]
= link_to_member(@project, assignee, size: 24)
- else
= icon('user', 'aria-hidden': 'true')
.title.hide-collapsed
......@@ -18,13 +23,13 @@
%a.gutter-toggle.float-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => _('Toggle sidebar') }
= sidebar_gutter_toggle_icon
.value.hide-collapsed
- if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do
- if !issuable.can_be_merged_by?(issuable.assignee)
- if issuable_sidebar[:assignee]
= link_to_member(@project, assignee, size: 32, extra_class: 'bold') do
- if issuable_sidebar[:assignee][:can_merge]
%span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: _('Not allowed to merge') }
= icon('exclamation-triangle', 'aria-hidden': 'true')
%span.username
= issuable.assignee.to_reference
@#{issuable_sidebar[:assignee][:username]}
- else
%span.assign-yourself.no-value
= _('No assignee')
......@@ -34,19 +39,33 @@
= _('assign yourself')
.selectbox.hide-collapsed
- issuable.assignees.each do |assignee|
= 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 }
- if assignees.none?
= 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')
- if issuable.is_a?(Issue)
- unless issuable.assignees.any?
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil
- if issuable_type == "issue"
- dropdown_options = issue_assignees_dropdown_options
- title = dropdown_options[:title]
- 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['dropdown-title'] = title
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
......
- is_collapsed = local_assigns.fetch(:is_collapsed, false)
- mark_content = is_collapsed ? sprite_icon('todo-done', css_class: 'todo-undone') : _('Mark todo as done')
- todo_content = is_collapsed ? sprite_icon('todo-add') : _('Add todo')
- has_todo = !!issuable_sidebar.dig(:current_user, :todo, :id)
- 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',
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')),
'aria-label' => (todo.nil? ? _('Add todo') : _('Mark todo as done')),
data: issuable_todo_button_data(issuable, todo, is_collapsed) }
title: button_title,
'aria-label' => button_title,
data: todo_button_data }
%span.issuable-todo-inner.js-issuable-todo-inner<
- if todo
= mark_content
- else
= todo_content
= is_collapsed ? button_icon : button_title
= icon('spin spinner', 'aria-hidden': 'true')
......@@ -19,10 +19,9 @@
.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"
.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.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
= 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 @@
%span.bold= milestone.due_date.to_s(:medium)
- else
%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?
= surround '(', ')' do
%span.remaining-days= remaining_days
......
---
title: Refactor issuable sidebar to use serializer
merge_request: 23379
author:
type: other
......@@ -18,8 +18,6 @@ class Groups::EpicsController < Groups::ApplicationController
before_action :authorize_update_issuable!, only: :update
before_action :authorize_create_epic!, only: [:create]
skip_before_action :labels
def index
@epics = @issuables
......
......@@ -5,7 +5,7 @@ module EE
extend ::Gitlab::Utils::Override
override :issuable_sidebar_options
def issuable_sidebar_options(issuable, can_edit_issuable)
def issuable_sidebar_options(sidebar_data)
super.merge(
weightOptions: ::Issue.weight_options,
weightNoneValue: ::Issue::WEIGHT_NONE
......
......@@ -3,7 +3,7 @@
module EpicsHelper
def epic_show_app_data(epic, opts)
group = epic.group
todo = issuable_todo(epic)
todo = epic_pending_todo(epic)
epic_meta = {
epic_id: epic.id,
......@@ -59,6 +59,10 @@ module EpicsHelper
}
end
def epic_pending_todo(epic)
current_user.pending_todo_for(epic) if current_user
end
def epic_author(epic, opts)
{
name: epic.author.name,
......
# frozen_string_literal: true
module EE
module IssuableSidebarEntity
extend ActiveSupport::Concern
prepended do
expose :weight, if: ->(issuable, options) { issuable.supports_weight? }
end
end
end
# frozen_string_literal: true
module EE
module IssueSidebarBasicEntity
extend ActiveSupport::Concern
prepended do
expose :supports_epic?, as: :supports_epic
expose :features_available do
expose :issue_weights do |issuable|
issuable.project.feature_available?(:issue_weights)
end
expose :epics do |issuable|
issuable.project&.group&.feature_available?(:epics)
end
end
end
end
end
# frozen_string_literal: true
module EE
module IssueSidebarEntity
module IssueSidebarExtrasEntity
extend ActiveSupport::Concern
prepended do
expose :epic, using: EpicBaseEntity
expose :weight
end
end
end
- if issuable.supports_epic?
- if issuable.project.group.feature_available?(:epics)
- if issuable_sidebar[:supports_epic]
- if issuable_sidebar[:features_available][:epics]
.block.epic
#js-vue-sidebar-item-epic
.title.hide-collapsed
......
- if local_assigns.fetch(:issuable).supports_weight?
- if issuable_sidebar[:type] == "issue"
- if issuable_sidebar[:features_available][:issue_weights]
.js-sidebar-weight-entry-point
= render 'shared/promotions/promote_issue_weights'
- else
= render 'shared/promotions/promote_issue_weights'
- promotion_feature = 'promote_weight_sidebar_dismissed'
- if !@project.feature_available?(:issue_weights) && show_promotions? && show_callout?(promotion_feature)
- if show_promotions? && show_callout?(promotion_feature)
.block.js-weight-sidebar-callout.promotion-issue-sidebar{ data: { uid: promotion_feature } }
.sidebar-collapsed-icon{ data: { toggle: "dropdown", target: ".js-weight-sidebar-callout" } }
%span{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: _("Weight") }
......
{
"allOf": [
{ "$ref": "../../../../../../spec/fixtures/api/schemas/entities/issue_sidebar.json" },
{
"properties": {
"supports_epic": { "type": ["null", "boolean"] },
"features_available": {
"type": "object",
"properties": {
"issue_weights": { "type": "boolean" },
"epics": { "type": ["null", "boolean"] }
}
}
},
"required": [
"supports_epic", "features_available"
]
}
]
}
{
"allOf": [
{ "$ref": "../../../../../../spec/fixtures/api/schemas/entities/issue_sidebar_extras.json" },
{
"properties": {
"epic": {
"type": ["object", "null"],
"properties": {
"id": { "type": "integer" },
"title": { "type": "string" },
"url": { "type": "string" }
}
},
"weight": { "type": ["integer", "null"] }
},
"required": [
"epic",
"weight"
]
}
]
}
require 'spec_helper'
describe IssueSerializer do
let(:resource) { create(:issue) }
let(:user) { create(:user) }
let(:json_entity) do
described_class.new(current_user: user)
.represent(resource, serializer: serializer)
.with_indifferent_access
end
before do
create(:epic_issue, issue: resource)
end
context 'sidebar issue serialization' do
let(:serializer) { 'sidebar' }
it 'matches issue_sidebar json schema' do
expect(json_entity).to match_schema('entities/issue_sidebar', dir: 'ee')
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', dir: 'ee')
end
end
end
......@@ -30,19 +30,6 @@ describe Projects::MergeRequestsController do
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
def go(extra_params = {})
params = {
......@@ -54,8 +41,6 @@ describe Projects::MergeRequestsController do
get :show, params: params.merge(extra_params)
end
it_behaves_like "loads labels", :show
describe 'as html' do
context 'when diff files were cleaned' do
render_views
......
{
"type": ["object", "null"],
"properties" : {
"id": { "type": "integer" },
"delete_path": { "type": "string" }
},
"additionalProperties": false
}
{
"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" }
},
"epic": {
"type": ["object", "null"],
"properties": {
"id": { "type": "integer" },
"title": { "type": "string" },
"url": { "type": "string" }
"type": { "type": "string" },
"author_id": { "type": "integer" },
"project_id": { "type": "integer" },
"discussion_locked": { "type": ["boolean", "null"] },
"due_date": { "type": "date" },
"confidential": { "type": "boolean" },
"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/milestone.json" }
]
},
"weight": { "type": ["integer", "null"] }
"labels": {
"type": "array",
"items": { "$ref": "label.json" }
},
"additionalProperties": false
"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" }
}
}
{
"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 @@
"state": { "type": "string" },
"merge_status": { "type": "string" },
"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"] },
"rebase_in_progress": { "type": "boolean" },
"assignee_id": { "type": ["integer", "null"] },
"subscribed": { "type": ["boolean", "null"] },
"participants": { "type": "array" },
"allow_collaboration": { "type": "boolean"},
"allow_maintainer_to_push": { "type": "boolean"},
"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",
"items": {
"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
"$ref": "./milestone.json"
}
}
......@@ -43,16 +43,19 @@ describe IssuablesHelper do
end
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
expect(issuable_labels_tooltip([])).to eq("Labels")
end
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
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
......@@ -198,33 +201,4 @@ describe IssuablesHelper do
expect(helper.issuable_initial_data(issue)).to eq(expected_data)
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
......@@ -66,7 +66,7 @@ const RESPONSE_MAP = {
},
labels: [],
},
'/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar': {
'/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras': {
assignees: [
{
name: 'User 0',
......@@ -181,7 +181,7 @@ const RESPONSE_MAP = {
const mockData = {
responseMap: RESPONSE_MAP,
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',
moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
......
......@@ -37,7 +37,7 @@ describe('Sidebar mediator', function() {
it('fetches the data', done => {
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();
this.mediator
......@@ -51,7 +51,7 @@ describe('Sidebar mediator', function() {
it('processes fetched data', () => {
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);
expect(this.mediator.store.assignees).toEqual(mockData.assignees);
......
......@@ -50,7 +50,7 @@ describe EntityDateHelper do
end
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
expect(milestone_remaining).to eq("<strong>12</strong> days remaining")
......@@ -58,7 +58,7 @@ describe EntityDateHelper do
end
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
expect(milestone_remaining).to eq("<strong>2</strong> months remaining")
......@@ -66,7 +66,7 @@ describe EntityDateHelper do
end
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
expect(milestone_remaining).to eq("<strong>1</strong> year remaining")
......@@ -74,7 +74,7 @@ describe EntityDateHelper do
end
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
expect(milestone_remaining).to eq("<strong>Past due</strong>")
......@@ -82,7 +82,7 @@ describe EntityDateHelper do
end
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
expect(milestone_remaining).to eq("<strong>Upcoming</strong>")
......@@ -90,7 +90,7 @@ describe EntityDateHelper do
end
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
expect(milestone_remaining).to eq("<strong>2</strong> days elapsed")
......
......@@ -9,6 +9,10 @@ describe IssueSerializer do
.with_indifferent_access
end
before do
create(:epic_issue, issue: resource)
end
context 'non-sidebar issue serialization' do
let(:serializer) { nil }
......@@ -20,12 +24,16 @@ describe IssueSerializer do
context 'sidebar issue serialization' do
let(:serializer) { 'sidebar' }
before do
create(:epic_issue, issue: resource)
it 'matches issue_sidebar json schema' do
expect(json_entity).to match_schema('entities/issue_sidebar')
end
end
context 'sidebar extras issue serialization' do
let(:serializer) { 'sidebar_extras' }
it 'matches sidebar issue json schema' do
expect(json_entity).to match_schema('entities/issue_sidebar', strict: true)
it 'matches issue_sidebar_extras json schema' do
expect(json_entity).to match_schema('entities/issue_sidebar_extras')
end
end
......
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
context 'sidebar merge request serialization' do
let(:serializer) { 'sidebar' }
it 'matches basic merge request json schema' do
expect(json_entity).to match_schema('entities/merge_request_basic')
it 'matches merge_request_sidebar json schema' do
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
......
......@@ -32,6 +32,11 @@ describe 'projects/merge_requests/show.html.haml' do
assign(:noteable, closed_merge_request)
assign(:notes, [])
assign(:pipelines, Ci::Pipeline.none)
assign(
:issuable_sidebar,
MergeRequestSerializer.new(current_user: user, project: project)
.represent(closed_merge_request, serializer: 'sidebar')
)
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