Read about our upcoming Code of Conduct on this issue

This instance will be upgraded to Heptapod 0.28.1 on 2022-01-26 at 16:00 UTC+1 (a few minutes of down time)

issuables_helper.rb 13.2 KB
Newer Older
1
2
# frozen_string_literal: true

3
module IssuablesHelper
4
5
  include GitlabRoutingHelper

6
  def sidebar_gutter_toggle_icon
Filipa Lacerda's avatar
Filipa Lacerda committed
7
    sidebar_gutter_collapsed? ? icon('angle-double-left', { 'aria-hidden': 'true' }) : icon('angle-double-right', { 'aria-hidden': 'true' })
8
9
10
11
12
13
  end

  def sidebar_gutter_collapsed_class
    "right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
  end

14
15
16
17
  def sidebar_gutter_tooltip_text
    sidebar_gutter_collapsed? ? _('Expand sidebar') : _('Collapse sidebar')
  end

18
19
20
21
22
23
  def assignees_label(issuable, include_value: true)
    label = 'Assignee'.pluralize(issuable.assignees.count)

    if include_value
      sanitized_list = sanitize_name(issuable.assignee_list)
      "#{label}: #{sanitized_list}"
24
    else
25
      label
26
27
28
    end
  end

29
  def sidebar_milestone_tooltip_label(milestone)
30
31
32
    return _('Milestone') unless milestone.present?

    [milestone[:title], sidebar_milestone_remaining_days(milestone) || _('Milestone')].join('<br/>')
33
34
  end

35
  def sidebar_milestone_remaining_days(milestone)
36
    due_date_with_remaining_days(milestone[:due_date], milestone[:start_date])
37
38
39
  end

  def sidebar_due_date_tooltip_label(due_date)
40
    [_('Due date'), due_date_with_remaining_days(due_date)].compact.join('<br/>')
41
42
  end

43
44
45
46
  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)})"
47
48
49
  end

  def sidebar_label_filter_path(base_path, label_name)
50
    query_params = { label_name: [label_name] }.to_query
51

52
    "#{base_path}?#{query_params}"
53
54
  end

55
  def multi_label_name(current_labels, default_label)
56
    return default_label if current_labels.blank?
57

58
59
60
61
    title = current_labels.first.try(:title) || current_labels.first[:title]

    if current_labels.size > 1
      "#{title} +#{current_labels.size - 1} more"
Arinde Eniola's avatar
Arinde Eniola committed
62
    else
63
      title
64
65
66
    end
  end

67
  def issuable_json_path(issuable)
Jacob Schatz's avatar
Jacob Schatz committed
68
69
    project = issuable.project

Douwe Maan's avatar
Douwe Maan committed
70
    if issuable.is_a?(MergeRequest)
71
      project_merge_request_path(project, issuable.iid, :json)
Jacob Schatz's avatar
Jacob Schatz committed
72
    else
73
      project_issue_path(project, issuable.iid, :json)
Jacob Schatz's avatar
Jacob Schatz committed
74
75
76
    end
  end

77
  def serialize_issuable(issuable, opts = {})
Eric Eastwood's avatar
Eric Eastwood committed
78
79
80
81
82
83
84
85
86
    serializer_klass = case issuable
                       when Issue
                         IssueSerializer
                       when MergeRequest
                         MergeRequestSerializer
                       end

    serializer_klass
      .new(current_user: current_user, project: issuable.project)
87
      .represent(issuable, opts)
Eric Eastwood's avatar
Eric Eastwood committed
88
      .to_json
89
90
  end

91
92
93
94
95
96
97
98
99
100
101
102
103
  def template_dropdown_tag(issuable, &block)
    title = selected_template(issuable) || "Choose a template"
    options = {
      toggle_class: 'js-issuable-selector',
      title: title,
      filter: true,
      placeholder: 'Filter',
      footer_content: true,
      data: {
        data: issuable_templates(issuable),
        field_name: 'issuable_template',
        selected: selected_template(issuable),
        project_path: ref_project.path,
104
        namespace_path: ref_project.namespace.full_path
105
106
107
108
109
110
111
112
      }
    }

    dropdown_tag(title, options: options) do
      capture(&block)
    end
  end

