diff options
Diffstat (limited to 'app/helpers')
40 files changed, 797 insertions, 391 deletions
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e5b811f3300..e5e64650708 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -77,7 +77,7 @@ module ApplicationHelper end if user - user.avatar_url(size) || default_avatar + user.avatar_url(size: size) || default_avatar else gravatar_icon(user_or_email, size, scale) end @@ -180,54 +180,22 @@ module ApplicationHelper element end - def edited_time_ago_with_tooltip(object, placement: 'top', html_class: 'time_ago', include_author: false) - return if object.updated_at == object.created_at + def edited_time_ago_with_tooltip(object, placement: 'top', html_class: 'time_ago', exclude_author: false) + return if object.last_edited_at == object.created_at || object.last_edited_at.blank? - content_tag :small, class: "edited-text" do - output = content_tag(:span, "Edited ") - output << time_ago_with_tooltip(object.updated_at, placement: placement, html_class: html_class) + content_tag :small, class: 'edited-text' do + output = content_tag(:span, 'Edited ') + output << time_ago_with_tooltip(object.last_edited_at, placement: placement, html_class: html_class) - if include_author && object.updated_by && object.updated_by != object.author - output << content_tag(:span, " by ") - output << link_to_member(object.project, object.updated_by, avatar: false, author_class: nil) + if !exclude_author && object.last_edited_by + output << content_tag(:span, ' by ') + output << link_to_member(object.project, object.last_edited_by, avatar: false, author_class: nil) end output end end - def render_markup(file_name, file_content) - if gitlab_markdown?(file_name) - Hamlit::RailsHelpers.preserve(markdown(file_content)) - elsif asciidoc?(file_name) - asciidoc(file_content) - elsif plain?(file_name) - content_tag :pre, class: 'plain-readme' do - file_content - end - else - other_markup(file_name, file_content) - end - rescue RuntimeError - simple_format(file_content) - end - - def plain?(filename) - Gitlab::MarkupHelper.plain?(filename) - end - - def markup?(filename) - Gitlab::MarkupHelper.markup?(filename) - end - - def gitlab_markdown?(filename) - Gitlab::MarkupHelper.gitlab_markdown?(filename) - end - - def asciidoc?(filename) - Gitlab::MarkupHelper.asciidoc?(filename) - end - def promo_host 'about.gitlab.com' end @@ -310,4 +278,22 @@ module ApplicationHelper def show_user_callout? cookies[:user_callout_dismissed] == 'true' end + + def linkedin_url(user) + name = user.linkedin + if name =~ %r{\Ahttps?:\/\/(www\.)?linkedin\.com\/in\/} + name + else + "https://www.linkedin.com/in/#{name}" + end + end + + def twitter_url(user) + name = user.twitter + if name =~ %r{\Ahttps?:\/\/(www\.)?twitter\.com\/} + name + else + "https://www.twitter.com/#{name}" + end + end end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 1ee6c1d3afa..9c71d6c7f4c 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -64,16 +64,8 @@ module AuthHelper current_user.identities.exists?(provider: provider.to_s) end - def two_factor_skippable? - current_application_settings.require_two_factor_authentication && - !current_user.two_factor_enabled? && - current_application_settings.two_factor_grace_period && - !two_factor_grace_period_expired? - end - - def two_factor_grace_period_expired? - current_user.otp_grace_period_started_at && - (current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current + def unlink_allowed?(provider) + %w(saml cas3).exclude?(provider.to_s) end extend self diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb index 167b09e678f..024cf38469e 100644 --- a/app/helpers/award_emoji_helper.rb +++ b/app/helpers/award_emoji_helper.rb @@ -1,10 +1,14 @@ module AwardEmojiHelper def toggle_award_url(awardable) - return url_for([:toggle_award_emoji, awardable]) unless @project + return url_for([:toggle_award_emoji, awardable]) unless @project || awardable.is_a?(Note) if awardable.is_a?(Note) # We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x) - toggle_award_emoji_namespace_project_note_url(@project.namespace, @project, awardable.id) + if awardable.for_personal_snippet? + toggle_award_emoji_snippet_note_path(awardable.noteable, awardable) + else + toggle_award_emoji_namespace_project_note_path(@project.namespace, @project, awardable.id) + end else url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 8631bc54509..622e14e21ff 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -8,31 +8,36 @@ module BlobHelper %w(credits changelog news copying copyright license authors) end - def edit_blob_link(project = @project, ref = @ref, path = @path, options = {}) - return unless current_user + def edit_path(project = @project, ref = @ref, path = @path, options = {}) + namespace_project_edit_blob_path(project.namespace, project, + tree_join(ref, path), + options[:link_opts]) + end + def edit_blob_link(project = @project, ref = @ref, path = @path, options = {}) blob = options.delete(:blob) blob ||= project.repository.blob_at(ref, path) rescue nil - return unless blob + return unless blob && blob.readable_text? - edit_path = namespace_project_edit_blob_path(project.namespace, project, - tree_join(ref, path), - options[:link_opts]) + common_classes = "btn js-edit-blob #{options[:extra_class]}" if !on_top_of_branch?(project, ref) - button_tag "Edit", class: "btn disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } - elsif can_edit_blob?(blob, project, ref) - link_to "Edit", edit_path, class: 'btn btn-sm' - elsif can?(current_user, :fork_project, project) + button_tag 'Edit', class: "#{common_classes} disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } + # This condition applies to anonymous or users who can edit directly + elsif !current_user || (current_user && can_modify_blob?(blob, project, ref)) + link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm" + elsif current_user && can?(current_user, :fork_project, project) continue_params = { - to: edit_path, + to: edit_path(project, ref, path, options), notice: edit_in_new_fork_notice, notice_now: edit_in_new_fork_notice_now } fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) - link_to "Edit", fork_path, class: 'btn', method: :post + button_tag 'Edit', + class: "#{common_classes} js-edit-blob-link-fork-toggler", + data: { action: 'edit', fork_path: fork_path } end end @@ -43,21 +48,25 @@ module BlobHelper return unless blob + common_classes = "btn btn-#{btn_class}" + if !on_top_of_branch?(project, ref) - button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' } - elsif blob.lfs_pointer? - button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' } - elsif can_edit_blob?(blob, project, ref) - button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' + button_tag label, class: "#{common_classes} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' } + elsif blob.stored_externally? + button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' } + elsif can_modify_blob?(blob, project, ref) + button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' elsif can?(current_user, :fork_project, project) continue_params = { - to: request.fullpath, + to: request.fullpath, notice: edit_in_new_fork_notice + " Try to #{action} this file again.", notice_now: edit_in_new_fork_notice_now } fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) - link_to label, fork_path, class: "btn btn-#{btn_class}", method: :post + button_tag label, + class: "#{common_classes} js-edit-blob-link-fork-toggler", + data: { action: action, fork_path: fork_path } end end @@ -85,8 +94,8 @@ module BlobHelper ) end - def can_edit_blob?(blob, project = @project, ref = @ref) - !blob.lfs_pointer? && can_edit_tree?(project, ref) + def can_modify_blob?(blob, project = @project, ref = @ref) + !blob.stored_externally? && can_edit_tree?(project, ref) end def leave_edit_message @@ -97,7 +106,7 @@ module BlobHelper if Gitlab::MarkupHelper.previewable?(filename) 'Preview' else - 'Preview Changes' + 'Preview changes' end end @@ -109,24 +118,25 @@ module BlobHelper icon("#{file_type_icon_class('file', mode, name)} fw") end - def blob_text_viewable?(blob) - blob && blob.text? && !blob.lfs_pointer? && !blob.only_display_raw? - end - - def blob_size(blob) - if blob.lfs_pointer? - blob.lfs_size - else - blob.size + def blob_raw_url + if @build && @entry + raw_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: @entry.path) + elsif @snippet + if @snippet.project_id + raw_namespace_project_snippet_path(@project.namespace, @project, @snippet) + else + raw_snippet_path(@snippet) + end + elsif @blob + namespace_project_raw_path(@project.namespace, @project, @id) end end # SVGs can contain malicious JavaScript; only include whitelisted # elements and attributes. Note that this whitelist is by no means complete # and may omit some elements. - def sanitize_svg(blob) - blob.data = Gitlab::Sanitizers::SVG.clean(blob.data) - blob + def sanitize_svg_data(data) + Gitlab::Sanitizers::SVG.clean(data) end # If we blindly set the 'real' content type when serving a Git blob we @@ -205,16 +215,82 @@ module BlobHelper end def copy_file_path_button(file_path) - clipboard_button(clipboard_text: file_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard') + clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard') + end + + def copy_blob_source_button(blob) + return unless blob.rendered_as_text?(ignore_errors: false) + + clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm js-copy-blob-source-btn", title: "Copy source to clipboard") + end + + def open_raw_blob_button(blob) + return if blob.empty? + + if blob.raw_binary? || blob.stored_externally? + icon = icon('download') + title = 'Download' + else + icon = icon('file-code-o') + title = 'Open raw' + end + + link_to icon, blob_raw_url, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' } + end + + def blob_render_error_reason(viewer) + case viewer.render_error + when :too_large + max_size = + if viewer.can_override_max_size? + viewer.overridable_max_size + else + viewer.max_size + end + "it is larger than #{number_to_human_size(max_size)}" + when :server_side_but_stored_externally + case viewer.blob.external_storage + when :lfs + 'it is stored in LFS' + when :build_artifact + 'it is stored as a job artifact' + else + 'it is stored externally' + end + end end - def copy_blob_content_button(blob) - return if markup?(blob.name) + def blob_render_error_options(viewer) + error = viewer.render_error + options = [] - clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard") + if error == :too_large && viewer.can_override_max_size? + options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, override_max_size: true, format: nil))) + end + + # If the error is `:server_side_but_stored_externally`, the simple viewer will show the same error, + # so don't bother switching. + if viewer.rich? && viewer.blob.rendered_as_text? && error != :server_side_but_stored_externally + options << link_to('view the source', '#', class: 'js-blob-viewer-switch-btn', data: { viewer: 'simple' }) + end + + options << link_to('download it', blob_raw_url, target: '_blank', rel: 'noopener noreferrer') + + options end - def open_raw_file_button(path) - link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' } + def contribution_options(project) + options = [] + + if can?(current_user, :create_issue, project) + options << link_to("submit an issue", new_namespace_project_issue_path(project.namespace, project)) + end + + merge_project = can?(current_user, :create_merge_request, project) ? project : (current_user && current_user.fork_of(project)) + if merge_project + options << link_to("create a merge request", new_namespace_project_merge_request_path(project.namespace, project)) + end + + options end end diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index f43827da446..e2df52e3833 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -9,6 +9,7 @@ module BoardsHelper issue_link_base: namespace_project_issues_path(@project.namespace, @project), root_path: root_path, bulk_update_path: bulk_update_namespace_project_issues_path(@project.namespace, @project), + default_avatar: image_path(default_avatar) } end end diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index 3fc85dc6b2b..59519c1335b 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -1,14 +1,4 @@ module BranchesHelper - def can_remove_branch?(project, branch_name) - if project.protected_branch? branch_name - false - elsif branch_name == project.repository.root_ref - false - else - can?(current_user, :push_code, project) - end - end - def filter_branches_path(options = {}) exist_opts = { search: params[:search], @@ -29,4 +19,8 @@ module BranchesHelper def project_branches options_for_select(@project.repository.branch_names, @project.default_branch) end + + def protected_branch?(project, branch) + ProtectedBranch.protected?(project, branch.name) + end end diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index 2fcb7a59fc3..2eb2c6c7389 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -1,4 +1,16 @@ module BuildsHelper + def build_summary(build, skip: false) + if build.has_trace? + if skip + link_to "View job trace", pipeline_build_url(build.pipeline, build) + else + build.trace.html(last_lines: 10).html_safe + end + else + "No job trace" + end + end + def sidebar_build_class(build, current_build) build_class = '' build_class += ' active' if build.id === current_build.id diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 0b30471f2ae..206d0753f08 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -1,29 +1,51 @@ module ButtonHelper # Output a "Copy to Clipboard" button # - # data - Data attributes passed to `content_tag` + # data - Data attributes passed to `content_tag` (default: {}): + # :text - Text to copy (optional) + # :gfm - GitLab Flavored Markdown to copy, if different from `text` (optional) + # :target - Selector for target element to copy from (optional) # # Examples: # # # Define the clipboard's text - # clipboard_button(clipboard_text: "Foo") + # clipboard_button(text: "Foo") # # => "<button class='...' data-clipboard-text='Foo'>...</button>" # # # Define the target element - # clipboard_button(clipboard_target: "div#foo") + # clipboard_button(target: "div#foo") # # => "<button class='...' data-clipboard-target='div#foo'>...</button>" # # See http://clipboardjs.com/#usage def clipboard_button(data = {}) css_class = data[:class] || 'btn-clipboard btn-transparent' title = data[:title] || 'Copy to clipboard' + + # This supports code in app/assets/javascripts/copy_to_clipboard.js that + # works around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM. + if text = data.delete(:text) + data[:clipboard_text] = + if gfm = data.delete(:gfm) + { text: text, gfm: gfm } + else + text + end + end + + target = data.delete(:target) + data[:clipboard_target] = target if target + data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) + content_tag :button, icon('clipboard', 'aria-hidden': 'true'), class: "btn #{css_class}", data: data, type: :button, - title: title + title: title, + aria: { + label: title + } end def http_clone_button(project, placement = 'right', append_link: true) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 2de9e0de310..32b1e7822af 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -1,10 +1,16 @@ +## +# DEPRECATED +# +# These helpers are deprecated in favor of detailed CI/CD statuses. +# +# See 'detailed_status?` method and `Gitlab::Ci::Status` module. +# module CiStatusHelper def ci_status_path(pipeline) project = pipeline.project namespace_project_pipeline_path(project.namespace, project, pipeline) end - # Is used by Commit and Merge Request Widget def ci_label_for_status(status) if detailed_status?(status) return status.label @@ -22,6 +28,23 @@ module CiStatusHelper end end + def ci_text_for_status(status) + if detailed_status?(status) + return status.text + end + + case status + when 'success' + 'passed' + when 'success_with_warnings' + 'passed' + when 'manual' + 'blocked' + else + status + end + end + def ci_status_for_statuseable(subject) status = subject.try(:status) || 'not found' status.humanize diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index cef624430da..d59d51905a6 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -74,12 +74,8 @@ module CommitsHelper # Returns the sorted alphabetically links to branches, separated by a comma def commit_branches_links(project, branches) branches.sort.map do |branch| - link_to( - namespace_project_tree_path(project.namespace, project, branch) - ) do - content_tag :span, class: 'label label-gray' do - icon('code-fork') + ' ' + branch - end + link_to(project_ref_path(project, branch), class: "label label-gray ref-name") do + icon('code-fork') + " #{branch}" end end.join(" ").html_safe end @@ -88,29 +84,22 @@ module CommitsHelper def commit_tags_links(project, tags) sorted = VersionSorter.rsort(tags) sorted.map do |tag| - link_to( - namespace_project_commits_path(project.namespace, project, - project.repository.find_tag(tag).name) - ) do - content_tag :span, class: 'label label-gray' do - icon('tag') + ' ' + tag - end + link_to(project_ref_path(project, tag), class: "label label-gray ref-name") do + icon('tag') + " #{tag}" end end.join(" ").html_safe end def link_to_browse_code(project, commit) + return unless current_controller?(:commits) + if @path.blank? return link_to( "Browse Files", namespace_project_tree_path(project.namespace, project, commit), class: "btn btn-default" ) - end - - return unless current_controller?(:projects, :commits) - - if @repo.blob_at(commit.id, @path) + elsif @repo.blob_at(commit.id, @path) return link_to( "Browse File", namespace_project_blob_path(project.namespace, project, @@ -200,8 +189,8 @@ module CommitsHelper tree_join(commit_sha, diff_new_path)), class: 'btn view-file js-view-file' ) do - raw('View file @') + content_tag(:span, commit_sha[0..6], - class: 'commit-short-id') + raw('View file @ ') + content_tag(:span, Commit.truncate_sha(commit_sha), + class: 'commit-sha') end end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index aed1d7c839f..4a06ee653ee 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -62,19 +62,21 @@ module DiffHelper end def parallel_diff_discussions(left, right, diff_file) - discussion_left = discussion_right = nil + return unless @grouped_diff_discussions + + discussions_left = discussions_right = nil if left && (left.unchanged? || left.removed?) line_code = diff_file.line_code(left) - discussion_left = @grouped_diff_discussions[line_code] + discussions_left = @grouped_diff_discussions[line_code] end if right && right.added? line_code = diff_file.line_code(right) - discussion_right = @grouped_diff_discussions[line_code] + discussions_right = @grouped_diff_discussions[line_code] end - [discussion_left, discussion_right] + [discussions_left, discussions_right] end def inline_diff_btn @@ -96,7 +98,7 @@ module DiffHelper [ content_tag(:span, link_to(truncate(blob.name, length: 40), tree)), '@', - content_tag(:span, commit_id, class: 'monospace'), + content_tag(:span, commit_id, class: 'commit-sha') ].join(' ').html_safe end diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 81e0b6bb5ae..8ed99642c7a 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -1,6 +1,6 @@ module DropdownsHelper def dropdown_tag(toggle_text, options: {}, &block) - content_tag :div, class: "dropdown" do + content_tag :div, class: "dropdown #{options[:wrapper_class] if options.has_key?(:wrapper_class)}" do data_attr = { toggle: "dropdown" } if options.has_key?(:data) @@ -20,7 +20,7 @@ module DropdownsHelper output << dropdown_filter(options[:placeholder]) end - output << content_tag(:div, class: "dropdown-content") do + output << content_tag(:div, class: "dropdown-content #{options[:content_class] if options.has_key?(:content_class)}") do capture(&block) if block && !options.has_key?(:footer_content) end diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index f927cfc998f..3b24f183785 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -12,7 +12,7 @@ module EmailsHelper "action" => { "@type" => "ViewAction", "name" => name, - "url" => url, + "url" => url } } diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index fb872a13f74..751d61955b7 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -1,9 +1,21 @@ module EventsHelper - def link_to_author(event) + ICON_NAMES_BY_EVENT_TYPE = { + 'pushed to' => 'icon_commit', + 'pushed new' => 'icon_commit', + 'created' => 'icon_status_open', + 'opened' => 'icon_status_open', + 'closed' => 'icon_status_closed', + 'accepted' => 'icon_code_fork', + 'commented on' => 'icon_comment_o', + 'deleted' => 'icon_trash_o' + }.freeze + + def link_to_author(event, self_added: false) author = event.author if author - link_to author.name, user_path(author.username), title: author.name + name = self_added ? 'You' : author.name + link_to name, user_path(author.username), title: name else event.author_name end @@ -29,7 +41,7 @@ module EventsHelper link_opts = { class: "event-filter-link", id: "#{key}_event_filter", - title: "Filter by #{tooltip.downcase}", + title: "Filter by #{tooltip.downcase}" } content_tag :li, class: active do @@ -152,9 +164,14 @@ module EventsHelper def event_note_title_html(event) if event.note_target - link_to(event_note_target_path(event), title: event.target_title, class: 'has-tooltip') do - "#{event.note_target_type} #{event.note_target_reference}" - end + text = raw("#{event.note_target_type} ") + + if event.commit_note? + content_tag(:span, event.note_target_reference, class: 'commit-sha') + else + event.note_target_reference + end + + link_to(text, event_note_target_path(event), title: event.target_title, class: 'has-tooltip') else content_tag(:strong, '(deleted)') end @@ -183,4 +200,21 @@ module EventsHelper "event-inline" end end + + def icon_for_event(note) + icon_name = ICON_NAMES_BY_EVENT_TYPE[note] + custom_icon(icon_name) if icon_name + end + + def icon_for_profile_event(event) + if current_path?('users#show') + content_tag :div, class: "system-note-image #{event.action_name.parameterize}-icon" do + icon_for_event(event.action_name) + end + else + content_tag :div, class: 'system-note-image user-avatar' do + author_avatar(event, size: 32) + end + end + end end diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb index 7bd212a3ef9..b981a1e8242 100644 --- a/app/helpers/explore_helper.rb +++ b/app/helpers/explore_helper.rb @@ -10,7 +10,7 @@ module ExploreHelper personal: params[:personal], archived: params[:archived], shared: params[:shared], - namespace_id: params[:namespace_id], + namespace_id: params[:namespace_id] } options = exist_opts.merge(options).delete_if { |key, value| value.blank? } diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index 1182939f656..53962b84618 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -15,4 +15,36 @@ module FormHelper end end end + + def issue_dropdown_options(issuable, has_multiple_assignees = true) + options = { + toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data', + title: 'Select assignee', + filter: true, + dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee', + placeholder: 'Search users', + data: { + first_user: current_user&.username, + null_user: true, + current_user: true, + project_id: issuable.project.try(:id), + field_name: "#{issuable.class.model_name.param_key}[assignee_ids][]", + default_label: 'Assignee', + 'max-select': 1, + 'dropdown-header': 'Assignee', + multi_select: true, + 'input-meta': 'name', + 'always-show-selectbox': true, + current_user_info: current_user.to_json(only: [:id, :name]) + } + } + + if has_multiple_assignees + options[:title] = 'Select assignee(s)' + options[:data][:'dropdown-header'] = 'Assignee(s)' + options[:data].delete(:'max-select') + end + + options + end end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index e9b7cbbad6a..fc308b3960e 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -54,6 +54,10 @@ module GitlabRoutingHelper namespace_project_builds_path(project.namespace, project, *args) end + def project_ref_path(project, ref_name, *args) + namespace_project_commits_path(project.namespace, project, ref_name, *args) + end + def project_container_registry_path(project, *args) namespace_project_container_registry_index_path(project.namespace, project, *args) end @@ -122,6 +126,14 @@ module GitlabRoutingHelper namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) end + def preview_markdown_path(project, *args) + if @snippet.is_a?(PersonalSnippet) + preview_markdown_snippet_path(@snippet) + else + preview_markdown_namespace_project_path(project.namespace, project, *args) + end + end + def toggle_subscription_path(entity, *args) if entity.is_a?(Issue) toggle_subscription_namespace_project_issue_path(entity.project.namespace, entity.project, entity) @@ -208,9 +220,31 @@ module GitlabRoutingHelper browse_namespace_project_build_artifacts_path(*args) when 'file' file_namespace_project_build_artifacts_path(*args) + when 'raw' + raw_namespace_project_build_artifacts_path(*args) end end + # Pipeline Schedules + def pipeline_schedules_path(project, *args) + namespace_project_pipeline_schedules_path(project.namespace, project, *args) + end + + def pipeline_schedule_path(schedule, *args) + project = schedule.project + namespace_project_pipeline_schedule_path(project.namespace, project, schedule, *args) + end + + def edit_pipeline_schedule_path(schedule) + project = schedule.project + edit_namespace_project_pipeline_schedule_path(project.namespace, project, schedule) + end + + def take_ownership_pipeline_schedule_path(schedule, *args) + project = schedule.project + take_ownership_namespace_project_pipeline_schedule_path(project.namespace, project, schedule, *args) + end + # Settings def project_settings_integrations_path(project, *args) namespace_project_settings_integrations_path(project.namespace, project, *args) diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index ab3ef454e1c..f29faeca22d 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -7,6 +7,12 @@ module IconsHelper # font-awesome-rails gem, but should we ever use a different icon pack in the # future we won't have to change hundreds of method calls. def icon(names, options = {}) + if (options.keys & %w[aria-hidden aria-label data-hidden]).empty? + # Add 'aria-hidden' and 'data-hidden' if they are not set in options. + options['aria-hidden'] = true + options['data-hidden'] = true + end + options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options) end @@ -14,6 +20,8 @@ module IconsHelper case names when "standard" names = "key" + when "two-factor" + names = "key" end options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index ec57fec4f99..9290e4ec133 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -37,7 +37,10 @@ module IssuablesHelper when Issue IssueSerializer.new.represent(issuable).to_json when MergeRequest - MergeRequestSerializer.new.represent(issuable).to_json + MergeRequestSerializer + .new(current_user: current_user, project: issuable.project) + .represent(issuable) + .to_json end end @@ -63,6 +66,17 @@ module IssuablesHelper end end + def users_dropdown_label(selected_users) + case selected_users.length + when 0 + "Unassigned" + when 1 + selected_users[0].name + else + "#{selected_users[0].name} + #{selected_users.length - 1} more" + end + end + def user_dropdown_label(user_id, default_label) return default_label if user_id.nil? return "Unassigned" if user_id == "0" @@ -123,11 +137,9 @@ module IssuablesHelper author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg") end - if issuable.tasks? - output << " ".html_safe - output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm") - output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg") - end + output << " ".html_safe + output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm") + output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg") output end @@ -165,11 +177,8 @@ module IssuablesHelper html.html_safe end - def cached_assigned_issuables_count(assignee, issuable_type, state) - cache_key = hexdigest(['assigned_issuables_count', assignee.id, issuable_type, state].join('-')) - Rails.cache.fetch(cache_key, expires_in: 2.minutes) do - assigned_issuables_count(assignee, issuable_type, state) - end + def assigned_issuables_count(issuable_type) + current_user.public_send("assigned_open_#{issuable_type}_count") end def issuable_filter_params @@ -192,10 +201,6 @@ module IssuablesHelper private - def assigned_issuables_count(assignee, issuable_type, state) - assignee.public_send("assigned_#{issuable_type}").public_send(state).count - end - def sidebar_gutter_collapsed? cookies[:collapsed_gutter] == 'true' end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 6978b0c89fd..82288f1da35 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -110,6 +110,14 @@ module IssuesHelper end end + def award_user_authored_class(award) + if award == 'thumbsdown' || award == 'thumbsup' + 'user-authored js-user-authored' + else + '' + end + end + def awards_sort(awards) awards.sort_by do |award, notes| if award == "thumbsup" diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb index 68c09c922a6..d5e77c7e271 100644 --- a/app/helpers/javascript_helper.rb +++ b/app/helpers/javascript_helper.rb @@ -3,7 +3,8 @@ module JavascriptHelper javascript_include_tag asset_path(js) end - def page_specific_javascript_bundle_tag(js) - javascript_include_tag(*webpack_asset_paths(js)) + # deprecated; use webpack_bundle_tag directly instead + def page_specific_javascript_bundle_tag(bundle) + webpack_bundle_tag(bundle) end end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/markup_helper.rb index cd442237086..941cfce8370 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/markup_helper.rb @@ -1,6 +1,25 @@ require 'nokogiri' -module GitlabMarkdownHelper +module MarkupHelper + include ActionView::Helpers::TagHelper + include ActionView::Context + + def plain?(filename) + Gitlab::MarkupHelper.plain?(filename) + end + + def markup?(filename) + Gitlab::MarkupHelper.markup?(filename) + end + + def gitlab_markdown?(filename) + Gitlab::MarkupHelper.gitlab_markdown?(filename) + end + + def asciidoc?(filename) + Gitlab::MarkupHelper.asciidoc?(filename) + end + # Use this in places where you would normally use link_to(gfm(...), ...). # # It solves a problem occurring with nested links (i.e. @@ -11,12 +30,12 @@ module GitlabMarkdownHelper # explicitly produce the correct linking behavior (i.e. # "<a>outer text </a><a>gfm ref</a><a> more outer text</a>"). def link_to_gfm(body, url, html_options = {}) - return "" if body.blank? + return '' if body.blank? context = { project: @project, current_user: (current_user if defined?(current_user)), - pipeline: :single_line, + pipeline: :single_line } gfm_body = Banzai.render(body, context) @@ -43,71 +62,73 @@ module GitlabMarkdownHelper fragment.to_html.html_safe end + # Return the first line of +text+, up to +max_chars+, after parsing the line + # as Markdown. HTML tags in the parsed output are not counted toward the + # +max_chars+ limit. If the length limit falls within a tag's contents, then + # the tag contents are truncated without removing the closing tag. + def first_line_in_markdown(text, max_chars = nil, options = {}) + md = markdown(text, options).strip + + truncate_visible(md, max_chars || md.length) if md.present? + end + def markdown(text, context = {}) - return "" unless text.present? + return '' unless text.present? context[:project] ||= @project - - html = Banzai.render(text, context) - banzai_postprocess(html, context) + html = markdown_unsafe(text, context) + prepare_for_rendering(html, context) end def markdown_field(object, field) object = object.for_display if object.respond_to?(:for_display) - return "" unless object.present? + return '' unless object.present? html = Banzai.render_field(object, field) - banzai_postprocess(html, object.banzai_render_context(field)) + prepare_for_rendering(html, object.banzai_render_context(field)) end - def asciidoc(text) - Gitlab::Asciidoc.render( - text, - project: @project, - current_user: (current_user if defined?(current_user)), - - # RelativeLinkFilter - project_wiki: @project_wiki, - requested_path: @path, - ref: @ref, - commit: @commit - ) + def markup(file_name, text, context = {}) + context[:project] ||= @project + html = context.delete(:rendered) || markup_unsafe(file_name, text, context) + prepare_for_rendering(html, context) end - def other_markup(file_name, text) - Gitlab::OtherMarkup.render( - file_name, - text, - project: @project, - current_user: (current_user if defined?(current_user)), + def render_wiki_content(wiki_page) + text = wiki_page.content + return '' unless text.present? + + context = { pipeline: :wiki, project: @project, project_wiki: @project_wiki, page_slug: wiki_page.slug } + + html = + case wiki_page.format + when :markdown + markdown_unsafe(text, context) + when :asciidoc + asciidoc_unsafe(text) + else + wiki_page.formatted_content.html_safe + end - # RelativeLinkFilter - project_wiki: @project_wiki, - requested_path: @path, - ref: @ref, - commit: @commit - ) + prepare_for_rendering(html, context) end - # Return the first line of +text+, up to +max_chars+, after parsing the line - # as Markdown. HTML tags in the parsed output are not counted toward the - # +max_chars+ limit. If the length limit falls within a tag's contents, then - # the tag contents are truncated without removing the closing tag. - def first_line_in_markdown(text, max_chars = nil, options = {}) - md = markdown(text, options).strip + def markup_unsafe(file_name, text, context = {}) + return '' unless text.present? - truncate_visible(md, max_chars || md.length) if md.present? - end - - def render_wiki_content(wiki_page) - case wiki_page.format - when :markdown - markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki, page_slug: wiki_page.slug) - when :asciidoc - asciidoc(wiki_page.content) + if gitlab_markdown?(file_name) + markdown_unsafe(text, context) + elsif asciidoc?(file_name) + asciidoc_unsafe(text, context) + elsif plain?(file_name) + content_tag :pre, class: 'plain-readme' do + text + end else - wiki_page.formatted_content.html_safe + other_markup_unsafe(file_name, text, context) end + rescue RuntimeError + simple_format(text) end # Returns the text necessary to reference `entity` across projects @@ -183,10 +204,10 @@ module GitlabMarkdownHelper end def markdown_toolbar_button(options = {}) - data = options[:data].merge({ container: "body" }) + data = options[:data].merge({ container: 'body' }) content_tag :button, - type: "button", - class: "toolbar-btn js-md has-tooltip hidden-xs", + type: 'button', + class: 'toolbar-btn js-md has-tooltip hidden-xs', tabindex: -1, data: data, title: options[:title], @@ -195,17 +216,35 @@ module GitlabMarkdownHelper end end - # Calls Banzai.post_process with some common context options - def banzai_postprocess(html, context) + def markdown_unsafe(text, context = {}) + Banzai.render(text, context) + end + + def asciidoc_unsafe(text, context = {}) + Gitlab::Asciidoc.render(text, context) + end + + def other_markup_unsafe(file_name, text, context = {}) + Gitlab::OtherMarkup.render(file_name, text, context) + end + + def prepare_for_rendering(html, context = {}) + return '' unless html.present? + context.merge!( current_user: (current_user if defined?(current_user)), # RelativeLinkFilter - requested_path: @path, + commit: @commit, project_wiki: @project_wiki, - ref: @ref + ref: @ref, + requested_path: @path ) - Banzai.post_process(html, context) + html = Banzai.post_process(html, context) + + Hamlit::RailsHelpers.preserve(html) end + + extend self end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 38be073c8dc..39d30631646 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -1,6 +1,6 @@ module MergeRequestsHelper def new_mr_path_from_push_event(event) - target_project = event.project.forked_from_project || event.project + target_project = event.project.default_merge_request_target new_namespace_project_merge_request_path( event.project.namespace, event.project, @@ -19,14 +19,6 @@ module MergeRequestsHelper } end - def mr_widget_refresh_url(mr) - if mr && mr.target_project - merge_widget_refresh_namespace_project_merge_request_url(mr.target_project.namespace, mr.target_project, mr) - else - '' - end - end - def mr_css_classes(mr) classes = "merge-request" classes << " closed" if mr.closed? @@ -55,22 +47,6 @@ module MergeRequestsHelper end end - def issues_sentence(issues) - # Sorting based on the `#123` or `group/project#123` reference will sort - # local issues first. - issues.map do |issue| - issue.to_reference(@project) - end.sort.to_sentence - end - - def mr_closes_issues - @mr_closes_issues ||= @merge_request.closes_issues(current_user) - end - - def mr_issues_mentioned_but_not_closing - @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing(current_user) - end - def mr_change_branches_path(merge_request) new_namespace_project_merge_request_path( @project.namespace, @project, @@ -78,41 +54,12 @@ module MergeRequestsHelper source_project_id: merge_request.source_project_id, target_project_id: merge_request.target_project_id, source_branch: merge_request.source_branch, - target_branch: merge_request.target_branch, + target_branch: merge_request.target_branch }, change_branches: true ) end - def mr_assign_issues_link - issues = MergeRequests::AssignIssuesService.new(@project, - current_user, - merge_request: @merge_request, - closes_issues: mr_closes_issues - ).assignable_issues - path = assign_related_issues_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) - if issues.present? - pluralize_this_issue = issues.count > 1 ? "these issues" : "this issue" - link_to "Assign yourself to #{pluralize_this_issue}", path, method: :post - end - end - - def source_branch_with_namespace(merge_request) - namespace = merge_request.source_project_namespace - branch = merge_request.source_branch - - if merge_request.source_branch_exists? - namespace = link_to(namespace, project_path(merge_request.source_project)) - branch = link_to(branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch)) - end - - if merge_request.for_fork? - namespace + ":" + branch - else - branch - end - end - def format_mr_branch_names(merge_request) source_path = merge_request.source_project_path target_path = merge_request.target_project_path @@ -126,6 +73,10 @@ module MergeRequestsHelper end end + def target_projects(project) + [project, project.default_merge_request_target].uniq + end + def merge_request_button_visibility(merge_request, closed) return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork? end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index c9e70faa52e..c515774140c 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -115,4 +115,28 @@ module MilestonesHelper end end end + + def milestone_merge_request_tab_path(milestone) + if @project + merge_requests_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json) + elsif @group + merge_requests_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json) + end + end + + def milestone_participants_tab_path(milestone) + if @project + participants_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json) + elsif @group + participants_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json) + end + end + + def milestone_labels_tab_path(milestone) + if @project + labels_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json) + elsif @group + labels_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json) + end + end end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index b0331f36a2f..375110b77e2 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -19,62 +19,29 @@ module NotesHelper id: noteable.id, class: noteable.class.name, resources: noteable.class.table_name, - project_id: noteable.project.id, + project_id: noteable.project.id }.to_json end def diff_view_data - return {} unless @comments_target + return {} unless @new_diff_note_attrs - @comments_target.slice(:noteable_id, :noteable_type, :commit_id) + @new_diff_note_attrs.slice(:noteable_id, :noteable_type, :commit_id) end def diff_view_line_data(line_code, position, line_type) return if @diff_notes_disabled - use_legacy_diff_note = @use_legacy_diff_notes - # If the controller doesn't force the use of legacy diff notes, we - # determine this on a line-by-line basis by seeing if there already exist - # active legacy diff notes at this line, in which case newly created notes - # will use the legacy technology as well. - # We do this because the discussion_id values of legacy and "new" diff - # notes, which are used to group notes on the merge request discussion tab, - # are incompatible. - # If we didn't, diff notes that would show for the same line on the changes - # tab, would show in different discussions on the discussion tab. - use_legacy_diff_note ||= begin - discussion = @grouped_diff_discussions[line_code] - discussion && discussion.legacy_diff_discussion? - end - data = { line_code: line_code, - line_type: line_type, + line_type: line_type } - if use_legacy_diff_note - discussion_id = LegacyDiffNote.discussion_id( - @comments_target[:noteable_type], - @comments_target[:noteable_id] || @comments_target[:commit_id], - line_code - ) - - data.merge!( - note_type: LegacyDiffNote.name, - discussion_id: discussion_id - ) + if @use_legacy_diff_notes + data[:note_type] = LegacyDiffNote.name else - discussion_id = DiffNote.discussion_id( - @comments_target[:noteable_type], - @comments_target[:noteable_id] || @comments_target[:commit_id], - position - ) - - data.merge!( - position: position.to_json, - note_type: DiffNote.name, - discussion_id: discussion_id - ) + data[:note_type] = DiffNote.name + data[:position] = position.to_json end data @@ -83,32 +50,73 @@ module NotesHelper def link_to_reply_discussion(discussion, line_type = nil) return unless current_user - data = discussion.reply_attributes.merge(line_type: line_type) + data = { discussion_id: discussion.id, line_type: line_type } button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', data: data, title: 'Add a reply' end - def preload_max_access_for_authors(notes, project) - user_ids = notes.map(&:author_id) - project.team.max_member_access_for_user_ids(user_ids) + def note_max_access_for_user(note) + note.project.team.human_max_access(note.author_id) end - def preload_noteable_for_regular_notes(notes) - ActiveRecord::Associations::Preloader.new.preload(notes.select { |note| !note.for_commit? }, :noteable) + def discussion_path(discussion) + if discussion.for_merge_request? + return unless discussion.diff_discussion? + + version_params = discussion.merge_request_version_params + return unless version_params + + path_params = version_params.merge(anchor: discussion.line_code) + + diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, path_params) + elsif discussion.for_commit? + anchor = discussion.line_code if discussion.diff_discussion? + + namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: anchor) + end end - def note_max_access_for_user(note) - note.project.team.human_max_access(note.author_id) + def notes_url + if @snippet.is_a?(PersonalSnippet) + snippet_notes_path(@snippet) + else + namespace_project_noteable_notes_path( + namespace_id: @project.namespace, + project_id: @project, + target_id: @noteable.id, + target_type: @noteable.class.name.underscore + ) + end end - def discussion_diff_path(discussion) - return unless discussion.diff_discussion? + def note_url(note) + if note.noteable.is_a?(PersonalSnippet) + snippet_note_path(note.noteable, note) + else + namespace_project_note_path(@project.namespace, @project, note) + end + end - if discussion.for_merge_request? && discussion.active? - diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code) - elsif discussion.for_commit? - namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code) + def form_resources + if @snippet.is_a?(PersonalSnippet) + [@note] + else + [@project.namespace.becomes(Namespace), @project, @note] + end + end + + def new_form_url + return nil unless @snippet.is_a?(PersonalSnippet) + + snippet_notes_path(@snippet) + end + + def can_create_note? + if @snippet.is_a?(PersonalSnippet) + can?(current_user, :comment_personal_snippet, @snippet) + else + can?(current_user, :create_note, @project) end end end diff --git a/app/helpers/pipeline_schedules_helper.rb b/app/helpers/pipeline_schedules_helper.rb new file mode 100644 index 00000000000..fee1edc2a1b --- /dev/null +++ b/app/helpers/pipeline_schedules_helper.rb @@ -0,0 +1,11 @@ +module PipelineSchedulesHelper + def timezone_data + ActiveSupport::TimeZone.all.map do |timezone| + { + name: timezone.name, + offset: timezone.utc_offset, + identifier: timezone.tzinfo.identifier + } + end + end +end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 243ef39ef61..de959f13713 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -63,6 +63,10 @@ module PreferencesHelper end def anonymous_project_view - @project.empty_repo? || !can?(current_user, :download_code, @project) ? 'activity' : 'readme' + if !@project.empty_repo? && can?(current_user, :download_code, @project) + 'files' + else + 'activity' + end end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index bd0c2cd661e..98bbcfaaba5 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -24,7 +24,7 @@ module ProjectsHelper return "(deleted)" unless author - author_html = "" + author_html = "" # Build avatar image tag author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]} #{opts[:avatar_class] if opts[:avatar_class]}", alt: '') if opts[:avatar] @@ -45,7 +45,7 @@ module ProjectsHelper link_to(author_html, user_path(author), class: "author_link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe else title = opts[:title].sub(":name", sanitize(author.name)) - link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe + link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' }).html_safe end end @@ -110,11 +110,8 @@ module ProjectsHelper end def license_short_name(project) - return 'LICENSE' if project.repository.license_key.nil? - - license = Licensee::License.new(project.repository.license_key) - - license.nickname || license.name + license = project.repository.license + license&.nickname || license&.name || 'LICENSE' end def last_push_event @@ -160,12 +157,25 @@ module ProjectsHelper end def project_list_cache_key(project) - key = [project.namespace.cache_key, project.cache_key, controller.controller_name, controller.action_name, current_application_settings.cache_key, 'v2.3'] + key = [ + project.route.cache_key, + project.cache_key, + controller.controller_name, + controller.action_name, + current_application_settings.cache_key, + 'v2.4' + ] + key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status? key end + def load_pipeline_status(projects) + Gitlab::Cache::Ci::ProjectPipelineStatus. + load_in_batch_for_projects(projects) + end + private def repo_children_classes(field) @@ -272,14 +282,14 @@ module ProjectsHelper end end - def add_special_file_path(project, file_name:, commit_message: nil, target_branch: nil, context: nil) + def add_special_file_path(project, file_name:, commit_message: nil, branch_name: nil, context: nil) namespace_project_new_blob_path( project.namespace, project, project.default_branch || 'master', file_name: file_name, commit_message: commit_message || "Add #{file_name.downcase}", - target_branch: target_branch, + branch_name: branch_name, context: context ) end @@ -407,7 +417,10 @@ module ProjectsHelper def sanitize_repo_path(project, message) return '' unless message.present? - message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]") + exports_path = File.join(Settings.shared['path'], 'tmp/project_exports') + filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]") + + filtered_message.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]") end def project_feature_options @@ -427,13 +440,22 @@ module ProjectsHelper end def visibility_select_options(project, selected_level) - levels_options_array = Gitlab::VisibilityLevel.values.map do |level| - [ + level_options = Gitlab::VisibilityLevel.values.each_with_object([]) do |level, level_options| + next if restricted_levels.include?(level) + + level_options << [ visibility_level_label(level), { data: { description: visibility_level_description(level, project) } }, level ] end - options_for_select(levels_options_array, selected_level) + + options_for_select(level_options, selected_level) + end + + def restricted_levels + return [] if current_user.admin? + + current_application_settings.restricted_visibility_levels || [] end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 8ff8db16514..9c46035057f 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -42,7 +42,7 @@ module SearchHelper { category: "Settings", label: "User settings", url: profile_path }, { category: "Settings", label: "SSH Keys", url: profile_keys_path }, { category: "Settings", label: "Dashboard", url: root_path }, - { category: "Settings", label: "Admin Section", url: admin_root_path }, + { category: "Settings", label: "Admin Section", url: admin_root_path } ] end @@ -57,7 +57,7 @@ module SearchHelper { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh/README") }, { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks/system_hooks") }, { category: "Help", label: "Webhooks Help", url: help_page_path("user/project/integrations/webhooks") }, - { category: "Help", label: "Workflow Help", url: help_page_path("workflow/README") }, + { category: "Help", label: "Workflow Help", url: help_page_path("workflow/README") } ] end @@ -76,7 +76,7 @@ module SearchHelper { category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, { category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, { category: "Current Project", label: "Members", url: namespace_project_settings_members_path(@project.namespace, @project) }, - { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, + { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) } ] else [] diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 8706876ae4a..a7d1fe4aa47 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -67,7 +67,7 @@ module SelectsHelper current_user: opts[:current_user] || false, "push-code-to-protected-branches" => opts[:push_code_to_protected_branches], author_id: opts[:author_id] || '', - skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil, + skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil } end end diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index 715e5893a2c..3707bb5ba36 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -13,8 +13,8 @@ module ServicesHelper "Event will be triggered when a confidential issue is created/updated/closed" when "merge_request", "merge_request_events" "Event will be triggered when a merge request is created/updated/merged" - when "build", "build_events" - "Event will be triggered when a build status changes" + when "pipeline", "pipeline_events" + "Event will be triggered when a pipeline status changes" when "wiki_page", "wiki_page_events" "Event will be triggered when a wiki page is created/updated" when "commit", "commit_events" diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 8c02b4061ca..2fd64b3441e 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -8,6 +8,14 @@ module SnippetsHelper end end + def download_snippet_path(snippet) + if snippet.project_id + raw_namespace_project_snippet_path(@project.namespace, @project, snippet, inline: false) + else + raw_snippet_path(snippet, inline: false) + end + end + # Return the path of a snippets index for a user or for a project # # @returns String, path to snippet index @@ -42,7 +50,7 @@ module SnippetsHelper 0, lined_content.size, surrounding_lines - ) if line.include?(query) + ) if line.downcase.include?(query.downcase) end used_lines.uniq.sort diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 5c89cbea3fc..b408ec0c6a4 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -25,8 +25,8 @@ module SortingHelper def projects_sort_options_hash options = { sort_value_name => sort_title_name, - sort_value_recently_updated => sort_title_recently_updated, - sort_value_oldest_updated => sort_title_oldest_updated, + sort_value_latest_activity => sort_title_latest_activity, + sort_value_oldest_activity => sort_title_oldest_activity, sort_value_recently_created => sort_title_recently_created, sort_value_oldest_created => sort_title_oldest_created } @@ -58,7 +58,23 @@ module SortingHelper sort_value_due_date_soon => sort_title_due_date_soon, sort_value_due_date_later => sort_title_due_date_later, sort_value_start_date_soon => sort_title_start_date_soon, - sort_value_start_date_later => sort_title_start_date_later, + sort_value_start_date_later => sort_title_start_date_later + } + end + + def branches_sort_options_hash + { + sort_value_name => sort_title_name, + sort_value_recently_updated => sort_title_recently_updated, + sort_value_oldest_updated => sort_title_oldest_updated + } + end + + def tags_sort_options_hash + { + sort_value_name => sort_title_name, + sort_value_recently_updated => sort_title_recently_updated, + sort_value_oldest_updated => sort_title_oldest_updated } end @@ -78,6 +94,14 @@ module SortingHelper 'Last updated' end + def sort_title_oldest_activity + 'Oldest updated' + end + + def sort_title_latest_activity + 'Last updated' + end + def sort_title_oldest_created 'Oldest created' end @@ -198,6 +222,14 @@ module SortingHelper 'updated_desc' end + def sort_value_oldest_activity + 'latest_activity_asc' + end + + def sort_value_latest_activity + 'latest_activity_desc' + end + def sort_value_oldest_created 'created_asc' end diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index fb95f2b565e..09b73eee8cf 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -1,28 +1,34 @@ module SubmoduleHelper include Gitlab::ShellAdapter + VALID_SUBMODULE_PROTOCOLS = %w[http https git ssh].freeze + # links to files listing for submodule if submodule is a project on this server def submodule_links(submodule_item, ref = nil, repository = @repository) url = repository.submodule_url_for(ref, submodule_item.path) - return url, nil unless url =~ /([^\/:]+)\/([^\/]+\.git)\Z/ - - namespace = $1 - project = $2 - project.chomp!('.git') - - if self_url?(url, namespace, project) - return namespace_project_path(namespace, project), - namespace_project_tree_path(namespace, project, - submodule_item.id) - elsif relative_self_url?(url) - relative_self_links(url, submodule_item.id) - elsif github_dot_com_url?(url) - standard_links('github.com', namespace, project, submodule_item.id) - elsif gitlab_dot_com_url?(url) - standard_links('gitlab.com', namespace, project, submodule_item.id) + if url == '.' || url == './' + url = File.join(Gitlab.config.gitlab.url, @project.full_path) + end + + if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/ + namespace, project = $1, $2 + project.sub!(/\.git\z/, '') + + if self_url?(url, namespace, project) + [namespace_project_path(namespace, project), + namespace_project_tree_path(namespace, project, submodule_item.id)] + elsif relative_self_url?(url) + relative_self_links(url, submodule_item.id) + elsif github_dot_com_url?(url) + standard_links('github.com', namespace, project, submodule_item.id) + elsif gitlab_dot_com_url?(url) + standard_links('gitlab.com', namespace, project, submodule_item.id) + else + [sanitize_submodule_url(url), nil] + end else - return url, nil + [sanitize_submodule_url(url), nil] end end @@ -37,14 +43,16 @@ module SubmoduleHelper end def self_url?(url, namespace, project) - return true if url == [Gitlab.config.gitlab.url, '/', namespace, '/', - project, '.git'].join('') - url == gitlab_shell.url_to_repo([namespace, '/', project].join('')) + url_no_dotgit = url.chomp('.git') + return true if url_no_dotgit == [Gitlab.config.gitlab.url, '/', namespace, '/', + project].join('') + url_with_dotgit = url_no_dotgit + '.git' + url_with_dotgit == gitlab_shell.url_to_repo([namespace, '/', project].join('')) end def relative_self_url?(url) # (./)?(../repo.git) || (./)?(../../project/repo.git) ) - url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\z/ + url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*(\.git)?\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*(\.git)?\z/ end def standard_links(host, namespace, project, commit) @@ -71,4 +79,16 @@ module SubmoduleHelper namespace_project_tree_path(namespace, base, commit) ] end + + def sanitize_submodule_url(url) + uri = URI.parse(url) + + if uri.scheme.in?(VALID_SUBMODULE_PROTOCOLS) + uri.to_s + else + nil + end + rescue URI::InvalidURIError + nil + end end diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb new file mode 100644 index 00000000000..d889d141101 --- /dev/null +++ b/app/helpers/system_note_helper.rb @@ -0,0 +1,27 @@ +module SystemNoteHelper + ICON_NAMES_BY_ACTION = { + 'commit' => 'icon_commit', + 'description' => 'icon_edit', + 'merge' => 'icon_merge', + 'merged' => 'icon_merged', + 'opened' => 'icon_status_open', + 'closed' => 'icon_status_closed', + 'time_tracking' => 'icon_stopwatch', + 'assignee' => 'icon_user', + 'title' => 'icon_edit', + 'task' => 'icon_check_square_o', + 'label' => 'icon_tags', + 'cross_reference' => 'icon_random', + 'branch' => 'icon_code_fork', + 'confidential' => 'icon_eye_slash', + 'visible' => 'icon_eye', + 'milestone' => 'icon_clock_o', + 'discussion' => 'icon_comment_o', + 'moved' => 'icon_arrow_circle_o_right' + }.freeze + + def icon_for_system_note(note) + icon_name = ICON_NAMES_BY_ACTION[note.system_note_metadata&.action] + custom_icon(icon_name) if icon_name + end +end diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index c0ec1634cdb..31aaf9e5607 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -21,4 +21,8 @@ module TagsHelper html.html_safe end + + def protected_tag?(project, tag) + ProtectedTag.protected?(project, tag.name) + end end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 4f5adf623f2..19286fadb19 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -13,21 +13,24 @@ module TodosHelper def todo_action_name(todo) case todo.action - when Todo::ASSIGNED then 'assigned you' - when Todo::MENTIONED then 'mentioned you on' + when Todo::ASSIGNED then todo.self_added? ? 'assigned' : 'assigned you' + when Todo::MENTIONED then "mentioned #{todo_action_subject(todo)} on" when Todo::BUILD_FAILED then 'The build failed for' when Todo::MARKED then 'added a todo for' - when Todo::APPROVAL_REQUIRED then 'set you as an approver for' + when Todo::APPROVAL_REQUIRED then "set #{todo_action_subject(todo)} as an approver for" when Todo::UNMERGEABLE then 'Could not merge' - when Todo::DIRECTLY_ADDRESSED then 'directly addressed you on' + when Todo::DIRECTLY_ADDRESSED then "directly addressed #{todo_action_subject(todo)} on" end end def todo_target_link(todo) - target = todo.target_type.titleize.downcase - link_to "#{target} #{todo.target_reference}", todo_target_path(todo), - class: 'has-tooltip', - title: todo.target.title + text = raw("#{todo.target_type.titleize.downcase} ") + + if todo.for_commit? + content_tag(:span, todo.target_reference, class: 'commit-sha') + else + todo.target_reference + end + link_to text, todo_target_path(todo), class: 'has-tooltip', title: todo.target.title end def todo_target_path(todo) @@ -63,7 +66,7 @@ module TodosHelper project_id: params[:project_id], author_id: params[:author_id], type: params[:type], - action_id: params[:action_id], + action_id: params[:action_id] } end @@ -148,6 +151,10 @@ module TodosHelper private + def todo_action_subject(todo) + todo.self_added? ? 'yourself' : 'you' + end + def show_todo_state?(todo) (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state) end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 4a76c679bad..e0d3e9b88f3 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -12,10 +12,6 @@ module TreeHelper tree.html_safe end - def render_readme(readme) - render_markup(readme.name, readme.data) - end - # Return an image icon depending on the file type and mode # # type - String type of the tree item; either 'folder' or 'file' @@ -35,7 +31,7 @@ module TreeHelper end def on_top_of_branch?(project = @project, ref = @ref) - project.repository.branch_names.include?(ref) + project.repository.branch_exists?(ref) end def can_edit_tree?(project = nil, ref = nil) @@ -80,19 +76,19 @@ module TreeHelper "A new branch will be created in your fork and a new merge request will be started." end - def tree_breadcrumbs(tree, max_links = 2) + def path_breadcrumbs(max_links = 6) if @path.present? part_path = "" parts = @path.split('/') - yield('..', nil) if parts.count > max_links + yield('..', File.join(*parts.first(parts.count - 2))) if parts.count > max_links parts.each do |part| part_path = File.join(part_path, part) unless part_path.empty? part_path = part if part_path.empty? next if parts.count > max_links && !parts.last(2).include?(part) - yield(part, tree_join(@ref, part_path)) + yield(part, part_path) end end end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 169cedeb796..b4aaf498068 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -85,7 +85,7 @@ module VisibilityLevelHelper end def restricted_visibility_levels(show_all = false) - return [] if current_user.is_admin? && !show_all + return [] if current_user.admin? && !show_all current_application_settings.restricted_visibility_levels || [] end diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb new file mode 100644 index 00000000000..6bacda9fe75 --- /dev/null +++ b/app/helpers/webpack_helper.rb @@ -0,0 +1,30 @@ +require 'webpack/rails/manifest' + +module WebpackHelper + def webpack_bundle_tag(bundle) + javascript_include_tag(*gitlab_webpack_asset_paths(bundle)) + end + + # override webpack-rails gem helper until changes can make it upstream + def gitlab_webpack_asset_paths(source, extension: nil) + return "" unless source.present? + + paths = Webpack::Rails::Manifest.asset_paths(source) + if extension + paths = paths.select { |p| p.ends_with? ".#{extension}" } + end + + # include full webpack-dev-server url for rspec tests running locally + if Rails.env.test? && Rails.configuration.webpack.dev_server.enabled + host = Rails.configuration.webpack.dev_server.host + port = Rails.configuration.webpack.dev_server.port + protocol = Rails.configuration.webpack.dev_server.https ? 'https' : 'http' + + paths.map! do |p| + "#{protocol}://#{host}:#{port}#{p}" + end + end + + paths + end +end |