diff options
Diffstat (limited to 'app/helpers')
69 files changed, 1565 insertions, 661 deletions
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index 16136d02530..cdf5fa5d4b7 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -20,7 +20,7 @@ module AppearancesHelper end def brand_item - @appearance ||= Appearance.first + @appearance ||= Appearance.current end def brand_header_logo diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index fff57472a4f..bcee81bdc15 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -68,7 +68,7 @@ module ApplicationHelper end end - def avatar_icon(user_or_email = nil, size = nil, scale = 2) + def avatar_icon(user_or_email = nil, size = nil, scale = 2, only_path: true) user = if user_or_email.is_a?(User) user_or_email @@ -77,7 +77,7 @@ module ApplicationHelper end if user - user.avatar_url(size) || default_avatar + user.avatar_url(size: size, only_path: only_path) || default_avatar else gravatar_icon(user_or_email, size, scale) end @@ -131,10 +131,7 @@ module ApplicationHelper end def body_data_page - path = controller.controller_path.split('/') - namespace = path.first if path.second - - [namespace, controller.controller_name, controller.action_name].compact.join(':') + [*controller.controller_path.split('/'), controller.action_name].compact.join(':') end # shortcut for gitlab config @@ -167,9 +164,9 @@ module ApplicationHelper css_classes = short_format ? 'js-short-timeago' : 'js-timeago' css_classes << " #{html_class}" unless html_class.blank? - element = content_tag :time, time.strftime("%b %d, %Y"), + element = content_tag :time, l(time, format: "%b %d, %Y"), class: css_classes, - title: time.to_time.in_time_zone.to_s(:medium), + title: l(time.to_time.in_time_zone, format: :timeago_tooltip), datetime: time.to_time.getutc.iso8601, data: { toggle: 'tooltip', @@ -180,16 +177,16 @@ 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 unless object.is_edited? - 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 @@ -204,6 +201,10 @@ module ApplicationHelper 'https://' + promo_host end + def support_url + current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/' + end + def page_filter_path(options = {}) without = options.delete(:without) add_label = options.delete(:label) @@ -263,7 +264,11 @@ module ApplicationHelper end def page_class - "issue-boards-page" if current_controller?(:boards) + class_names = [] + class_names << 'issue-boards-page' if current_controller?(:boards) + class_names << 'with-performance-bar' if performance_bar_enabled? + + class_names end # Returns active css class when condition returns true @@ -275,7 +280,37 @@ module ApplicationHelper 'active' if condition end - def show_user_callout? - cookies[:user_callout_dismissed] == 'true' + def show_callout?(name) + cookies[name] != '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 + + def show_new_nav? + cookies["new_nav"] == "true" + end + + def collapsed_sidebar? + cookies["sidebar_collapsed"] == "true" + end + + def show_new_repo? + cookies["new_repo"] == "true" && body_data_page != 'projects:show' end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index ca326dd0627..150188f0b65 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -1,7 +1,8 @@ module ApplicationSettingsHelper + extend self delegate :gravatar_enabled?, :signup_enabled?, - :signin_enabled?, + :password_authentication_enabled?, :akismet_enabled?, :koding_enabled?, to: :current_application_settings @@ -34,17 +35,17 @@ module ApplicationSettingsHelper # Return a group of checkboxes that use Bootstrap's button plugin for a # toggle button effect. - def restricted_level_checkboxes(help_block_id) - Gitlab::VisibilityLevel.options.map do |name, level| + def restricted_level_checkboxes(help_block_id, checkbox_name) + Gitlab::VisibilityLevel.values.map do |level| checked = restricted_visibility_levels(true).include?(level) css_class = checked ? 'active' : '' - checkbox_name = "application_setting[restricted_visibility_levels][]" + tag_name = "application_setting_visibility_level_#{level}" - label_tag(name, class: css_class) do + label_tag(tag_name, class: css_class) do check_box_tag(checkbox_name, level, checked, autocomplete: 'off', 'aria-describedby' => help_block_id, - id: name) + visibility_level_icon(level) + name + id: tag_name) + visibility_level_icon(level) + visibility_level_label(level) end end end @@ -91,4 +92,89 @@ module ApplicationSettingsHelper def sidekiq_queue_options_for_select options_for_select(Sidekiq::Queue.all.map(&:name), @application_setting.sidekiq_throttling_queues) end + + def visible_attributes + [ + :admin_notification_email, + :after_sign_out_path, + :after_sign_up_text, + :akismet_api_key, + :akismet_enabled, + :clientside_sentry_dsn, + :clientside_sentry_enabled, + :container_registry_token_expire_delay, + :default_artifacts_expire_in, + :default_branch_protection, + :default_group_visibility, + :default_project_visibility, + :default_projects_limit, + :default_snippet_visibility, + :disabled_oauth_sign_in_sources, + :domain_blacklist_enabled, + :domain_blacklist_raw, + :domain_whitelist_raw, + :email_author_in_body, + :enabled_git_access_protocol, + :gravatar_enabled, + :help_page_hide_commercial_content, + :help_page_support_url, + :help_page_text, + :home_page_url, + :housekeeping_bitmaps_enabled, + :housekeeping_enabled, + :housekeeping_full_repack_period, + :housekeeping_gc_period, + :housekeeping_incremental_repack_period, + :html_emails_enabled, + :import_sources, + :koding_enabled, + :koding_url, + :max_artifacts_size, + :max_attachment_size, + :max_pages_size, + :metrics_enabled, + :metrics_host, + :metrics_method_call_threshold, + :metrics_packet_size, + :metrics_pool_size, + :metrics_port, + :metrics_sample_interval, + :metrics_timeout, + :password_authentication_enabled, + :performance_bar_allowed_group_id, + :performance_bar_enabled, + :plantuml_enabled, + :plantuml_url, + :polling_interval_multiplier, + :project_export_enabled, + :prometheus_metrics_enabled, + :recaptcha_enabled, + :recaptcha_private_key, + :recaptcha_site_key, + :repository_checks_enabled, + :repository_storages, + :require_two_factor_authentication, + :restricted_visibility_levels, + :send_user_confirmation_email, + :sentry_dsn, + :sentry_enabled, + :session_expire_delay, + :shared_runners_enabled, + :shared_runners_text, + :sidekiq_throttling_enabled, + :sidekiq_throttling_factor, + :sidekiq_throttling_queues, + :sign_in_text, + :signup_enabled, + :terminal_max_session_time, + :two_factor_grace_period, + :unique_ips_limit_enabled, + :unique_ips_limit_per_user, + :unique_ips_limit_time_window, + :usage_ping_enabled, + :user_default_external, + :user_oauth_applications, + :version_check_enabled + ] + end end diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index b7e0ff8ecd0..4b51269533c 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -8,18 +8,24 @@ module AvatarsHelper })) end - def user_avatar(options = {}) + def user_avatar_without_link(options = {}) avatar_size = options[:size] || 16 user_name = options[:user].try(:name) || options[:user_name] - css_class = options[:css_class] || '' - - avatar = image_tag( - avatar_icon(options[:user] || options[:user_email], avatar_size), - class: "avatar has-tooltip s#{avatar_size} #{css_class}", + avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size) + data_attributes = { container: 'body' } + + image_tag( + avatar_url, + class: %W[avatar has-tooltip s#{avatar_size}].push(*options[:css_class]), alt: "#{user_name}'s avatar", title: user_name, - data: { container: 'body' } + data: data_attributes, + lazy: true ) + end + + def user_avatar(options = {}) + avatar = user_avatar_without_link(options) if options[:user] link_to(avatar, user_path(options[:user])) diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb index 167b09e678f..86b19368cfd 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_project_note_path(@project, awardable.id) + end else url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) end diff --git a/app/helpers/blame_helper.rb b/app/helpers/blame_helper.rb new file mode 100644 index 00000000000..d1dc4d94560 --- /dev/null +++ b/app/helpers/blame_helper.rb @@ -0,0 +1,21 @@ +module BlameHelper + def age_map_duration(blame_groups, project) + now = Time.zone.now + start_date = blame_groups.map { |blame_group| blame_group[:commit].committed_date } + .append(project.created_at).min + + { + now: now, + started_days_ago: (now - start_date).to_i / 1.day + } + end + + def age_map_class(commit_date, duration) + commit_date_days_ago = (duration[:now] - commit_date).to_i / 1.day + # Numbers 0 to 10 come from this calculation, but only commits on the oldest + # day get number 10 (all other numbers can be multiple days), so the range + # is normalized to 0-9 + age_group = [(10 * commit_date_days_ago) / duration[:started_days_ago], 9].min + "blame-commit-age-#{age_group}" + end +end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 36b16421e8f..18075ee8be7 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -9,7 +9,7 @@ module BlobHelper end def edit_path(project = @project, ref = @ref, path = @path, options = {}) - namespace_project_edit_blob_path(project.namespace, project, + project_edit_blob_path(project, tree_join(ref, path), options[:link_opts]) end @@ -18,7 +18,7 @@ module BlobHelper blob = options.delete(:blob) blob ||= project.repository.blob_at(ref, path) rescue nil - return unless blob + return unless blob && blob.readable_text? common_classes = "btn js-edit-blob #{options[:extra_class]}" @@ -33,7 +33,7 @@ module BlobHelper 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) + fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params) button_tag 'Edit', class: "#{common_classes} js-edit-blob-link-fork-toggler", @@ -52,7 +52,7 @@ module BlobHelper if !on_top_of_branch?(project, ref) 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.lfs_pointer? + 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' @@ -62,7 +62,7 @@ module BlobHelper 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) + fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params) button_tag label, class: "#{common_classes} js-edit-blob-link-fork-toggler", @@ -95,7 +95,7 @@ module BlobHelper end def can_modify_blob?(blob, project = @project, ref = @ref) - !blob.lfs_pointer? && can_edit_tree?(project, ref) + !blob.stored_externally? && can_edit_tree?(project, ref) end def leave_edit_message @@ -118,28 +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_rendered_as_text?(blob) - blob_text_viewable?(blob) && blob.to_partial_path(@project) == 'text' - end - - def blob_size(blob) - if blob.lfs_pointer? - blob.lfs_size - else - blob.size + def blob_raw_path + if @build && @entry + raw_project_job_artifacts_path(@project, @build, path: @entry.path) + elsif @snippet + if @snippet.project_id + raw_project_snippet_path(@project, @snippet) + else + raw_snippet_path(@snippet) + end + elsif @blob + project_raw_path(@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 @@ -221,13 +218,75 @@ module BlobHelper 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_content_button(blob) - return if markup?(blob.name) + 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_path, 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 :collapsed + "it is larger than #{number_to_human_size(viewer.collapse_limit)}" + when :too_large + "it is larger than #{number_to_human_size(viewer.size_limit)}" + 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 blob_render_error_options(viewer) + error = viewer.render_error + options = [] + + if error == :collapsed + options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, expanded: 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_path, target: '_blank', rel: 'noopener noreferrer') - clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard") + 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_project_issue_path(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", project_new_merge_request_path(project)) + end + + options end end diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index f43827da446..8b33c362a9c 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -3,12 +3,13 @@ module BoardsHelper board = @board || @boards.first { - endpoint: namespace_project_boards_path(@project.namespace, @project), + endpoint: project_boards_path(@project), board_id: board.id, disabled: "#{!can?(current_user, :admin_list, @project)}", - issue_link_base: namespace_project_issues_path(@project.namespace, @project), + issue_link_base: project_issues_path(@project), root_path: root_path, - bulk_update_path: bulk_update_namespace_project_issues_path(@project.namespace, @project), + bulk_update_path: bulk_update_project_issues_path(@project), + default_avatar: image_path(default_avatar) } end end diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index b7a28b1b4a7..686437fc99a 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 ProtectedBranch.protected?(project, 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], @@ -17,7 +7,7 @@ module BranchesHelper options = exist_opts.merge(options) - namespace_project_branches_path(@project.namespace, @project, @id, options) + project_branches_path(@project, @id, options) end def can_push_branch?(project, branch_name) diff --git a/app/helpers/breadcrumbs_helper.rb b/app/helpers/breadcrumbs_helper.rb new file mode 100644 index 00000000000..abe8edd6a8c --- /dev/null +++ b/app/helpers/breadcrumbs_helper.rb @@ -0,0 +1,25 @@ +module BreadcrumbsHelper + def add_to_breadcrumbs(text, link) + @breadcrumbs_extra_links ||= [] + @breadcrumbs_extra_links.push({ + text: text, + link: link + }) + end + + def breadcrumb_title_link + return @breadcrumb_link if @breadcrumb_link + + if controller.available_action?(:index) + url_for(action: "index") + else + request.path + end + end + + def breadcrumb_title(title) + return if defined?(@breadcrumb_title) + + @breadcrumb_title = title + end +end diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb index eb03ced67eb..0a15c29cfb5 100644 --- a/app/helpers/broadcast_messages_helper.rb +++ b/app/helpers/broadcast_messages_helper.rb @@ -1,5 +1,5 @@ module BroadcastMessagesHelper - def broadcast_message(message = BroadcastMessage.current) + def broadcast_message(message) return unless message.present? content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index 2fcb7a59fc3..85bc784d53c 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_job_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 @@ -8,8 +20,8 @@ module BuildsHelper def javascript_build_options { - page_url: namespace_project_build_url(@project.namespace, @project, @build), - build_url: namespace_project_build_url(@project.namespace, @project, @build, :json), + page_url: project_job_url(@project, @build), + build_url: project_job_url(@project, @build, :json), build_status: @build.status, build_stage: @build.stage, log_state: '' @@ -19,7 +31,7 @@ module BuildsHelper def build_failed_issue_options { title: "Build Failed ##{@build.id}", - description: namespace_project_build_url(@project.namespace, @project, @build) + description: project_job_url(@project, @build) } end end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index c85e96cf78d..bf9ad95b7c2 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -42,23 +42,33 @@ module ButtonHelper 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) klass = 'http-selector' - klass << ' has-tooltip' if current_user.try(:require_password?) + klass << ' has-tooltip' if current_user.try(:require_password_creation?) || current_user.try(:require_personal_access_token_creation_for_git_auth?) protocol = gitlab_config.protocol.upcase + tooltip_title = + if current_user.try(:require_password_creation?) + _("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol } + else + _("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol } + end + content_tag (append_link ? :a : :span), protocol, class: klass, - href: (project.http_url_to_repo(current_user) if append_link), + href: (project.http_url_to_repo if append_link), data: { html: true, placement: placement, container: 'body', - title: "Set a password on your account<br>to pull or push via #{protocol}" + title: tooltip_title } end @@ -73,7 +83,7 @@ module ButtonHelper html: true, placement: placement, container: 'body', - title: 'Add an SSH key to your profile<br>to pull or push via SSH.' + title: _('Add an SSH key to your profile to pull or push via SSH.') } end end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 32b1e7822af..8022547a6ad 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -8,7 +8,7 @@ module CiStatusHelper def ci_status_path(pipeline) project = pipeline.project - namespace_project_pipeline_path(project.namespace, project, pipeline) + project_pipeline_path(project, pipeline) end def ci_label_for_status(status) @@ -16,16 +16,18 @@ module CiStatusHelper return status.label end - case status - when 'success' - 'passed' - when 'success_with_warnings' - 'passed with warnings' - when 'manual' - 'waiting for manual action' - else - status - end + label = case status + when 'success' + 'passed' + when 'success_with_warnings' + 'passed with warnings' + when 'manual' + 'waiting for manual action' + else + status + end + translation = "CiStatusLabel|#{label}" + s_(translation) end def ci_text_for_status(status) @@ -35,13 +37,22 @@ module CiStatusHelper case status when 'success' - 'passed' + s_('CiStatusText|passed') when 'success_with_warnings' - 'passed' + s_('CiStatusText|passed') when 'manual' - 'blocked' + s_('CiStatusText|blocked') else - status + # All states are already being translated inside the detailed statuses: + # :running => Gitlab::Ci::Status::Running + # :skipped => Gitlab::Ci::Status::Skipped + # :failed => Gitlab::Ci::Status::Failed + # :success => Gitlab::Ci::Status::Success + # :canceled => Gitlab::Ci::Status::Canceled + # The following states are customized above: + # :manual => Gitlab::Ci::Status::Manual + status_translation = "CiStatusText|#{status}" + s_(status_translation) end end @@ -88,10 +99,7 @@ module CiStatusHelper def render_project_pipeline_status(pipeline_status, tooltip_placement: 'auto left') project = pipeline_status.project - path = pipelines_namespace_project_commit_path( - project.namespace, - project, - pipeline_status.sha) + path = pipelines_project_commit_path(project, pipeline_status.sha) render_status_with_link( 'commit', @@ -102,10 +110,7 @@ module CiStatusHelper def render_commit_status(commit, ref: nil, tooltip_placement: 'auto left') project = commit.project - path = pipelines_namespace_project_commit_path( - project.namespace, - project, - commit) + path = pipelines_project_commit_path(project, commit) render_status_with_link( 'commit', @@ -116,7 +121,7 @@ module CiStatusHelper def render_pipeline_status(pipeline, tooltip_placement: 'auto left') project = pipeline.project - path = namespace_project_pipeline_path(project.namespace, project, pipeline) + path = project_pipeline_path(project, pipeline) render_status_with_link('pipeline', pipeline.status, path, tooltip_placement: tooltip_placement) end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index cef624430da..72e26b64e60 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -15,16 +15,6 @@ module CommitsHelper commit_person_link(commit, options.merge(source: :committer)) end - def image_diff_class(diff) - if diff.deleted_file - "deleted" - elsif diff.new_file - "added" - else - nil - end - end - def commit_to_html(commit, ref, project) render 'projects/commits/commit', commit: commit, @@ -40,7 +30,7 @@ module CommitsHelper crumbs = content_tag(:li) do link_to( @project.path, - namespace_project_commits_path(@project.namespace, @project, @ref) + project_commits_path(@project, @ref) ) end @@ -52,8 +42,7 @@ module CommitsHelper # The text is just the individual part, but the link needs all the parts before it link_to( part, - namespace_project_commits_path( - @project.namespace, + project_commits_path( @project, tree_join(@ref, parts[0..i].join('/')) ) @@ -74,12 +63,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,39 +73,32 @@ 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), + _("Browse Files"), + project_tree_path(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, + _("Browse File"), + project_blob_path(project, tree_join(commit.id, @path)), class: "btn btn-default" ) elsif @path.present? return link_to( - "Browse Directory", - namespace_project_tree_path(project.namespace, project, + _("Browse Directory"), + project_tree_path(project, tree_join(commit.id, @path)), class: "btn btn-default" ) @@ -135,6 +113,10 @@ module CommitsHelper commit_action_link('cherry-pick', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip) end + def commit_signature_badge_classes(additional_classes) + %w(btn status-box gpg-status-box) + Array(additional_classes) + end + protected # Private: Returns a link to a person. If the person has a matching user and @@ -146,10 +128,10 @@ module CommitsHelper # avatar: true will prepend the avatar image # size: size of the avatar image in px def commit_person_link(commit, options = {}) - user = commit.send(options[:source]) + user = commit.public_send(options[:source]) # rubocop:disable GitlabSecurity/PublicSend - source_name = clean(commit.send "#{options[:source]}_name".to_sym) - source_email = clean(commit.send "#{options[:source]}_email".to_sym) + source_name = clean(commit.public_send(:"#{options[:source]}_name")) # rubocop:disable GitlabSecurity/PublicSend + source_email = clean(commit.public_send(:"#{options[:source]}_email")) # rubocop:disable GitlabSecurity/PublicSend person_name = user.try(:name) || source_name @@ -186,7 +168,7 @@ module CommitsHelper notice: "#{edit_in_new_fork_notice} Try to #{action} this commit again.", notice_now: edit_in_new_fork_notice_now } - fork_path = namespace_project_forks_path(@project.namespace, @project, + fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params) @@ -196,12 +178,12 @@ module CommitsHelper def view_file_button(commit_sha, diff_new_path, project) link_to( - namespace_project_blob_path(project.namespace, project, + project_blob_path(project, 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/compare_helper.rb b/app/helpers/compare_helper.rb index 2aa0449c46e..2c28dd81c87 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -9,8 +9,7 @@ module CompareHelper end def create_mr_path(from = params[:from], to = params[:to], project = @project) - new_namespace_project_merge_request_path( - project.namespace, + project_new_merge_request_path( project, merge_request: { source_branch: to, diff --git a/app/helpers/conversational_development_index_helper.rb b/app/helpers/conversational_development_index_helper.rb new file mode 100644 index 00000000000..1ff54415811 --- /dev/null +++ b/app/helpers/conversational_development_index_helper.rb @@ -0,0 +1,16 @@ +module ConversationalDevelopmentIndexHelper + def score_level(score) + if score < 33.33 + 'low' + elsif score < 66.66 + 'average' + else + 'high' + end + end + + def format_score(score) + precision = score < 1 ? 2 : 1 + number_with_precision(score, precision: precision) + end +end diff --git a/app/helpers/defer_script_tag_helper.rb b/app/helpers/defer_script_tag_helper.rb new file mode 100644 index 00000000000..e1567556e5e --- /dev/null +++ b/app/helpers/defer_script_tag_helper.rb @@ -0,0 +1,6 @@ +module DeferScriptTagHelper + # Override the default ActionView `javascript_include_tag` helper to support page specific deferred loading + def javascript_include_tag(*sources) + super(*sources, defer: true) + end +end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index dc144906548..28f591a4e22 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -8,24 +8,24 @@ module DiffHelper [marked_old_line, marked_new_line] end - def expand_all_diffs? - params[:expand_all_diffs].present? + def diffs_expanded? + params[:expanded].present? end def diff_view @diff_view ||= begin diff_views = %w(inline parallel) - diff_view = cookies[:diff_view] + diff_view = params[:view] || cookies[:diff_view] diff_view = diff_views.first unless diff_views.include?(diff_view) diff_view.to_sym end end def diff_options - options = { ignore_whitespace_change: hide_whitespace?, no_collapse: expand_all_diffs? } + options = { ignore_whitespace_change: hide_whitespace?, expanded: diffs_expanded? } if action_name == 'diff_for_path' - options[:no_collapse] = true + options[:expanded] = true options[:paths] = params.values_at(:old_path, :new_path) end @@ -63,15 +63,15 @@ module DiffHelper def parallel_diff_discussions(left, right, diff_file) return unless @grouped_diff_discussions - + discussions_left = discussions_right = nil - if left && (left.unchanged? || left.removed?) + if left && left.discussable? && (left.unchanged? || left.removed?) line_code = diff_file.line_code(left) discussions_left = @grouped_diff_discussions[line_code] end - if right && right.added? + if right && right.discussable? && right.added? line_code = diff_file.line_code(right) discussions_right = @grouped_diff_discussions[line_code] end @@ -88,40 +88,82 @@ module DiffHelper end def submodule_link(blob, ref, repository = @repository) - tree, commit = submodule_links(blob, ref, repository) - commit_id = if commit.nil? + project_url, tree_url = submodule_links(blob, ref, repository) + commit_id = if tree_url.nil? Commit.truncate_sha(blob.id) else - link_to Commit.truncate_sha(blob.id), commit + link_to Commit.truncate_sha(blob.id), tree_url end [ - content_tag(:span, link_to(truncate(blob.name, length: 40), tree)), + content_tag(:span, link_to(truncate(blob.name, length: 40), project_url)), '@', - content_tag(:span, commit_id, class: 'monospace'), + content_tag(:span, commit_id, class: 'commit-sha') ].join(' ').html_safe end - def commit_for_diff(diff_file) - return diff_file.content_commit if diff_file.content_commit + def diff_file_blob_raw_path(diff_file) + project_raw_path(@project, tree_join(diff_file.content_sha, diff_file.file_path)) + end - if diff_file.deleted_file - @base_commit || @commit.parent || @commit - else - @commit - end + def diff_file_old_blob_raw_path(diff_file) + sha = diff_file.old_content_sha + return unless sha + project_raw_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path)) end def diff_file_html_data(project, diff_file_path, diff_commit_id) { - blob_diff_path: namespace_project_blob_diff_path(project.namespace, project, + blob_diff_path: project_blob_diff_path(project, tree_join(diff_commit_id, diff_file_path)), view: diff_view } end - def editable_diff?(diff) - !diff.deleted_file && @merge_request && @merge_request.source_project + def editable_diff?(diff_file) + !diff_file.deleted_file? && @merge_request && @merge_request.source_project + end + + def diff_render_error_reason(viewer) + case viewer.render_error + when :too_large + "it is too large" + when :server_side_but_stored_externally + case viewer.diff_file.external_storage + when :lfs + 'it is stored in LFS' + else + 'it is stored externally' + end + end + end + + def diff_render_error_options(viewer) + diff_file = viewer.diff_file + options = [] + + blob_url = project_blob_path(@project, tree_join(diff_file.content_sha, diff_file.file_path)) + options << link_to('view the blob', blob_url) + + options + end + + def diff_file_changed_icon(diff_file) + if diff_file.deleted_file? || diff_file.renamed_file? + "minus" + elsif diff_file.new_file? + "plus" + else + "adjust" + end + end + + def diff_file_changed_icon_color(diff_file) + if diff_file.deleted_file? + "cred" + elsif diff_file.new_file? + "cgreen" + end end private @@ -139,17 +181,17 @@ module DiffHelper end def commit_diff_whitespace_link(project, commit, options) - url = namespace_project_commit_path(project.namespace, project, commit.id, params_with_whitespace) + url = project_commit_path(project, commit.id, params_with_whitespace) toggle_whitespace_link(url, options) end def diff_merge_request_whitespace_link(project, merge_request, options) - url = diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, params_with_whitespace) + url = diffs_project_merge_request_path(project, merge_request, params_with_whitespace) toggle_whitespace_link(url, options) end def diff_compare_whitespace_link(project, from, to, options) - url = namespace_project_compare_path(project.namespace, project, from, to, params_with_whitespace) + url = project_compare_path(project, from, to, params_with_whitespace) toggle_whitespace_link(url, options) end diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 8ed99642c7a..ff305fa39b4 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -1,27 +1,27 @@ module DropdownsHelper def dropdown_tag(toggle_text, options: {}, &block) - content_tag :div, class: "dropdown #{options[:wrapper_class] if options.has_key?(:wrapper_class)}" do + content_tag :div, class: "dropdown #{options[:wrapper_class] if options.key?(:wrapper_class)}" do data_attr = { toggle: "dropdown" } - if options.has_key?(:data) + if options.key?(:data) data_attr = options[:data].merge(data_attr) end dropdown_output = dropdown_toggle(toggle_text, data_attr, options) - dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.has_key?(:dropdown_class)}") do + dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.key?(:dropdown_class)}") do output = "" - if options.has_key?(:title) + if options.key?(:title) output << dropdown_title(options[:title]) end - if options.has_key?(:filter) + if options.key?(:filter) output << dropdown_filter(options[:placeholder]) end - 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) + output << content_tag(:div, class: "dropdown-content #{options[:content_class] if options.key?(:content_class)}") do + capture(&block) if block && !options.key?(:footer_content) end if block && options[:footer_content] @@ -41,18 +41,18 @@ module DropdownsHelper def dropdown_toggle(toggle_text, data_attr, options = {}) default_label = data_attr[:default_label] - content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do + content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.key?(:toggle_class)}", id: (options[:id] if options.key?(:id)), type: "button", data: data_attr) do output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}") output << icon('chevron-down') output.html_safe end end - def dropdown_title(title, back: false) + def dropdown_title(title, options: {}) content_tag :div, class: "dropdown-title" do title_output = "" - if back + if options.fetch(:back, false) title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do icon('arrow-left') end @@ -60,14 +60,25 @@ module DropdownsHelper title_output << content_tag(:span, title) - title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do - icon('times', class: 'dropdown-menu-close-icon') + if options.fetch(:close, true) + title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do + icon('times', class: 'dropdown-menu-close-icon') + end end title_output.html_safe end end + def dropdown_input(placeholder, input_id: nil) + content_tag :div, class: "dropdown-input" do + filter_output = text_field_tag input_id, nil, class: "dropdown-input-field dropdown-no-filter", placeholder: placeholder, autocomplete: 'off' + filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button") + + filter_output.html_safe + end + end + def dropdown_filter(placeholder, search_id: nil) content_tag :div, class: "dropdown-input" do filter_output = search_field_tag search_id, nil, class: "dropdown-input-field", placeholder: placeholder, autocomplete: 'off' diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index f927cfc998f..5f11fe62030 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 } } @@ -61,9 +61,22 @@ module EmailsHelper else image_tag( image_url('mailers/gitlab_header_logo.gif'), - size: "55x50", - alt: "GitLab" + size: '55x50', + alt: 'GitLab' ) end end + + def email_default_heading(text) + content_tag :h1, text, style: [ + "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif", + 'color:#333333', + 'font-size:18px', + 'font-weight:400', + 'line-height:1.4', + 'padding:0', + 'margin:0', + 'text-align:center' + ].join(';') + end end diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb index ff8550439d0..1e78a189c08 100644 --- a/app/helpers/environment_helper.rb +++ b/app/helpers/environment_helper.rb @@ -8,7 +8,7 @@ module EnvironmentHelper def environment_link_for_build(project, build) environment = environment_for_build(project, build) if environment - link_to environment.name, namespace_project_environment_path(project.namespace, project, environment) + link_to environment.name, project_environment_path(project, environment) else content_tag :span, build.expanded_environment_name end diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index 515e802e01e..4ce89f89fa9 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -1,7 +1,7 @@ module EnvironmentsHelper def environments_list_data { - endpoint: namespace_project_environments_path(@project.namespace, @project, format: :json) + endpoint: project_environments_path(@project, format: :json) } end end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 5f5c76d3722..48c87dca217 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -10,11 +10,12 @@ module EventsHelper 'deleted' => 'icon_trash_o' }.freeze - def link_to_author(event) + 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 @@ -40,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 @@ -98,13 +99,12 @@ module EventsHelper def event_feed_url(event) if event.issue? - namespace_project_issue_url(event.project.namespace, event.project, + project_issue_url(event.project, event.issue) elsif event.merge_request? - namespace_project_merge_request_url(event.project.namespace, - event.project, event.merge_request) + project_merge_request_url(event.project, event.merge_request) elsif event.commit_note? - namespace_project_commit_url(event.project.namespace, event.project, + project_commit_url(event.project, event.note_target) elsif event.note? if event.note_target @@ -118,15 +118,15 @@ module EventsHelper def push_event_feed_url(event) if event.push_with_commits? && event.md_ref? if event.commits_count > 1 - namespace_project_compare_url(event.project.namespace, event.project, + project_compare_url(event.project, from: event.commit_from, to: event.commit_to) else - namespace_project_commit_url(event.project.namespace, event.project, + project_commit_url(event.project, id: event.commit_to) end else - namespace_project_commits_url(event.project.namespace, event.project, + project_commits_url(event.project, event.ref_name) end end @@ -145,15 +145,9 @@ module EventsHelper def event_note_target_path(event) if event.commit_note? - namespace_project_commit_path(event.project.namespace, - event.project, - event.note_target, - anchor: dom_id(event.target)) + project_commit_path(event.project, event.note_target, anchor: dom_id(event.target)) elsif event.project_snippet_note? - namespace_project_snippet_path(event.project.namespace, - event.project, - event.note_target, - anchor: dom_id(event.target)) + project_snippet_path(event.project, event.note_target, anchor: dom_id(event.target)) else polymorphic_path([event.project.namespace.becomes(Namespace), event.project, event.note_target], @@ -163,9 +157,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 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/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb index defd87d6bbe..8cf890b74a8 100644 --- a/app/helpers/external_wiki_helper.rb +++ b/app/helpers/external_wiki_helper.rb @@ -4,7 +4,7 @@ module ExternalWikiHelper if external_wiki_service external_wiki_service.properties['external_wiki_url'] else - namespace_project_wiki_path(project.namespace, project, :home) + project_wiki_path(project, :home) end end end diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index 1182939f656..9247b1f72de 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -8,11 +8,35 @@ module FormHelper content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do content_tag(:h4, headline) << content_tag(:ul) do - model.errors.full_messages. - map { |msg| content_tag(:li, msg) }. - join. - html_safe + model.errors.full_messages + .map { |msg| content_tag(:li, msg) } + .join + .html_safe end end end + + def issue_assignees_dropdown_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: @project.id, + field_name: 'issue[assignee_ids][]', + default_label: 'Unassigned', + '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]) + } + } + end end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index e9b7cbbad6a..d4a91e533c1 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -1,132 +1,89 @@ -# Shorter routing method for project and project items -# Since update to rails 4.1.9 we are now allowed to use `/` in project routing -# so we use nested routing for project resources which include project and -# project namespace. To avoid writing long methods every time we define shortcuts for -# some of routing. -# -# For example instead of this: -# -# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) -# -# We can simply use shortcut: -# -# merge_request_path(merge_request) -# +# Shorter routing method for some project items module GitlabRoutingHelper - # Project - def project_path(project, *args) - namespace_project_path(project.namespace, project, *args) - end - - def project_url(project, *args) - namespace_project_url(project.namespace, project, *args) - end + extend ActiveSupport::Concern - def edit_project_path(project, *args) - edit_namespace_project_path(project.namespace, project, *args) + included do + Gitlab::Routing.includes_helpers(self) end - def edit_project_url(project, *args) - edit_namespace_project_url(project.namespace, project, *args) - end - - def project_files_path(project, *args) - namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref) - end - - def project_commits_path(project, *args) - namespace_project_commits_path(project.namespace, project, @ref || project.repository.root_ref) - end - - def project_pipelines_path(project, *args) - namespace_project_pipelines_path(project.namespace, project, *args) - end - - def project_environments_path(project, *args) - namespace_project_environments_path(project.namespace, project, *args) - end - - def project_cycle_analytics_path(project, *args) - namespace_project_cycle_analytics_path(project.namespace, project, *args) - end - - def project_builds_path(project, *args) - namespace_project_builds_path(project.namespace, project, *args) + # Project + def project_tree_path(project, ref = nil, *args) + namespace_project_tree_path(project.namespace, project, ref || @ref || project.repository.root_ref, *args) # rubocop:disable Cop/ProjectPathHelper end - def project_container_registry_path(project, *args) - namespace_project_container_registry_index_path(project.namespace, project, *args) + def project_commits_path(project, ref = nil, *args) + namespace_project_commits_path(project.namespace, project, ref || @ref || project.repository.root_ref, *args) # rubocop:disable Cop/ProjectPathHelper end - def activity_project_path(project, *args) - activity_namespace_project_path(project.namespace, project, *args) + def project_ref_path(project, ref_name, *args) + project_commits_path(project, ref_name, *args) end def runners_path(project, *args) - namespace_project_runners_path(project.namespace, project, *args) + project_runners_path(project, *args) end def runner_path(runner, *args) - namespace_project_runner_path(@project.namespace, @project, runner, *args) + project_runner_path(@project, runner, *args) end def environment_path(environment, *args) - namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args) + project_environment_path(environment.project, environment, *args) end def environment_metrics_path(environment, *args) - metrics_namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args) + metrics_project_environment_path(environment.project, environment, *args) end def issue_path(entity, *args) - namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args) + project_issue_path(entity.project, entity, *args) end def merge_request_path(entity, *args) - namespace_project_merge_request_path(entity.project.namespace, entity.project, entity, *args) + project_merge_request_path(entity.project, entity, *args) end def pipeline_path(pipeline, *args) - namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, *args) - end - - def milestone_path(entity, *args) - namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args) + project_pipeline_path(pipeline.project, pipeline.id, *args) end def issue_url(entity, *args) - namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args) + project_issue_url(entity.project, entity, *args) end def merge_request_url(entity, *args) - namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args) + project_merge_request_url(entity.project, entity, *args) end def pipeline_url(pipeline, *args) - namespace_project_pipeline_url(pipeline.project.namespace, pipeline.project, pipeline.id, *args) + project_pipeline_url(pipeline.project, pipeline.id, *args) end - def pipeline_build_url(pipeline, build, *args) - namespace_project_build_url(pipeline.project.namespace, pipeline.project, build.id, *args) + def pipeline_job_url(pipeline, build, *args) + project_job_url(pipeline.project, build.id, *args) end def commits_url(entity, *args) - namespace_project_commits_url(entity.project.namespace, entity.project, entity.ref, *args) + project_commits_url(entity.project, entity.ref, *args) end def commit_url(entity, *args) - namespace_project_commit_url(entity.project.namespace, entity.project, entity.sha, *args) + project_commit_url(entity.project, entity.sha, *args) end - def project_snippet_url(entity, *args) - namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) + def preview_markdown_path(project, *args) + if @snippet.is_a?(PersonalSnippet) + preview_markdown_snippets_path + else + preview_markdown_project_path(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) + toggle_subscription_project_issue_path(entity.project, entity) else - toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity) + toggle_subscription_project_merge_request_path(entity.project, entity) end end @@ -140,32 +97,27 @@ module GitlabRoutingHelper ## Members def project_members_url(project, *args) - namespace_project_project_members_url(project.namespace, project) + project_project_members_url(project, *args) end def project_member_path(project_member, *args) - namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) + project_project_member_path(project_member.source, project_member) end def request_access_project_members_path(project, *args) - request_access_namespace_project_project_members_path(project.namespace, project) + request_access_project_project_members_path(project) end def leave_project_members_path(project, *args) - leave_namespace_project_project_members_path(project.namespace, project) + leave_project_project_members_path(project) end def approve_access_request_project_member_path(project_member, *args) - approve_access_request_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) + approve_access_request_project_project_member_path(project_member.source, project_member) end def resend_invite_project_member_path(project_member, *args) - resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) - end - - # Snippets - def personal_snippet_url(snippet, *args) - snippet_url(snippet) + resend_invite_project_project_member_path(project_member.source, project_member) end # Groups @@ -199,28 +151,37 @@ module GitlabRoutingHelper def artifacts_action_path(path, project, build) action, path_params = path.split('/', 2) - args = [project.namespace, project, build, path_params] + args = [project, build, path_params] case action when 'download' - download_namespace_project_build_artifacts_path(*args) + download_project_job_artifacts_path(*args) when 'browse' - browse_namespace_project_build_artifacts_path(*args) + browse_project_job_artifacts_path(*args) when 'file' - file_namespace_project_build_artifacts_path(*args) + file_project_job_artifacts_path(*args) + when 'raw' + raw_project_job_artifacts_path(*args) end end - # Settings - def project_settings_integrations_path(project, *args) - namespace_project_settings_integrations_path(project.namespace, project, *args) + # Pipeline Schedules + def pipeline_schedules_path(project, *args) + project_pipeline_schedules_path(project, *args) + end + + def pipeline_schedule_path(schedule, *args) + project = schedule.project + project_pipeline_schedule_path(project, schedule, *args) end - def project_settings_members_path(project, *args) - namespace_project_settings_members_path(project.namespace, project, *args) + def edit_pipeline_schedule_path(schedule) + project = schedule.project + edit_project_pipeline_schedule_path(project, schedule) end - def project_settings_ci_cd_path(project, *args) - namespace_project_settings_ci_cd_path(project.namespace, project, *args) + def take_ownership_pipeline_schedule_path(schedule, *args) + project = schedule.project + take_ownership_project_pipeline_schedule_path(project, schedule, *args) end end diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb index c2ab80f2e0d..c53ea4519da 100644 --- a/app/helpers/graph_helper.rb +++ b/app/helpers/graph_helper.rb @@ -3,7 +3,7 @@ module GraphHelper refs = "" # Commit::ref_names already strips the refs/XXX from important refs (e.g. refs/heads/XXX) # so anything leftover is internally used by GitLab - commit_refs = commit.ref_names(repo).reject{ |name| name.starts_with?('refs/') } + commit_refs = commit.ref_names(repo).reject { |name| name.starts_with?('refs/') } refs << commit_refs.join(' ') # append note count @@ -17,13 +17,10 @@ module GraphHelper ids.zip(parent_spaces) end - def success_ratio(success_builds, failed_builds) - failed_builds = failed_builds.count(:all) - success_builds = success_builds.count(:all) + def success_ratio(counts) + return 100 if counts[:failed].zero? - return 100 if failed_builds.zero? - - ratio = (success_builds.to_f / (success_builds + failed_builds)) * 100 + ratio = (counts[:success].to_f / (counts[:success] + counts[:failed])) * 100 ratio.to_i end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index a6014088e92..4123a96911f 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -8,19 +8,20 @@ module GroupsHelper group = Group.find_by_full_path(group) end - group.try(:avatar_url) || image_path('no_group_avatar.png') + group.try(:avatar_url) || ActionController::Base.helpers.image_path('no_group_avatar.png') end def group_title(group, name = nil, url = nil) @has_group_title = true full_title = '' - group.ancestors.each do |parent| - full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable') + group.ancestors.reverse.each do |parent| + full_title += group_title_link(parent, hidable: true) + full_title += '<span class="hidable"> / </span>'.html_safe end - full_title += link_to(simple_sanitize(group.name), group_path(group), class: 'group-path') + full_title += group_title_link(group) full_title += ' · '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name content_tag :span, class: 'group-title' do @@ -56,4 +57,25 @@ module GroupsHelper def group_issues(group) IssuesFinder.new(current_user, group_id: group.id).execute end + + def remove_group_message(group) + _("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") % + { group_name: group.name } + end + + private + + def group_title_link(group, hidable: false) + link_to(group_path(group), class: "group-path #{'hidable' if hidable}") do + output = + if show_new_nav? + image_tag(group_icon(group), class: "avatar-tile", width: 16, height: 16) + else + "" + end + + output << simple_sanitize(group.name) + output.html_safe + end + end end diff --git a/app/helpers/hooks_helper.rb b/app/helpers/hooks_helper.rb new file mode 100644 index 00000000000..551b9cca6b1 --- /dev/null +++ b/app/helpers/hooks_helper.rb @@ -0,0 +1,17 @@ +module HooksHelper + def link_to_test_hook(hook, trigger) + path = case hook + when ProjectHook + project = hook.project + test_project_hook_path(project, hook, trigger: trigger) + when SystemHook + test_admin_hook_path(hook, trigger: trigger) + end + + trigger_human_name = trigger.to_s.tr('_', ' ').camelize + + link_to path, rel: 'nofollow' do + content_tag(:span, trigger_human_name) + end + end +end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 55fa81e95ef..9a404832423 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -1,4 +1,5 @@ module IconsHelper + extend self include FontAwesome::Rails::IconHelper # Creates an icon tag given icon name(s) and possible icon modifiers. @@ -7,9 +8,10 @@ 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]).empty? - # Add `aria-hidden` if there are no aria's set + 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) @@ -19,6 +21,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/import_helper.rb b/app/helpers/import_helper.rb index a57b5a8fea5..a18ebfb6030 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -5,7 +5,7 @@ module ImportHelper end def provider_project_link(provider, path_with_namespace) - url = __send__("#{provider}_project_url", path_with_namespace) + url = __send__("#{provider}_project_url", path_with_namespace) # rubocop:disable GitlabSecurity/PublicSend link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer' end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 0b13dbf5f8d..197c90c4081 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -26,9 +26,9 @@ module IssuablesHelper project = issuable.project if issuable.is_a?(MergeRequest) - namespace_project_merge_request_path(project.namespace, project, issuable.iid, :json) + project_merge_request_path(project, issuable.iid, :json) else - namespace_project_issue_path(project.namespace, project, issuable.iid, :json) + project_issue_path(project, issuable.iid, :json) end end @@ -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 if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm") + output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg") output end @@ -139,7 +151,7 @@ module IssuablesHelper end def issuable_labels_tooltip(labels, limit: 5) - first, last = labels.partition.with_index{ |_, i| i < limit } + first, last = labels.partition.with_index { |_, i| i < limit } label_names = first.collect(&:name) label_names << "and #{last.size} more" unless last.empty? @@ -153,11 +165,7 @@ module IssuablesHelper } state_title = titles[state] || state.to_s.humanize - - count = - Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do - issuables_count_for_state(issuable_type, state) - end + count = issuables_count_for_state(issuable_type, state) html = content_tag(:span, state_title) html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge') @@ -166,7 +174,14 @@ module IssuablesHelper end def assigned_issuables_count(issuable_type) - current_user.public_send("assigned_open_#{issuable_type}_count") + 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 end def issuable_filter_params @@ -187,40 +202,115 @@ module IssuablesHelper issuable_filter_params.any? { |k| params.key?(k) } end - private + def issuable_initial_data(issuable) + data = { + endpoint: project_issue_path(@project, issuable), + canUpdate: can?(current_user, :update_issue, issuable), + canDestroy: can?(current_user, :destroy_issue, issuable), + canMove: current_user ? issuable.can_move?(current_user) : false, + issuableRef: issuable.to_reference, + isConfidential: issuable.confidential, + markdownPreviewUrl: preview_markdown_path(@project), + markdownDocs: help_page_path('user/markdown'), + projectsAutocompleteUrl: autocomplete_projects_path(project_id: @project.id), + issuableTemplates: issuable_templates(issuable), + projectPath: ref_project.path, + projectNamespace: ref_project.namespace.full_path, + initialTitleHtml: markdown_field(issuable, :title), + initialTitleText: issuable.title, + initialDescriptionHtml: markdown_field(issuable, :description), + initialDescriptionText: issuable.description, + initialTaskStatus: issuable.task_status + } - def sidebar_gutter_collapsed? - cookies[:collapsed_gutter] == 'true' + data.merge!(updated_at_by(issuable)) + + data.to_json end - def base_issuable_scope(issuable) - issuable.project.send(issuable.class.table_name).send(issuable_state_scope(issuable)) + def updated_at_by(issuable) + return {} unless issuable.is_edited? + + { + updatedAt: issuable.updated_at.to_time.iso8601, + updatedBy: { + name: issuable.last_edited_by.name, + path: user_path(issuable.last_edited_by) + } + } end - def issuable_state_scope(issuable) - if issuable.respond_to?(:merged?) && issuable.merged? - :merged - else - issuable.open? ? :opened : :closed + def issuables_count_for_state(issuable_type, state, finder: nil) + finder ||= public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend + cache_key = finder.state_counter_cache_key + + @counts ||= {} + @counts[cache_key] ||= Rails.cache.fetch(cache_key, expires_in: 2.minutes) do + finder.count_by_state end + + @counts[cache_key][state] end - def issuables_count_for_state(issuable_type, state) - @counts ||= {} - @counts[issuable_type] ||= public_send("#{issuable_type}_finder").count_by_state - @counts[issuable_type][state] + def close_issuable_url(issuable) + issuable_url(issuable, close_reopen_params(issuable, :close)) + end + + def reopen_issuable_url(issuable) + issuable_url(issuable, close_reopen_params(issuable, :reopen)) + end + + def close_reopen_issuable_url(issuable, should_inverse = false) + issuable.closed? ^ should_inverse ? reopen_issuable_url(issuable) : close_issuable_url(issuable) + end + + def issuable_url(issuable, *options) + case issuable + when Issue + issue_url(issuable, *options) + when MergeRequest + merge_request_url(issuable, *options) + end + end + + def issuable_button_visibility(issuable, closed) + case issuable + when Issue + issue_button_visibility(issuable, closed) + when MergeRequest + merge_request_button_visibility(issuable, closed) + end + end + + def issuable_close_reopen_button_method(issuable) + case issuable + when Issue + '' + when MergeRequest + 'put' + end end - IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze - private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY + def issuable_author_is_current_user(issuable) + issuable.author == current_user + end + + def issuable_display_type(issuable) + issuable.model_name.human.downcase + end + + private - def issuables_state_counter_cache_key(issuable_type, state) - opts = params.with_indifferent_access - opts[:state] = state - opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY) - opts.delete_if { |_, value| value.blank? } + def sidebar_gutter_collapsed? + cookies[:collapsed_gutter] == 'true' + end - hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-')) + def issuable_state_scope(issuable) + if issuable.respond_to?(:merged?) && issuable.merged? + :merged + else + issuable.open? ? :opened : :closed + end end def issuable_templates(issuable) @@ -230,8 +320,6 @@ module IssuablesHelper issue_template_names when MergeRequest merge_request_template_names - else - raise 'Unknown issuable type!' end end @@ -244,7 +332,7 @@ module IssuablesHelper end def selected_template(issuable) - params[:issuable_template] if issuable_templates(issuable).any?{ |template| template[:name] == params[:issuable_template] } + params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] } end def issuable_todo_button_data(issuable, todo, is_collapsed) @@ -255,10 +343,28 @@ module IssuablesHelper mark_icon: (is_collapsed ? icon('check-square', class: 'todo-undone') : nil), issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, - url: namespace_project_todos_path(@project.namespace, @project), + url: project_todos_path(@project), delete_path: (dashboard_todo_path(todo) if todo), placement: (is_collapsed ? 'left' : nil), container: (is_collapsed ? 'body' : nil) } end + + def close_reopen_params(issuable, action) + { + issuable.model_name.to_s.underscore => { state_event: action } + }.tap do |params| + params[:format] = :json if issuable.is_a?(Issue) + end + end + + def issuable_sidebar_options(issuable, can_edit_issuable) + { + endpoint: "#{issuable_json_path(issuable)}?basic=true", + editable: can_edit_issuable, + currentUser: current_user.as_json(only: [:username, :id, :name], methods: :avatar_url), + rootPath: root_path, + fullPath: @project.full_path + } + end end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 82288f1da35..7e1ccb23e9e 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -17,10 +17,10 @@ module IssuesHelper return '' if project.nil? url = - if options[:only_path] - project.issues_tracker.issue_path(issue_iid) + if options[:internal] + url_for_internal_issue(issue_iid, project, options) else - project.issues_tracker.issue_url(issue_iid) + url_for_tracker_issue(issue_iid, project, options) end # Ensure we return a valid URL to prevent possible XSS. @@ -29,6 +29,24 @@ module IssuesHelper '' end + def url_for_tracker_issue(issue_iid, project, options) + if options[:only_path] + project.issues_tracker.issue_path(issue_iid) + else + project.issues_tracker.issue_url(issue_iid) + end + end + + def url_for_internal_issue(issue_iid, project = @project, options = {}) + helpers = Gitlab::Routing.url_helpers + + if options[:only_path] + helpers.namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: issue_iid) + else + helpers.namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: issue_iid) + end + end + def bulk_update_milestone_options milestones = @project.milestones.active.reorder(due_date: :asc, title: :asc).to_a milestones.unshift(Milestone::None) @@ -150,7 +168,7 @@ module IssuesHelper Gitlab::UrlBuilder.build(single_discussion.first_note) else project = merge_request.project - namespace_project_merge_request_path(project.namespace, project, merge_request) + project_merge_request_path(project, merge_request) end link_to link_text, path @@ -158,4 +176,6 @@ module IssuesHelper # Required for Banzai::Filter::IssueReferenceFilter module_function :url_for_issue + module_function :url_for_internal_issue + module_function :url_for_tracker_issue end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index e5b1e6e8bc7..e60513b35c7 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -43,11 +43,11 @@ module LabelsHelper def label_filter_path(subject, label, type: :issue) case subject when Group - send("#{type.to_s.pluralize}_group_path", + send("#{type.to_s.pluralize}_group_path", # rubocop:disable GitlabSecurity/PublicSend subject, label_name: [label.name]) when Project - send("namespace_project_#{type.to_s.pluralize}_path", + send("namespace_project_#{type.to_s.pluralize}_path", # rubocop:disable GitlabSecurity/PublicSend subject.namespace, subject, label_name: [label.name]) @@ -57,25 +57,24 @@ module LabelsHelper def edit_label_path(label) case label when GroupLabel then edit_group_label_path(label.group, label) - when ProjectLabel then edit_namespace_project_label_path(label.project.namespace, label.project, label) + when ProjectLabel then edit_project_label_path(label.project, label) end end def destroy_label_path(label) case label when GroupLabel then group_label_path(label.group, label) - when ProjectLabel then namespace_project_label_path(label.project.namespace, label.project, label) + when ProjectLabel then project_label_path(label.project, label) end end def render_colored_label(label, label_suffix = '', tooltip: true) - label_color = label.color || Label::DEFAULT_COLOR - text_color = text_color_for_bg(label_color) + text_color = text_color_for_bg(label.color) # Intentionally not using content_tag here so that this method can be called # by LabelReferenceFilter span = %(<span class="label color-label #{"has-tooltip" if tooltip}" ) + - %(style="background-color: #{label_color}; color: #{text_color}" ) + + %(style="background-color: #{label.color}; color: #{text_color}" ) + %(title="#{escape_once(label.description)}" data-container="body">) + %(#{escape_once(label.name)}#{label_suffix}</span>) @@ -128,27 +127,34 @@ module LabelsHelper project = @target_project || @project if project - namespace_project_labels_path(project.namespace, project, :json) + project_labels_path(project, :json) else dashboard_labels_path(:json) end end + def can_subscribe_to_label_in_different_levels?(label) + defined?(@project) && label.is_a?(GroupLabel) + end + def label_subscription_status(label, project) - return 'project-level' if label.subscribed?(current_user, project) return 'group-level' if label.subscribed?(current_user) + return 'project-level' if label.subscribed?(current_user, project) 'unsubscribed' end - def group_label_unsubscribe_path(label, project) + def toggle_subscription_label_path(label, project) + return toggle_subscription_group_label_path(label.group, label) unless project + case label_subscription_status(label, project) - when 'project-level' then toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) when 'group-level' then toggle_subscription_group_label_path(label.group, label) + when 'project-level' then toggle_subscription_project_label_path(project, label) + when 'unsubscribed' then toggle_subscription_project_label_path(project, label) end end - def label_subscription_toggle_button_text(label, project) + def label_subscription_toggle_button_text(label, project = nil) label.subscribed?(current_user, project) ? 'Unsubscribe' : 'Subscribe' end diff --git a/app/helpers/lazy_image_tag_helper.rb b/app/helpers/lazy_image_tag_helper.rb new file mode 100644 index 00000000000..2c5619ac41b --- /dev/null +++ b/app/helpers/lazy_image_tag_helper.rb @@ -0,0 +1,24 @@ +module LazyImageTagHelper + def placeholder_image + "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" + end + + # Override the default ActionView `image_tag` helper to support lazy-loading + def image_tag(source, options = {}) + options = options.symbolize_keys + + unless options.delete(:lazy) == false + options[:data] ||= {} + options[:data][:src] = path_to_image(source) + options[:class] ||= "" + options[:class] << " lazy" + + source = placeholder_image + end + + super(source, options) + end + + # Required for Banzai::Filter::ImageLazyLoadFilter + module_function :placeholder_image +end diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index 0781874d7fc..941cfce8370 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -1,6 +1,9 @@ require 'nokogiri' module MarkupHelper + include ActionView::Helpers::TagHelper + include ActionView::Context + def plain?(filename) Gitlab::MarkupHelper.plain?(filename) end @@ -32,7 +35,7 @@ module MarkupHelper context = { project: @project, current_user: (current_user if defined?(current_user)), - pipeline: :single_line, + pipeline: :single_line } gfm_body = Banzai.render(body, context) @@ -74,7 +77,7 @@ module MarkupHelper context[:project] ||= @project html = markdown_unsafe(text, context) - banzai_postprocess(html, context) + prepare_for_rendering(html, context) end def markdown_field(object, field) @@ -82,13 +85,13 @@ module MarkupHelper 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 markup(file_name, text, context = {}) context[:project] ||= @project html = context.delete(:rendered) || markup_unsafe(file_name, text, context) - banzai_postprocess(html, context) + prepare_for_rendering(html, context) end def render_wiki_content(wiki_page) @@ -107,22 +110,22 @@ module MarkupHelper wiki_page.formatted_content.html_safe end - banzai_postprocess(html, context) + prepare_for_rendering(html, context) end def markup_unsafe(file_name, text, context = {}) return '' unless text.present? if gitlab_markdown?(file_name) - Hamlit::RailsHelpers.preserve(markdown_unsafe(text, context)) + markdown_unsafe(text, context) elsif asciidoc?(file_name) - asciidoc_unsafe(text) + asciidoc_unsafe(text, context) elsif plain?(file_name) content_tag :pre, class: 'plain-readme' do text end else - other_markup_unsafe(file_name, text) + other_markup_unsafe(file_name, text, context) end rescue RuntimeError simple_format(text) @@ -217,16 +220,15 @@ module MarkupHelper Banzai.render(text, context) end - def asciidoc_unsafe(text) - Gitlab::Asciidoc.render(text) + def asciidoc_unsafe(text, context = {}) + Gitlab::Asciidoc.render(text, context) end - def other_markup_unsafe(file_name, text) - Gitlab::OtherMarkup.render(file_name, text) + def other_markup_unsafe(file_name, text, context = {}) + Gitlab::OtherMarkup.render(file_name, text, context) end - # Calls Banzai.post_process with some common context options - def banzai_postprocess(html, context = {}) + def prepare_for_rendering(html, context = {}) return '' unless html.present? context.merge!( @@ -239,7 +241,9 @@ module MarkupHelper requested_path: @path ) - Banzai.post_process(html, context) + html = Banzai.post_process(html, context) + + Hamlit::RailsHelpers.preserve(html) end extend self diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index e347f61fb8d..c31023f2d9a 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -1,8 +1,7 @@ module MergeRequestsHelper def new_mr_path_from_push_event(event) - target_project = event.project.forked_from_project || event.project - new_namespace_project_merge_request_path( - event.project.namespace, + target_project = event.project.default_merge_request_target + project_new_merge_request_path( event.project, new_mr_from_push_event(event, target_project) ) @@ -19,14 +18,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? @@ -49,71 +40,25 @@ module MergeRequestsHelper def merge_path_description(merge_request, separator) if merge_request.for_fork? - "Project:Branches: #{@merge_request.source_project_path}:#{@merge_request.source_branch} #{separator} #{@merge_request.target_project.path_with_namespace}:#{@merge_request.target_branch}" + "Project:Branches: #{@merge_request.source_project_path}:#{@merge_request.source_branch} #{separator} #{@merge_request.target_project.full_path}:#{@merge_request.target_branch}" else "Branches: #{@merge_request.source_branch} #{separator} #{@merge_request.target_branch}" end end - def issues_sentence(issues) - # Issuable sorter will sort local issues, then issues from the same - # namespace, then all other issues. - issues = Gitlab::IssuableSorter.sort(@project, issues).map do |issue| - issue.to_reference(@project) - end - issues.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, + project_new_merge_request_path( + @project, merge_request: { 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 @@ -127,14 +72,16 @@ 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 def merge_request_version_path(project, merge_request, merge_request_diff, start_sha = nil) - diffs_namespace_project_merge_request_path( - project.namespace, project, merge_request, - diff_id: merge_request_diff.id, start_sha: start_sha) + diffs_project_merge_request_path(project, merge_request, diff_id: merge_request_diff.id, start_sha: start_sha) end def version_index(merge_request_diff) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index c9e70faa52e..86666022a2a 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -1,7 +1,7 @@ module MilestonesHelper def milestones_filter_path(opts = {}) if @project - namespace_project_milestones_path(@project.namespace, @project, opts) + project_milestones_path(@project, opts) elsif @group group_milestones_path(@group, opts) else @@ -11,7 +11,7 @@ module MilestonesHelper def milestones_label_path(opts = {}) if @project - namespace_project_issues_path(@project.namespace, @project, opts) + project_issues_path(@project, opts) elsif @group issues_group_path(@group, opts) else @@ -32,7 +32,18 @@ module MilestonesHelper end def milestone_issues_by_label_count(milestone, label, state:) - milestone.issues.with_label(label.title).send(state).size + issues = milestone.issues.with_label(label.title) + issues = + case state + when :opened + issues.opened + when :closed + issues.closed + else + raise ArgumentError, "invalid milestone state `#{state}`" + end + + issues.size end # Returns count of milestones for different states @@ -54,8 +65,10 @@ module MilestonesHelper def milestone_class_for_state(param, check, match_blank_param = false) if match_blank_param 'active' if param.blank? || param == check + elsif param == check + 'active' else - 'active' if param == check + check end end @@ -73,7 +86,9 @@ module MilestonesHelper def milestones_filter_dropdown_path project = @target_project || @project if project - namespace_project_milestones_path(project.namespace, project, :json) + project_milestones_path(project, :json) + elsif @group + group_milestones_path(@group, :json) else dashboard_milestones_path(:json) end @@ -115,4 +130,44 @@ module MilestonesHelper end end end + + def milestone_merge_request_tab_path(milestone) + if @project + merge_requests_project_milestone_path(@project, milestone, format: :json) + elsif @group + merge_requests_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json) + else + merge_requests_dashboard_milestone_path(milestone, title: milestone.title, format: :json) + end + end + + def milestone_participants_tab_path(milestone) + if @project + participants_project_milestone_path(@project, milestone, format: :json) + elsif @group + participants_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json) + else + participants_dashboard_milestone_path(milestone, title: milestone.title, format: :json) + end + end + + def milestone_labels_tab_path(milestone) + if @project + labels_project_milestone_path(@project, milestone, format: :json) + elsif @group + labels_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json) + else + labels_dashboard_milestone_path(milestone, title: milestone.title, format: :json) + end + end + + def group_milestone_route(milestone, params = {}) + params = nil if params.empty? + + if milestone.is_legacy_group_milestone? + group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: params) + else + group_milestone_path(@group, milestone.iid, milestone: params) + end + end end diff --git a/app/helpers/milestones_routing_helper.rb b/app/helpers/milestones_routing_helper.rb new file mode 100644 index 00000000000..766d5262018 --- /dev/null +++ b/app/helpers/milestones_routing_helper.rb @@ -0,0 +1,17 @@ +module MilestonesRoutingHelper + def milestone_path(milestone, *args) + if milestone.is_group_milestone? + group_milestone_path(milestone.group, milestone, *args) + elsif milestone.is_project_milestone? + project_milestone_path(milestone.project, milestone, *args) + end + end + + def milestone_url(milestone, *args) + if milestone.is_group_milestone? + group_milestone_url(milestone.group, milestone, *args) + elsif milestone.is_project_milestone? + project_milestone_url(milestone.project, milestone, *args) + end + end +end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 17bfd07e00f..b63b3b70903 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -1,42 +1,50 @@ module NavHelper + def page_with_sidebar_class + class_name = page_gutter_class + class_name << 'page-with-new-sidebar' if defined?(@new_sidebar) && @new_sidebar + class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @new_sidebar + + class_name + end + def page_gutter_class if current_path?('merge_requests#show') || - current_path?('merge_requests#diffs') || - current_path?('merge_requests#commits') || - current_path?('merge_requests#builds') || - current_path?('merge_requests#conflicts') || - current_path?('merge_requests#pipelines') || + current_path?('projects/merge_requests/conflicts#show') || current_path?('issues#show') || current_path?('milestones#show') if cookies[:collapsed_gutter] == 'true' - "page-gutter right-sidebar-collapsed" + %w[page-gutter right-sidebar-collapsed] else - "page-gutter right-sidebar-expanded" + %w[page-gutter right-sidebar-expanded] end - elsif current_path?('builds#show') - "page-gutter build-sidebar right-sidebar-expanded" + elsif current_path?('jobs#show') + %w[page-gutter build-sidebar right-sidebar-expanded] elsif current_path?('wikis#show') || current_path?('wikis#edit') || current_path?('wikis#update') || current_path?('wikis#history') || current_path?('wikis#git_access') - "page-gutter wiki-sidebar right-sidebar-expanded" + %w[page-gutter wiki-sidebar right-sidebar-expanded] + else + [] end end def nav_header_class - class_name = '' - class_name << " with-horizontal-nav" if defined?(nav) && nav + class_names = [] + class_names << 'with-horizontal-nav' if defined?(nav) && nav - class_name + class_names end def layout_nav_class - class_name = '' - class_name << " page-with-layout-nav" if defined?(nav) && nav - class_name << " page-with-sub-nav" if content_for?(:sub_nav) + return [] if show_new_nav? - class_name + class_names = [] + class_names << 'page-with-layout-nav' if defined?(nav) && nav + class_names << 'page-with-sub-nav' if content_for?(:sub_nav) + + class_names end def nav_control_class diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index eab0738a368..e857e837c16 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -10,8 +10,8 @@ module NotesHelper Ability.can_edit_note?(current_user, note) end - def note_supports_slash_commands?(note) - Notes::SlashCommandsService.supported?(note, current_user) + def note_supports_quick_actions?(note) + Notes::QuickActionsService.supported?(note, current_user) end def noteable_json(noteable) @@ -19,7 +19,7 @@ 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 @@ -34,7 +34,7 @@ module NotesHelper data = { line_code: line_code, - line_type: line_type, + line_type: line_type } if @use_legacy_diff_notes @@ -47,10 +47,26 @@ module NotesHelper data end + def add_diff_note_button(line_code, position, line_type) + return if @diff_notes_disabled + + button_tag '', + class: 'add-diff-note js-add-diff-note-button', + type: 'submit', name: 'button', + data: diff_view_line_data(line_code, position, line_type), + title: 'Add a comment to this line' do + icon('comment-o') + end + end + def link_to_reply_discussion(discussion, line_type = nil) return unless current_user - data = { discussion_id: discussion.id, line_type: line_type } + data = { + discussion_id: discussion.reply_id, + discussion_project_id: discussion.project&.id, + line_type: line_type + } button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', data: data, title: 'Add a reply' @@ -60,24 +76,72 @@ module NotesHelper note.project.team.human_max_access(note.author_id) end - def discussion_diff_path(discussion) - if discussion.for_merge_request? && discussion.diff_discussion? - if discussion.active? - # Without a diff ID, the link always points to the latest diff version - diff_id = nil - elsif merge_request_diff = discussion.latest_merge_request_diff - diff_id = merge_request_diff.id - else - # If the discussion is not active, and we cannot find the latest - # merge request diff for this discussion, we return no path at all. - return - end - - diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, diff_id: diff_id, anchor: discussion.line_code) + 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_project_merge_request_path(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) + project_commit_path(discussion.project, discussion.noteable, anchor: anchor) + end + end + + def notes_url + if @snippet.is_a?(PersonalSnippet) + snippet_notes_path(@snippet) + else + project_noteable_notes_path(@project, target_id: @noteable.id, target_type: @noteable.class.name.underscore) + end + end + + def note_url(note, project = @project) + if note.noteable.is_a?(PersonalSnippet) + snippet_note_path(note.noteable, note) + else + project_note_path(project, note) end end + + def noteable_note_url(note) + Gitlab::UrlBuilder.build(note) + end + + 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 + + def initial_notes_data(autocomplete) + { + notesUrl: notes_url, + notesIds: @notes.map(&:id), + now: Time.now.to_i, + diffView: diff_view, + autocomplete: autocomplete + } + end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 03cc8f2b6bd..fde961e2da4 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -21,30 +21,36 @@ module NotificationsHelper end def notification_title(level) + # Can be anything in `NotificationSetting.level: case level.to_sym when :participating - 'Participate' + s_('NotificationLevel|Participate') when :mention - 'On mention' + s_('NotificationLevel|On mention') else - level.to_s.titlecase + N_('NotificationLevel|Global') + N_('NotificationLevel|Watch') + N_('NotificationLevel|Disabled') + N_('NotificationLevel|Custom') + level = "NotificationLevel|#{level.to_s.humanize}" + s_(level) end end def notification_description(level) case level.to_sym when :participating - 'You will only receive notifications for threads you have participated in' + _('You will only receive notifications for threads you have participated in') when :mention - 'You will receive notifications only for comments in which you were @mentioned' + _('You will receive notifications only for comments in which you were @mentioned') when :watch - 'You will receive notifications for any activity' + _('You will receive notifications for any activity') when :disabled - 'You will not get any notifications via email' + _('You will not get any notifications via email') when :global - 'Use your global notification setting' + _('Use your global notification setting') when :custom - 'You will only receive notifications for the events you choose' + _('You will only receive notifications for the events you choose') end end @@ -76,11 +82,22 @@ module NotificationsHelper end def notification_event_name(event) + # All values from NotificationSetting::EMAIL_EVENTS case event when :success_pipeline - 'Successful pipeline' + s_('NotificationEvent|Successful pipeline') else - event.to_s.humanize + N_('NotificationEvent|New note') + N_('NotificationEvent|New issue') + N_('NotificationEvent|Reopen issue') + N_('NotificationEvent|Close issue') + N_('NotificationEvent|Reassign issue') + N_('NotificationEvent|New merge request') + N_('NotificationEvent|Close merge request') + N_('NotificationEvent|Reassign merge request') + N_('NotificationEvent|Merge merge request') + N_('NotificationEvent|Failed pipeline') + s_(event.to_s.humanize) end end end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 3286a92a8a7..b30b2eb1d03 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -4,6 +4,10 @@ module PageLayoutHelper @page_title.push(*titles.compact) if titles.any? + if show_new_nav? && titles.any? && !defined?(@breadcrumb_title) + @breadcrumb_title = @page_title.last + end + # Segments are seperated by middot @page_title.join(" \u00b7 ") end diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb new file mode 100644 index 00000000000..83dd76a01dd --- /dev/null +++ b/app/helpers/pagination_helper.rb @@ -0,0 +1,21 @@ +module PaginationHelper + def paginate_collection(collection, remote: nil) + if collection.is_a?(Kaminari::PaginatableWithoutCount) + paginate_without_count(collection) + elsif collection.respond_to?(:total_pages) + paginate_with_count(collection, remote: remote) + end + end + + def paginate_without_count(collection) + render( + 'kaminari/gitlab/without_count', + previous_path: path_to_prev_page(collection), + next_path: path_to_next_page(collection) + ) + end + + def paginate_with_count(collection, remote: nil) + paginate(collection, remote: remote, theme: 'gitlab') + end +end diff --git a/app/helpers/performance_bar_helper.rb b/app/helpers/performance_bar_helper.rb new file mode 100644 index 00000000000..d24efe37f5f --- /dev/null +++ b/app/helpers/performance_bar_helper.rb @@ -0,0 +1,7 @@ +module PerformanceBarHelper + # This is a hack since using `alias_method :performance_bar_enabled?, :peek_enabled?` + # in WithPerformanceBar breaks tests (but works in the browser). + def performance_bar_enabled? + peek_enabled? + end +end diff --git a/app/helpers/pipeline_schedules_helper.rb b/app/helpers/pipeline_schedules_helper.rb new file mode 100644 index 00000000000..6edaf78de1b --- /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 de959f13713..d36bb4ab074 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -49,7 +49,7 @@ module PreferencesHelper user_view = current_user.project_view - if @project.feature_available?(:repository, current_user) + if can?(current_user, :download_code, @project) user_view elsif user_view == "activity" "activity" diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb new file mode 100644 index 00000000000..45238f12ac7 --- /dev/null +++ b/app/helpers/profiles_helper.rb @@ -0,0 +1,7 @@ +module ProfilesHelper + def email_provider_label + return unless current_user.external_email? + + current_user.email_provider.present? ? Gitlab::OAuth::Provider.label_for(current_user.email_provider) : "LDAP" + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 8c26348a975..bee4950e414 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -58,7 +58,17 @@ module ProjectsHelper link_to(simple_sanitize(owner.name), user_path(owner)) end - project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } + project_link = link_to project_path(project), { class: "project-item-select-holder" } do + output = + if show_new_nav? + project_icon(project, alt: project.name, class: 'avatar-tile', width: 16, height: 16) + else + "" + end + + output << simple_sanitize(project.name) + output.html_safe + end if current_user project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do @@ -70,21 +80,30 @@ module ProjectsHelper end def remove_project_message(project) - "You are going to remove #{project.name_with_namespace}.\n Removed project CANNOT be restored!\n Are you ABSOLUTELY sure?" + _("You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") % + { project_name_with_namespace: project.name_with_namespace } end def transfer_project_message(project) - "You are going to transfer #{project.name_with_namespace} to another owner. Are you ABSOLUTELY sure?" + _("You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?") % + { project_name_with_namespace: project.name_with_namespace } end def remove_fork_project_message(project) - "You are going to remove the fork relationship to source project #{@project.forked_from_project.name_with_namespace}. Are you ABSOLUTELY sure?" + _("You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?") % + { forked_from_project: @project.forked_from_project.name_with_namespace } end def project_nav_tabs @nav_tabs ||= get_project_nav_tabs(@project, current_user) end + def project_search_tabs?(tab) + abilities = Array(search_tab_ability_map[tab]) + + abilities.any? { |ability| can?(current_user, ability, @project) } + end + def project_nav_tab?(name) project_nav_tabs.include? name end @@ -110,15 +129,13 @@ 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 return unless current_user + return current_user.recent_push unless @project project_ids = [@project.id] if fork = current_user.fork_of(@project) @@ -132,43 +149,103 @@ module ProjectsHelper # Don't show option "everyone with access" if project is private options = project_feature_options + level = @project.project_feature.public_send(field) # rubocop:disable GitlabSecurity/PublicSend + if @project.private? - level = @project.project_feature.send(field) - options.delete('Everyone with access') - highest_available_option = options.values.max if level == ProjectFeature::ENABLED + disabled_option = ProjectFeature::ENABLED + highest_available_option = ProjectFeature::PRIVATE if level == disabled_option end - options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field)) + options = options_for_select( + options.invert, + selected: highest_available_option || level, + disabled: disabled_option + ) - content_tag( - :select, - options, - name: "project[project_feature_attributes][#{field}]", - id: "project_project_feature_attributes_#{field}", - class: "pull-right form-control #{repo_children_classes(field)}", - data: { field: field } - ).html_safe + content_tag :div, class: "select-wrapper" do + concat( + content_tag( + :select, + options, + name: "project[project_feature_attributes][#{field}]", + id: "project_project_feature_attributes_#{field}", + class: "pull-right form-control select-control #{repo_children_classes(field)} ", + data: { field: field } + ) + ) + concat( + icon('chevron-down') + ) + end.html_safe end def link_to_autodeploy_doc - link_to 'About auto deploy', help_page_path('ci/autodeploy/index'), target: '_blank' + link_to _('About auto deploy'), help_page_path('ci/autodeploy/index'), target: '_blank' end def autodeploy_flash_notice(branch_name) - "Branch <strong>#{truncate(sanitize(branch_name))}</strong> was created. To set up auto deploy, \ - choose a GitLab CI Yaml template and commit your changes. #{link_to_autodeploy_doc}".html_safe + translation = _("Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}") % + { branch_name: truncate(sanitize(branch_name)), link_to_autodeploy_doc: link_to_autodeploy_doc } + translation.html_safe 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.4'] + key = [ + project.route.cache_key, + project.cache_key, + controller.controller_name, + controller.action_name, + current_application_settings.cache_key, + 'v2.5' + ] + 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) + Gitlab::Cache::Ci::ProjectPipelineStatus + .load_in_batch_for_projects(projects) + end + + def show_no_ssh_key_message? + cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key? + end + + def show_no_password_message? + cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && + ( current_user.require_password_creation? || current_user.require_personal_access_token_creation_for_git_auth? ) + end + + def link_to_set_password + if current_user.require_password_creation? + link_to s_('SetPasswordToCloneLink|set a password'), edit_profile_password_path + else + link_to s_('CreateTokenToCloneLink|create a personal access token'), profile_personal_access_tokens_path + end + end + + # Returns true if any projects are present. + # + # If the relation has a LIMIT applied we'll cast the relation to an Array + # since repeated any? checks would otherwise result in multiple COUNT queries + # being executed. + # + # If no limit is applied we'll just issue a COUNT since the result set could + # be too large to load into memory. + def any_projects?(projects) + return projects.any? if projects.is_a?(Array) + + if projects.limit_value + projects.to_a.any? + else + projects.except(:offset).any? + end + end + + def has_projects_or_name?(projects, params) + !!(params[:name] || any_projects?(projects)) end private @@ -198,18 +275,9 @@ module ProjectsHelper nav_tabs << :container_registry end - tab_ability_map = { - environments: :read_environment, - milestones: :read_milestone, - pipelines: :read_pipeline, - snippets: :read_project_snippet, - settings: :admin_project, - builds: :read_build, - labels: :read_label, - issues: :read_issue, - team: :read_project_member, - wiki: :read_wiki - } + if project.builds_enabled? && can?(current_user, :read_pipeline, project) + nav_tabs << :pipelines + end tab_ability_map.each do |tab, ability| if can?(current_user, ability, project) @@ -220,14 +288,37 @@ module ProjectsHelper nav_tabs.flatten end + def tab_ability_map + { + environments: :read_environment, + milestones: :read_milestone, + snippets: :read_project_snippet, + settings: :admin_project, + builds: :read_build, + labels: :read_label, + issues: :read_issue, + project_members: :read_project_member, + wiki: :read_wiki + } + end + + def search_tab_ability_map + @search_tab_ability_map ||= tab_ability_map.merge( + blobs: :download_code, + commits: :download_code, + merge_requests: :read_merge_request, + notes: [:read_merge_request, :download_code, :read_issue, :read_project_snippet] + ) + end + def project_lfs_status(project) if project.lfs_enabled? content_tag(:span, class: 'lfs-enabled') do - 'Enabled' + s_('LFSStatus|Enabled') end else content_tag(:span, class: 'lfs-disabled') do - 'Disabled' + s_('LFSStatus|Disabled') end end end @@ -236,7 +327,7 @@ module ProjectsHelper if current_user current_user.name else - "Your name" + _("Your name") end end @@ -253,7 +344,7 @@ module ProjectsHelper when 'ssh' project.ssh_url_to_repo else - project.http_url_to_repo(current_user) + project.http_url_to_repo end end @@ -273,25 +364,24 @@ module ProjectsHelper if project.last_activity_at time_ago_with_tooltip(project.last_activity_at, placement: 'bottom', html_class: 'last_activity_time_ago') else - "Never" + s_("ProjectLastActivity|Never") end end def add_special_file_path(project, file_name:, commit_message: nil, branch_name: nil, context: nil) - namespace_project_new_blob_path( - project.namespace, + commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name.downcase } + project_new_blob_path( project, project.default_branch || 'master', file_name: file_name, - commit_message: commit_message || "Add #{file_name.downcase}", + commit_message: commit_message, branch_name: branch_name, context: context ) end def add_koding_stack_path(project) - namespace_project_new_blob_path( - project.namespace, + project_new_blob_path( project, project.default_branch || 'master', file_name: '.koding.yml', @@ -331,7 +421,7 @@ module ProjectsHelper if project import_path = "/Home/Stacks/import" - repo = project.path_with_namespace + repo = project.full_path branch ||= project.default_branch sha ||= project.commit.short_id @@ -345,8 +435,7 @@ module ProjectsHelper def contribution_guide_path(project) if project && contribution_guide = project.repository.contribution_guide - namespace_project_blob_path( - project.namespace, + project_blob_path( project, tree_join(project.default_branch, contribution_guide.name) @@ -376,7 +465,7 @@ module ProjectsHelper def project_wiki_path_with_version(proj, page, version, is_newest) url_params = is_newest ? {} : { version_id: version } - namespace_project_wiki_path(proj.namespace, proj, page, url_params) + project_wiki_path(proj, page, url_params) end def project_status_css_class(status) @@ -392,7 +481,7 @@ module ProjectsHelper def readme_cache_key sha = @project.commit.try(:sha) || 'nil' - [@project.path_with_namespace, sha, "readme"].join('-') + [@project.full_path, sha, "readme"].join('-') end def current_ref @@ -400,9 +489,8 @@ module ProjectsHelper end def filename_path(project, filename) - if project && blob = project.repository.send(filename) - namespace_project_blob_path( - project.namespace, + if project && blob = project.repository.public_send(filename) # rubocop:disable GitlabSecurity/PublicSend + project_blob_path( project, tree_join(project.default_branch, blob.name) ) @@ -420,9 +508,9 @@ module ProjectsHelper def project_feature_options { - 'Disabled' => ProjectFeature::DISABLED, - 'Only team members' => ProjectFeature::PRIVATE, - 'Everyone with access' => ProjectFeature::ENABLED + ProjectFeature::DISABLED => s_('ProjectFeature|Disabled'), + ProjectFeature::PRIVATE => s_('ProjectFeature|Only team members'), + ProjectFeature::ENABLED => s_('ProjectFeature|Everyone with access') } end @@ -453,4 +541,12 @@ module ProjectsHelper current_application_settings.restricted_visibility_levels || [] end + + def find_file_path + return unless @project && !@project.empty_repo? + + ref = @ref || @project.repository.root_ref + + project_find_file_path(@project, ref) + end end diff --git a/app/helpers/rss_helper.rb b/app/helpers/rss_helper.rb index ea5d2932ef4..9ac4df88dc3 100644 --- a/app/helpers/rss_helper.rb +++ b/app/helpers/rss_helper.rb @@ -1,5 +1,5 @@ module RssHelper def rss_url_options - { format: :atom, private_token: current_user.try(:private_token) } + { format: :atom, rss_token: current_user.try(:rss_token) } end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 8ff8db16514..ae0e0aa3cf9 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 @@ -67,16 +67,16 @@ module SearchHelper ref = @ref || @project.repository.root_ref [ - { category: "Current Project", label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, - { category: "Current Project", label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, - { category: "Current Project", label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, - { category: "Current Project", label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, - { category: "Current Project", label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) }, - { category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, - { 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: "Files", url: project_tree_path(@project, ref) }, + { category: "Current Project", label: "Commits", url: project_commits_path(@project, ref) }, + { category: "Current Project", label: "Network", url: project_network_path(@project, ref) }, + { category: "Current Project", label: "Graph", url: project_graph_path(@project, ref) }, + { category: "Current Project", label: "Issues", url: project_issues_path(@project) }, + { category: "Current Project", label: "Merge Requests", url: project_merge_requests_path(@project) }, + { category: "Current Project", label: "Milestones", url: project_milestones_path(@project) }, + { category: "Current Project", label: "Snippets", url: project_snippets_path(@project) }, + { category: "Current Project", label: "Members", url: project_project_members_path(@project) }, + { category: "Current Project", label: "Wiki", url: project_wikis_path(@project) } ] else [] @@ -97,14 +97,14 @@ module SearchHelper # Autocomplete results for the current user's projects def projects_autocomplete(term, limit = 5) - current_user.authorized_projects.search_by_title(term). - sorted_by_stars.non_archived.limit(limit).map do |p| + current_user.authorized_projects.search_by_title(term) + .sorted_by_stars.non_archived.limit(limit).map do |p| { category: "Projects", id: p.id, value: "#{search_result_sanitize(p.name)}", label: "#{search_result_sanitize(p.name_with_namespace)}", - url: namespace_project_path(p.namespace, p) + url: project_path(p) } end end @@ -126,6 +126,26 @@ module SearchHelper search_path(options) end + def search_filter_input_options(type) + opts = { + id: "filtered-search-#{type}", + placeholder: 'Search or filter results...', + data: { + 'username-params' => @users.to_json(only: [:id, :username]) + } + } + + if @project.present? + opts[:data]['project-id'] = @project.id + opts[:data]['base-endpoint'] = project_path(@project) + else + # Group context + opts[:data]['base-endpoint'] = group_canonical_path(@group) + end + + opts + end + # Sanitize a HTML field for search display. Most tags are stripped out and the # maximum length is set to 200 characters. def search_md_sanitize(object, field) diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 8706876ae4a..1a4f1431bdc 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -45,6 +45,14 @@ module SelectsHelper end end + with_feature_enabled_data_attribute = + case opts.delete(:with_feature_enabled) + when 'issues' then 'data-with-issues-enabled' + when 'merge_requests' then 'data-with-merge-requests-enabled' + end + + opts[with_feature_enabled_data_attribute] = true + hidden_field_tag(id, opts[:selected], opts) end @@ -67,7 +75,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/snippets_helper.rb b/app/helpers/snippets_helper.rb index 979264c9421..b447d4952e7 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -1,19 +1,26 @@ module SnippetsHelper def reliable_snippet_path(snippet, opts = nil) if snippet.project_id? - namespace_project_snippet_path(snippet.project.namespace, - snippet.project, snippet, opts) + project_snippet_path(snippet.project, snippet, opts) else snippet_path(snippet, opts) end end + def download_snippet_path(snippet) + if snippet.project_id + raw_project_snippet_path(@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 def subject_snippets_path(subject = nil, opts = nil) if subject.is_a?(Project) - namespace_project_snippets_path(subject.namespace, subject, opts) + project_snippets_path(subject, opts) else # assume subject === User dashboard_snippets_path(opts) end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 2fda98cae90..b408ec0c6a4 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -58,7 +58,7 @@ 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 @@ -70,6 +70,14 @@ module SortingHelper } 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 + def sort_title_priority 'Priority' end diff --git a/app/helpers/storage_health_helper.rb b/app/helpers/storage_health_helper.rb new file mode 100644 index 00000000000..544c9efb845 --- /dev/null +++ b/app/helpers/storage_health_helper.rb @@ -0,0 +1,37 @@ +module StorageHealthHelper + def failing_storage_health_message(storage_health) + storage_name = content_tag(:strong, h(storage_health.storage_name)) + host_names = h(storage_health.failing_on_hosts.to_sentence) + translation_params = { storage_name: storage_name, + host_names: host_names, + failed_attempts: storage_health.total_failures } + + translation = n_('%{storage_name}: failed storage access attempt on host:', + '%{storage_name}: %{failed_attempts} failed storage access attempts:', + storage_health.total_failures) % translation_params + + translation.html_safe + end + + def message_for_circuit_breaker(circuit_breaker) + maximum_failures = circuit_breaker.failure_count_threshold + current_failures = circuit_breaker.failure_count + permanently_broken = circuit_breaker.circuit_broken? && current_failures >= maximum_failures + + translation_params = { number_of_failures: current_failures, + maximum_failures: maximum_failures, + number_of_seconds: circuit_breaker.failure_wait_time } + + if permanently_broken + s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\ + "retry automatically. Reset storage information when the problem is "\ + "resolved.") % translation_params + elsif circuit_breaker.circuit_broken? + _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\ + "block access for %{number_of_seconds} seconds.") % translation_params + else + _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\ + "allow access on the next attempt.") % translation_params + end + end +end diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index a762b320d56..88f7702db1e 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -1,28 +1,46 @@ module SubmoduleHelper - include Gitlab::ShellAdapter + extend self + + 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 + gitlab_hosts = [Gitlab.config.gitlab.url, + Gitlab.config.gitlab_shell.ssh_path_prefix] + + gitlab_hosts.each do |host| + if url.start_with?(host) + namespace, _, project = url.sub(host, '').rpartition('/') + break + end + end + + namespace.sub!(/\A\//, '') + project.rstrip! + 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 @@ -41,7 +59,7 @@ module SubmoduleHelper 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('')) + url_with_dotgit == Gitlab::Shell.new.url_to_repo([namespace, '/', project].join('')) end def relative_self_url?(url) @@ -55,6 +73,7 @@ module SubmoduleHelper end def relative_self_links(url, commit) + url.rstrip! # Map relative links to a namespace and project # For example: # ../bar.git -> same namespace, repo bar @@ -73,4 +92,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 index 1ea60e39386..08fd97cd048 100644 --- a/app/helpers/system_note_helper.rb +++ b/app/helpers/system_note_helper.rb @@ -1,6 +1,7 @@ module SystemNoteHelper ICON_NAMES_BY_ACTION = { 'commit' => 'icon_commit', + 'description' => 'icon_edit', 'merge' => 'icon_merge', 'merged' => 'icon_merged', 'opened' => 'icon_status_open', @@ -16,7 +17,9 @@ module SystemNoteHelper 'visible' => 'icon_eye', 'milestone' => 'icon_clock_o', 'discussion' => 'icon_comment_o', - 'moved' => 'icon_arrow_circle_o_right' + 'moved' => 'icon_arrow_circle_o_right', + 'outdated' => 'icon_edit', + 'duplicate' => 'icon_clone' }.freeze def icon_for_system_note(note) diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 1a55ee05996..ee701076a14 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -107,8 +107,7 @@ module TabHelper def branches_tab_class if current_controller?(:protected_branches) || current_controller?(:branches) || - current_page?(namespace_project_repository_path(@project.namespace, - @project)) + current_page?(project_repository_path(@project)) 'active' end end diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index 31aaf9e5607..d000d6b1c0a 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -10,7 +10,7 @@ module TagsHelper } options = exist_opts.merge(options) - namespace_project_tags_path(@project.namespace, @project, @id, options) + project_tags_path(@project, @id, options) end def tag_list(project) diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 4f5adf623f2..2a7aa299e83 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -4,7 +4,7 @@ module TodosHelper end def todos_count_format(count) - count > 99 ? '99+' : count + count > 99 ? '99+' : count.to_s end def todos_done_count @@ -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) @@ -36,7 +39,7 @@ module TodosHelper anchor = dom_id(todo.note) if todo.note.present? if todo.for_commit? - namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project, + project_commit_path(todo.project, todo.target, anchor: anchor) else path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target] @@ -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 f7b5a5f4dfc..e0d3e9b88f3 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -76,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/triggers_helper.rb b/app/helpers/triggers_helper.rb index a48d4475e97..ce435ca2241 100644 --- a/app/helpers/triggers_helper.rb +++ b/app/helpers/triggers_helper.rb @@ -8,6 +8,6 @@ module TriggersHelper end def service_trigger_url(service) - "#{Settings.gitlab.url}/api/v3/projects/#{service.project_id}/services/#{service.to_param}/trigger" + "#{Settings.gitlab.url}/api/v4/projects/#{service.project_id}/services/#{service.to_param}/trigger" end end diff --git a/app/helpers/u2f_helper.rb b/app/helpers/u2f_helper.rb index 143b4ca6b51..81bfe5d4eeb 100644 --- a/app/helpers/u2f_helper.rb +++ b/app/helpers/u2f_helper.rb @@ -1,5 +1,5 @@ module U2fHelper def inject_u2f_api? - browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile? + ((browser.chrome? && browser.version.to_i >= 41) || (browser.opera? && browser.version.to_i >= 40)) && !browser.device.mobile? end end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 9c623c9ba7c..b5f54d3e154 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -4,4 +4,14 @@ module UsersHelper title: user.email, class: 'has-tooltip commit-committer-link') end + + def user_email_help_text(user) + return 'We also use email for avatar detection if no avatar is uploaded.' unless user.unconfirmed_email.present? + + confirmation_link = link_to 'Resend confirmation e-mail', user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post + + h('Please click the link in the confirmation email before continuing. It was sent to ') + + content_tag(:strong) { user.unconfirmed_email } + h('.') + + content_tag(:p) { confirmation_link } + end end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index b4aaf498068..35755bc149b 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -29,11 +29,11 @@ module VisibilityLevelHelper def project_visibility_level_description(level) case level when Gitlab::VisibilityLevel::PRIVATE - "Project access must be granted explicitly to each user." + _("Project access must be granted explicitly to each user.") when Gitlab::VisibilityLevel::INTERNAL - "The project can be cloned by any logged in user." + _("The project can be accessed by any logged in user.") when Gitlab::VisibilityLevel::PUBLIC - "The project can be cloned without any authentication." + _("The project can be accessed without any authentication.") end end @@ -81,7 +81,9 @@ module VisibilityLevelHelper end def visibility_level_label(level) - Project.visibility_levels.key(level) + # The visibility level can be: + # 'VisibilityLevel|Private', 'VisibilityLevel|Internal', 'VisibilityLevel|Public' + s_(Project.visibility_levels.key(level)) end def restricted_visibility_levels(show_all = false) diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb index 6bacda9fe75..33453dd178f 100644 --- a/app/helpers/webpack_helper.rb +++ b/app/helpers/webpack_helper.rb @@ -11,20 +11,31 @@ module WebpackHelper paths = Webpack::Rails::Manifest.asset_paths(source) if extension - paths = paths.select { |p| p.ends_with? ".#{extension}" } + paths.select! { |p| p.ends_with? ".#{extension}" } end - # include full webpack-dev-server url for rspec tests running locally + force_host = webpack_public_host + if force_host + paths.map! { |p| "#{force_host}#{p}" } + end + + paths + end + + def webpack_public_host 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 + "#{protocol}://#{host}:#{port}" + else + ActionController::Base.asset_host.try(:chomp, '/') end + end - paths + def webpack_public_path + relative_path = Rails.application.config.relative_url_root + webpack_path = Rails.application.config.webpack.public_path + File.join(webpack_public_host.to_s, relative_path.to_s, webpack_path.to_s, '') end end diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 3e3f6246fc5..99212a3438f 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -6,8 +6,8 @@ module WikiHelper # Returns a String composed of the capitalized name of each directory and the # capitalized name of the page itself. def breadcrumb(page_slug) - page_slug.split('/'). - map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize }. - join(' / ') + page_slug.split('/') + .map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize } + .join(' / ') end end |