113
  def users_dropdown_label(selected_users)
114
115
    case selected_users.length
    when 0
116
      "Unassigned"
117
    when 1
118
119
120
121
122
123
      selected_users[0].name
    else
      "#{selected_users[0].name} + #{selected_users.length - 1} more"
    end
  end

124
  # rubocop: disable CodeReuse/ActiveRecord
125
  def user_dropdown_label(user_id, default_label)
Phil Hughes's avatar
Phil Hughes committed
126
    return default_label if user_id.nil?
127
128
    return "Unassigned" if user_id == "0"

Phil Hughes's avatar
Phil Hughes committed
129
    user = User.find_by(id: user_id)
130
131
132
133
134
135
136

    if user
      user.name
    else
      default_label
    end
  end
137
  # rubocop: enable CodeReuse/ActiveRecord
138

139
  # rubocop: disable CodeReuse/ActiveRecord
140
141
142
143
144
145
146
  def project_dropdown_label(project_id, default_label)
    return default_label if project_id.nil?
    return "Any project" if project_id == "0"

    project = Project.find_by(id: project_id)

    if project
147
      project.full_name
148
149
150
151
    else
      default_label
    end
  end
152
  # rubocop: enable CodeReuse/ActiveRecord
153

154
  # rubocop: disable CodeReuse/ActiveRecord
155
156
157
158
159
160
161
162
163
164
165
166
  def group_dropdown_label(group_id, default_label)
    return default_label if group_id.nil?
    return "Any group" if group_id == "0"

    group = ::Group.find_by(id: group_id)

    if group
      group.full_name
    else
      default_label
    end
  end
167
  # rubocop: enable CodeReuse/ActiveRecord
168

Phil Hughes's avatar
Phil Hughes committed
169
  def milestone_dropdown_label(milestone_title, default_label = "Milestone")
170
171
172
173
174
175
    title =
      case milestone_title
      when Milestone::Upcoming.name then Milestone::Upcoming.title
      when Milestone::Started.name then Milestone::Started.title
      else milestone_title.presence
      end
Phil Hughes's avatar
Phil Hughes committed
176

177
    h(title || default_label)
178
179
  end

180
181
182
183
184
185
186
187
188
189
190
  def to_url_reference(issuable)
    case issuable
    when Issue
      link_to issuable.to_reference, issue_url(issuable)
    when MergeRequest
      link_to issuable.to_reference, merge_request_url(issuable)
    else
      issuable.to_reference
    end
  end

191
  def issuable_meta(issuable, project, text)
192
    output = []
Phil Hughes's avatar
Phil Hughes committed
193
    output << "Opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
194

195
    output << content_tag(:strong) do
196
      author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline")
197
      author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-inline d-sm-none")
198
199

      if status = user_status(issuable.author)
200
        author_output << "#{status}".html_safe
201
202
203
      end

      author_output
204
    end
205

206
    output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip prepend-left-4', title: _('1st contribution!'))
207

208
    output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block prepend-left-8")
Clement Ho's avatar
Clement Ho committed
209
    output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none")
210

211
    output.join.html_safe
212
213
  end

Phil Hughes's avatar
Phil Hughes committed
214
  def issuable_labels_tooltip(labels, limit: 5)
215
    first, last = labels.partition.with_index { |_, i| i < limit }
216

217
    if labels && labels.any?
218
      label_names = first.collect { |label| label.fetch(:title) }
219
      label_names << "and #{last.size} more" unless last.empty?
220

221
222
223
224
      label_names.join(', ')
    else
      _("Labels")
    end
225
226
  end

227
  def issuables_state_counter_text(issuable_type, state, display_count)
228
229
230
231
232
233
    titles = {
      opened: "Open"
    }

    state_title = titles[state] || state.to_s.humanize
    html = content_tag(:span, state_title)
