Commit 9f976548 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu Committed by Heinrich Lee Yu

Refactor sidebar to use data from serializer

parent 6c79e930
......@@ -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');
......
......@@ -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: merge_request.project)
end
end
......@@ -22,8 +22,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
......@@ -43,8 +42,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,23 +23,36 @@ module IssuablesHelper
end
end
def sidebar_due_date_tooltip_label(issuable)
if issuable.due_date
"#{_('Due date')}<br />#{due_date_remaining_days(issuable)}"
def sidebar_milestone_tooltip_label(milestone)
if milestone && milestone[:due_date]
"#{milestone[:title]}<br/>#{sidebar_milestone_remaining_days(milestone)}"
else
_('Due date')
_('Milestone') + (milestone ? "<br/>#{milestone[:title]}" : "")
end
end
def due_date_remaining_days(issuable)
remaining_days_in_words = remaining_days_in_words(issuable)
def sidebar_milestone_remaining_days(milestone)
due_date_remaining_days(due_date: milestone[:due_date], start_date: milestone[:start_date]) if milestone[:due_date]
end
def sidebar_due_date_tooltip_label(due_date)
_('Due date') + (due_date ? "<br/>#{due_date_remaining_days(due_date)}" : "")
end
def due_date_remaining_days(due_date, start_date = nil)
"#{due_date.to_s(:medium)} (#{remaining_days_in_words(due_date: due_date, start_date: start_date)})"
end
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)
title = current_labels.first.try(:title) || current_labels.first[:title]
if current_labels.size > 1
"#{title} +#{current_labels.size - 1} more"
else
......@@ -50,13 +63,13 @@ module IssuablesHelper
end
end
def issuable_json_path(issuable, url_params = {})
def issuable_json_path(issuable)
project = issuable.project
if issuable.is_a?(MergeRequest)
project_merge_request_path(project, issuable.iid, :json, url_params)
project_merge_request_path(project, issuable.iid, :json)
else
project_issue_path(project, issuable.iid, :json, url_params)
project_issue_path(project, issuable.iid, :json)
end
end
......@@ -197,19 +210,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 { |l| l[:title] }
label_names << "and #{last.size} more" unless last.empty?
label_names.join(', ')
......@@ -356,12 +361,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,22 +385,6 @@ 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)
{
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'
}
end
def close_reopen_params(issuable, action)
{
issuable.model_name.to_s.underscore => { state_event: action }
......@@ -418,27 +401,20 @@ module IssuablesHelper
end
end
def issuable_sidebar_options(issuable, can_edit_issuable)
def issuable_sidebar_options(sidebar_data)
{
endpoint: issuable_json_path(issuable, serializer: 'sidebar_extras'),
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: "#{sidebar_data[:issuable_json_path]}?serializer=sidebar_extras",
toggleSubscriptionEndpoint: sidebar_data[:toggle_subscription_path],
moveIssueEndpoint: sidebar_data[:move_issue_path],
projectsAutocompleteEndpoint: sidebar_data[:projects_autocomplete_path],
editable: sidebar_data[:can_edit],
currentUser: sidebar_data[:current_user],
rootPath: root_path,
fullPath: @project.full_path
fullPath: sidebar_data[: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
......@@ -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"
......
......@@ -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
......
......@@ -45,13 +45,16 @@ module EntityDateHelper
# 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?)
start_date = entity.try(:start_date) || entity.try(:[], :start_date)
due_date = entity.try(:due_date) || entity.try(:[], :due_date)
if due_date && due_date.past?
content_tag(:strong, 'Past due')
elsif entity.try(:upcoming?)
elsif start_date && 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 +66,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 && start_date.past?
days = (Date.today - start_date).to_i
"#{content_tag(:strong, days)} #{'day'.pluralize(days)} elapsed".html_safe
end
end
......
......@@ -3,6 +3,109 @@
class IssuableSidebarEntity < Grape::Entity
include RequestAwareEntity
with_options if: { include_basic: true } do
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?, as: :discussion_locked
expose :reference do |issuable|
issuable.to_reference(issuable.project, full: true)
end
expose :current_user, using: UserEntity do |issuable|
request.current_user
end
# Relationships
expose :todo, using: IssuableSidebarTodoEntity do |issuable|
request.current_user.pending_todo_for(issuable) if request.current_user
end
expose :milestone, using: ::API::Entities::Milestone
expose :labels, using: LabelEntity
# Permissions
expose :signed_in do |issuable|
request.current_user.present?
end
expose :can_edit do |issuable|
can?(request.current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
end
expose :can_move do |issuable|
request.current_user && issuable.can_move?(request.current_user)
end
expose :can_admin_label do |issuable|
can?(request.current_user, :admin_label, issuable.project)
end
# Paths
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
end
with_options if: { include_extras: true } do
include TimeTrackableEntity
......
# 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
......@@ -7,9 +7,9 @@ class IssueSerializer < BaseSerializer
def represent(issue, opts = {})
entity =
case opts[:serializer]
when 'sidebar_extras'
opts[:include_basic] = false
opts[:include_extras] = true
when 'sidebar', 'sidebar_extras'
opts[:include_basic] = (opts[:serializer] == 'sidebar')
opts[:include_extras] = (opts[:serializer] == 'sidebar_extras')
IssueSidebarEntity
when 'board'
IssueBoardEntity
......
# frozen_string_literal: true
class IssueSidebarEntity < IssuableSidebarEntity
with_options if: { include_basic: true } do
expose :due_date
expose :confidential
end
with_options if: { include_extras: true } do
expose :assignees, using: API::Entities::UserBasic
end
......
......@@ -7,9 +7,9 @@ class MergeRequestSerializer < BaseSerializer
def represent(merge_request, opts = {})
entity =
case opts[:serializer]
when 'sidebar_extras'
opts[:include_basic] = false
opts[:include_extras] = true
when 'sidebar', 'sidebar_extras'
opts[:include_basic] = (opts[:serializer] == 'sidebar')
opts[:include_extras] = (opts[:serializer] == 'sidebar_extras')
MergeRequestSidebarEntity
when 'basic'
MergeRequestBasicEntity
......
# frozen_string_literal: true
class MergeRequestSidebarEntity < IssuableSidebarEntity
with_options if: { include_basic: true } do
expose :assignee, using: API::Entities::UserBasic
expose :can_merge do |issuable|
issuable.can_be_merged_by?(issuable.assignee) if issuable.assignee
end
end
end
......@@ -88,4 +88,4 @@
%section.issuable-discussion
= render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable: @issue
= render 'shared/issuable/sidebar', issuable: @issue, issuable_sidebar: @issuable_sidebar
......@@ -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: @merge_request, issuable_sidebar: @issuable_sidebar
#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: @merge_request, issuable_sidebar: @issuable_sidebar
- 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?
......
- 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[:signed_in]
- can_edit_issuable = issuable_sidebar[: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
.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, issuable.assignee, size: 24)
- else
= icon('user', 'aria-hidden': 'true')
......@@ -18,13 +22,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
- if issuable_sidebar[:assignee]
= link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do
- if !issuable.can_be_merged_by?(issuable.assignee)
- if issuable_sidebar[: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 +38,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 issuable.assignees.none?
= hidden_field_tag "#{issuable_type}[assignee_ids][]", 0, id: nil
- else
- issuable.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')
- todo = issuable_sidebar[:todo] || {}
- 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')
- mark_content = is_collapsed ? mark_icon : mark_text
- todo_content = is_collapsed ? todo_icon : todo_text
%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: (todo[:id] ? mark_text : todo_text),
'aria-label' => (todo[:id] ? mark_text : todo_text),
data: { todo_text: todo_text,
mark_text: mark_text,
todo_icon: is_collapsed ? todo_icon : nil,
mark_icon: is_collapsed ? mark_icon : nil,
issuable_id: issuable_sidebar[:id],
issuable_type: issuable_sidebar[:type],
create_path: issuable_sidebar[:create_todo_path],
delete_path: todo[:delete_path] } }
%span.issuable-todo-inner.js-issuable-todo-inner<
- if todo
- if todo[:id]
= mark_content
- else
= todo_content
......
......@@ -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"
......
......@@ -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
......
......@@ -43,16 +43,19 @@ describe IssuablesHelper do
end
describe '#issuable_labels_tooltip' do
let (:label_entity) { LabelEntity.represent(label) }
let (:label2_entity) { LabelEntity.represent(label2) }
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