234
235
236

    if display_count
      count = issuables_count_for_state(issuable_type, state)
Clement Ho's avatar
Clement Ho committed
237
      html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge badge-pill')
238
    end
239
240
241
242

    html.html_safe
  end

243
  def issuable_first_contribution_icon
micael.bergeron's avatar
micael.bergeron committed
244
    content_tag(:span, class: 'fa-stack') do
245
246
      concat(icon('certificate', class: "fa-stack-2x"))
      concat(content_tag(:strong, '1', class: 'fa-inverse fa-stack-1x'))
micael.bergeron's avatar
micael.bergeron committed
247
    end
248
249
  end

250
  def assigned_issuables_count(issuable_type)
251
252
253
254
255
256
257
258
    case issuable_type
    when :issues
      current_user.assigned_open_issues_count
    when :merge_requests
      current_user.assigned_open_merge_requests_count
    else
      raise ArgumentError, "invalid issuable `#{issuable_type}`"
    end
259
260
  end

261
262
263
264
  def issuable_reference(issuable)
    @show_full_reference ? issuable.to_reference(full: true) : issuable.to_reference(@group || @project)
  end

265
  def issuable_initial_data(issuable)
266
    data = {
267
      endpoint: issuable_path(issuable),
Clement Ho's avatar
Clement Ho committed
268
      updateEndpoint: "#{issuable_path(issuable)}.json",
269
270
      canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable),
      canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable),
271
      issuableRef: issuable.to_reference,
272
      markdownPreviewPath: preview_markdown_path(parent),
273
      markdownDocsPath: help_page_path('user/markdown'),
274
      lockVersion: issuable.lock_version,
275
      issuableTemplateNamesPath: template_names_path(parent, issuable),
276
277
278
      initialTitleHtml: markdown_field(issuable, :title),
      initialTitleText: issuable.title,
      initialDescriptionHtml: markdown_field(issuable, :description),
279
280
      initialDescriptionText: issuable.description,
      initialTaskStatus: issuable.task_status
281
282
    }

283
    data[:hasClosingMergeRequest] = issuable.merge_requests_count(current_user) != 0 if issuable.is_a?(Issue)
284
    data[:zoomMeetingUrl] = ZoomMeeting.canonical_meeting_url(issuable) if issuable.is_a?(Issue)
285

286
287
288
289
290
291
    if parent.is_a?(Group)
      data[:groupPath] = parent.path
    else
      data.merge!(projectPath: ref_project.path, projectNamespace: ref_project.namespace.full_path)
    end

Luke "Jared" Bennett's avatar
Luke "Jared" Bennett committed
292
293
    data.merge!(updated_at_by(issuable))

294
    data
295
296
297
  end

  def updated_at_by(issuable)
298
    return {} unless issuable.edited?
299
300

    {
301
      updatedAt: issuable.last_edited_at.to_time.iso8601,
302
      updatedBy: {
303
304
305
306
307
308
        name: issuable.last_edited_by.name,
        path: user_path(issuable.last_edited_by)
      }
    }
  end

309
  def issuables_count_for_state(issuable_type, state)
310
    Gitlab::IssuablesCountForState.new(finder)[state]
311
312
  end

313
314
  def close_issuable_path(issuable)
    issuable_path(issuable, close_reopen_params(issuable, :close))
315
316
  end

317
318
  def reopen_issuable_path(issuable)
    issuable_path(issuable, close_reopen_params(issuable, :reopen))
319
320
  end

321
322
323
324
325
  def close_reopen_issuable_path(issuable, should_inverse = false)
    issuable.closed? ^ should_inverse ? reopen_issuable_path(issuable) : close_issuable_path(issuable)
  end

  def issuable_path(issuable, *options)
326
    polymorphic_path(issuable, *options)
327
328
  end

329
  def issuable_url(issuable, *options)
330
    case issuable
331
332
333
334
    when Issue
      issue_url(issuable, *options)
    when MergeRequest
      merge_request_url(issuable, *options)
335
336
337
    end
  end

338
  def issuable_button_visibility(issuable, closed)
Jacopo's avatar
Jacopo committed
339
340
341
342
    return 'hidden' if issuable_button_hidden?(issuable, closed)
  end

  def issuable_button_hidden?(issuable, closed)
343
    case issuable
344
    when Issue
Jacopo's avatar
Jacopo committed
345
      issue_button_hidden?(issuable, closed)
346
    when MergeRequest
Jacopo's avatar
Jacopo committed
347
      merge_request_button_hidden?(issuable, closed)
348
349
350
351
352
    end
  end

  def issuable_close_reopen_button_method(issuable)
    case issuable
353
354
355
356
    when Issue
      ''
    when MergeRequest
      'put'
357
358
359
    end
  end

360
361
362
363
364
365
366
367
  def issuable_author_is_current_user(issuable)
    issuable.author == current_user
  end

  def issuable_display_type(issuable)
    issuable.model_name.human.downcase
  end

368
369
370
371
  def has_filter_bar_param?
    finder.class.scalar_params.any? { |p| params[p].present? }
  end

372
373
374
375
376
377
  def assignee_sidebar_data(assignee, merge_request: nil)
    { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }.tap do |data|
      data[:can_merge] = merge_request.can_be_merged_by?(assignee) if merge_request
    end
  end

378
379
380
381
382
383
  private

  def sidebar_gutter_collapsed?
    cookies[:collapsed_gutter] == 'true'
  end

384
385
386
387
  def issuable_templates(issuable)
    @issuable_templates ||=
      case issuable
      when Issue
Sean McGivern's avatar
Sean McGivern committed
388
        ref_project.repository.issue_template_names
389
      when MergeRequest
Sean McGivern's avatar
Sean McGivern committed
390
        ref_project.repository.merge_request_template_names
391
392
393
      end
  end

394
395
396
397
  def issuable_templates_names(issuable)
    issuable_templates(issuable).map { |template| template[:name] }
  end

398
  def selected_template(issuable)
399
    params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] }
400
  end
Phil Hughes's avatar
Phil Hughes committed
401

402
403
  def issuable_todo_button_data(issuable, is_collapsed)
    {
Christie Lenneville's avatar
Christie Lenneville committed
404
405
      todo_text: _('Add a To Do'),
      mark_text: _('Mark as done'),
406
407
408
409
410
411
412
413
414
      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',
Donald Cook's avatar
Donald Cook committed
415
416
417
418
419
      is_collapsed: is_collapsed,
      track_label: "right_sidebar",
      track_property: "update_todo",
      track_event: "click_button",
      track_value: ""
420
421
422
    }
  end

423
  def close_reopen_params(issuable, action)
424
425
426
427
428
    {
      issuable.model_name.to_s.underscore => { state_event: action }
    }.tap do |params|
      params[:format] = :json if issuable.is_a?(Issue)
    end
429
  end
Phil Hughes's avatar
Phil Hughes committed
430

Felipe Artur's avatar
Felipe Artur committed
431
432
433
434
435
436
437
438
  def labels_path
    if @project
      project_labels_path(@project)
    elsif @group
      group_labels_path(@group)
    end
  end

439
440
441
442
443
444
  def template_names_path(parent, issuable)
    return '' unless parent.is_a?(Project)

    project_template_names_path(parent, template_type: issuable.class.name.underscore)
  end

445
  def issuable_sidebar_options(issuable)
Phil Hughes's avatar
Phil Hughes committed
446
    {
447
448
449
450
451
452
      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],
Phil Hughes's avatar
Phil Hughes committed
453
      rootPath: root_path,
454
455
      fullPath: issuable[:project_full_path],
      timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours
Phil Hughes's avatar
Phil Hughes committed
456
457
    }
  end
458
459
460
461

  def parent
    @project || @group
  end
462
end
463
464

IssuablesHelper.prepend_if_ee('EE::IssuablesHelper')