diff options
Diffstat (limited to 'app/helpers')
79 files changed, 2541 insertions, 1152 deletions
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index 16136d02530..c037de33c22 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -1,39 +1,36 @@ module AppearancesHelper def brand_title - if brand_item && brand_item.title - brand_item.title - else - 'GitLab Community Edition' - end + brand_item&.title.presence || 'GitLab Community Edition' end def brand_image - if brand_item.logo? - image_tag brand_item.logo - else - nil - end + image_tag(brand_item.logo) if brand_item&.logo? end def brand_text markdown_field(brand_item, :description) end + def brand_new_project_guidelines + markdown_field(brand_item, :new_project_guidelines) + end + def brand_item - @appearance ||= Appearance.first + @appearance ||= Appearance.current end def brand_header_logo - if brand_item && brand_item.header_logo? + if brand_item&.header_logo? image_tag brand_item.header_logo else render 'shared/logo.svg' end end - def custom_icon(icon_name, size: 16) - # We can't simply do the below, because there are some .erb SVGs. - # File.read(Rails.root.join("app/views/shared/icons/_#{icon_name}.svg")).html_safe - render "shared/icons/#{icon_name}.svg", size: size + # Skip the 'GitLab' type logo when custom brand logo is set + def brand_header_logo_type + unless brand_item&.header_logo? + render 'shared/logo_type.svg' + end end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index dc7ff78f3df..701be97ee96 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -34,7 +34,7 @@ module ApplicationHelper def project_icon(project_id, options = {}) project = - if project_id.is_a?(Project) + if project_id.respond_to?(:avatar_url) project_id else Project.find_by_full_path(project_id) @@ -68,18 +68,32 @@ module ApplicationHelper end end - 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 - else - User.find_by_any_email(user_or_email.try(:downcase)) - end + # Takes both user and email and returns the avatar_icon by + # user (preferred) or email. + def avatar_icon_for(user = nil, email = nil, size = nil, scale = 2, only_path: true) + if user + avatar_icon_for_user(user, size, scale, only_path: only_path) + elsif email + avatar_icon_for_email(email, size, scale, only_path: only_path) + else + default_avatar + end + end + def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true) + user = User.find_by_any_email(email.try(:downcase)) + if user + avatar_icon_for_user(user, size, scale, only_path: only_path) + else + gravatar_icon(email, size, scale) + end + end + + def avatar_icon_for_user(user = nil, size = nil, scale = 2, only_path: true) if user user.avatar_url(size: size, only_path: only_path) || default_avatar else - gravatar_icon(user_or_email, size, scale) + gravatar_icon(nil, size, scale) end end @@ -89,7 +103,7 @@ module ApplicationHelper end def default_avatar - 'no_avatar.png' + asset_path('no_avatar.png') end def last_commit(project) @@ -131,10 +145,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 @@ -181,7 +192,7 @@ module ApplicationHelper end def edited_time_ago_with_tooltip(object, placement: 'top', html_class: 'time_ago', exclude_author: false) - return unless object.is_edited? + return unless object.edited? content_tag :small, class: 'edited-text' do output = content_tag(:span, 'Edited ') @@ -205,7 +216,7 @@ module ApplicationHelper end def support_url - current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/' + Gitlab::CurrentSettings.current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/' end def page_filter_path(options = {}) @@ -267,7 +278,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 @@ -285,7 +300,7 @@ module ApplicationHelper def linkedin_url(user) name = user.linkedin - if name =~ %r{\Ahttps?:\/\/(www\.)?linkedin\.com\/in\/} + if name =~ %r{\Ahttps?://(www\.)?linkedin\.com/in/} name else "https://www.linkedin.com/in/#{name}" @@ -294,18 +309,25 @@ module ApplicationHelper def twitter_url(user) name = user.twitter - if name =~ %r{\Ahttps?:\/\/(www\.)?twitter\.com\/} + if name =~ %r{\Ahttps?://(www\.)?twitter\.com/} name else - "https://www.twitter.com/#{name}" + "https://twitter.com/#{name}" end end - def can_toggle_new_nav? - Rails.env.development? + def collapsed_sidebar? + cookies["sidebar_collapsed"] == "true" + end + + def locale_path + asset_path("locale/#{Gitlab::I18n.locale}/app.js") end - def show_new_nav? - cookies["new_nav"] == "true" + # Overridden in EE + def read_only_message + return unless Gitlab::Database.read_only? + + _('You are on a read-only GitLab instance.') end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index ca326dd0627..b3b080e6dcf 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -1,21 +1,23 @@ module ApplicationSettingsHelper - delegate :gravatar_enabled?, - :signup_enabled?, - :signin_enabled?, + extend self + + delegate :allow_signup?, + :gravatar_enabled?, + :password_authentication_enabled_for_web?, :akismet_enabled?, :koding_enabled?, - to: :current_application_settings + to: :'Gitlab::CurrentSettings.current_application_settings' def user_oauth_applications? - current_application_settings.user_oauth_applications + Gitlab::CurrentSettings.user_oauth_applications end def allowed_protocols_present? - current_application_settings.enabled_git_access_protocol.present? + Gitlab::CurrentSettings.enabled_git_access_protocol.present? end def enabled_protocol - case current_application_settings.enabled_git_access_protocol + case Gitlab::CurrentSettings.enabled_git_access_protocol when 'http' gitlab_config.protocol when 'ssh' @@ -26,25 +28,25 @@ module ApplicationSettingsHelper def enabled_project_button(project, protocol) case protocol when 'ssh' - ssh_clone_button(project, 'bottom', append_link: false) + ssh_clone_button(project, append_link: false) else - http_clone_button(project, 'bottom', append_link: false) + http_clone_button(project, append_link: false) end end # 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 @@ -53,7 +55,7 @@ module ApplicationSettingsHelper # toggle button effect. def import_sources_checkboxes(help_block_id) Gitlab::ImportSources.options.map do |name, source| - checked = current_application_settings.import_sources.include?(source) + checked = Gitlab::CurrentSettings.import_sources.include?(source) css_class = checked ? 'active' : '' checkbox_name = 'application_setting[import_sources][]' @@ -68,27 +70,183 @@ module ApplicationSettingsHelper def oauth_providers_checkboxes button_based_providers.map do |source| - disabled = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s) + disabled = Gitlab::CurrentSettings.disabled_oauth_sign_in_sources.include?(source.to_s) css_class = 'btn' css_class << ' active' unless disabled checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]' label_tag(checkbox_name, class: css_class) do check_box_tag(checkbox_name, source, !disabled, - autocomplete: 'off') + Gitlab::OAuth::Provider.label_for(source) + autocomplete: 'off') + Gitlab::Auth::OAuth::Provider.label_for(source) end end end - def repository_storages_options_for_select + def key_restriction_options_for_select(type) + bit_size_options = Gitlab::SSHPublicKey.supported_sizes(type).map do |bits| + ["Must be at least #{bits} bits", bits] + end + + [ + ['Are allowed', 0], + *bit_size_options, + ['Are forbidden', ApplicationSetting::FORBIDDEN_KEY_VALUE] + ] + end + + def repository_storages_options_for_select(selected) options = Gitlab.config.repositories.storages.map do |name, storage| - ["#{name} - #{storage['path']}", name] + ["#{name} - #{storage['gitaly_address']}", name] end - options_for_select(options, @application_setting.repository_storages) + options_for_select(options, selected) end def sidekiq_queue_options_for_select options_for_select(Sidekiq::Queue.all.map(&:name), @application_setting.sidekiq_throttling_queues) end + + def circuitbreaker_failure_count_help_text + health_link = link_to(s_('AdminHealthPageLink|health page'), admin_health_check_path) + api_link = link_to(s_('CircuitBreakerApiLink|circuitbreaker api'), help_page_path("api/repository_storage_health")) + message = _("The number of failures of after which GitLab will completely "\ + "prevent access to the storage. The number of failures can be "\ + "reset in the admin interface: %{link_to_health_page} or using "\ + "the %{api_documentation_link}.") + message = message % { link_to_health_page: health_link, api_documentation_link: api_link } + + message.html_safe + end + + def circuitbreaker_access_retries_help_text + _('The number of attempts GitLab will make to access a storage.') + end + + def circuitbreaker_failure_reset_time_help_text + _("The time in seconds GitLab will keep failure information. When no "\ + "failures occur during this time, information about the mount is reset.") + end + + def circuitbreaker_storage_timeout_help_text + _("The time in seconds GitLab will try to access storage. After this time a "\ + "timeout error will be raised.") + end + + def circuitbreaker_check_interval_help_text + _("The time in seconds between storage checks. When a previous check did "\ + "complete yet, GitLab will skip a check.") + end + + def visible_attributes + [ + :admin_notification_email, + :after_sign_out_path, + :after_sign_up_text, + :akismet_api_key, + :akismet_enabled, + :authorized_keys_enabled, + :auto_devops_enabled, + :auto_devops_domain, + :circuitbreaker_access_retries, + :circuitbreaker_check_interval, + :circuitbreaker_failure_count_threshold, + :circuitbreaker_failure_reset_time, + :circuitbreaker_storage_timeout, + :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, + :dsa_key_restriction, + :ecdsa_key_restriction, + :ed25519_key_restriction, + :email_author_in_body, + :enabled_git_access_protocol, + :gitaly_timeout_default, + :gitaly_timeout_medium, + :gitaly_timeout_fast, + :gravatar_enabled, + :hashed_storage_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, + :pages_domain_verification_enabled, + :password_authentication_enabled_for_web, + :password_authentication_enabled_for_git, + :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, + :rsa_key_restriction, + :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, + :throttle_unauthenticated_enabled, + :throttle_unauthenticated_requests_per_period, + :throttle_unauthenticated_period_in_seconds, + :throttle_authenticated_web_enabled, + :throttle_authenticated_web_requests_per_period, + :throttle_authenticated_web_period_in_seconds, + :throttle_authenticated_api_enabled, + :throttle_authenticated_api_requests_per_period, + :throttle_authenticated_api_period_in_seconds, + :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, + :allow_local_requests_from_hooks_and_services + ] + end end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 9c71d6c7f4c..c109954f3a3 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -3,7 +3,7 @@ module AuthHelper FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze def ldap_enabled? - Gitlab::LDAP::Config.enabled? + Gitlab::Auth::LDAP::Config.enabled? end def omniauth_enabled? @@ -15,11 +15,11 @@ module AuthHelper end def auth_providers - Gitlab::OAuth::Provider.providers + Gitlab::Auth::OAuth::Provider.providers end def label_for_provider(name) - Gitlab::OAuth::Provider.label_for(name) + Gitlab::Auth::OAuth::Provider.label_for(name) end def form_based_provider?(name) @@ -39,7 +39,7 @@ module AuthHelper end def enabled_button_based_providers - disabled_providers = current_application_settings.disabled_oauth_sign_in_sources || [] + disabled_providers = Gitlab::CurrentSettings.disabled_oauth_sign_in_sources || [] button_based_providers.map(&:to_s) - disabled_providers end diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb new file mode 100644 index 00000000000..16451993e93 --- /dev/null +++ b/app/helpers/auto_devops_helper.rb @@ -0,0 +1,36 @@ +module AutoDevopsHelper + def show_auto_devops_callout?(project) + Feature.get(:auto_devops_banner_disabled).off? && + show_callout?('auto_devops_settings_dismissed') && + can?(current_user, :admin_pipeline, project) && + project.has_auto_devops_implicitly_disabled? && + !project.repository.gitlab_ci_yml && + !project.ci_service + end + + def auto_devops_warning_message(project) + if missing_auto_devops_service?(project) + params = { + kubernetes: link_to('Kubernetes cluster', project_clusters_path(project)) + } + + if missing_auto_devops_domain?(project) + _('Auto Review Apps and Auto Deploy need a domain name and a %{kubernetes} to work correctly.') % params + else + _('Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly.') % params + end + elsif missing_auto_devops_domain?(project) + _('Auto Review Apps and Auto Deploy need a domain name to work correctly.') + end + end + + private + + def missing_auto_devops_domain?(project) + !(project.auto_devops || project.build_auto_devops)&.has_domain? + end + + def missing_auto_devops_service?(project) + !project.deployment_platform&.active? + end +end diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index bbe7f3c8fb4..21b6c0a8ad5 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -8,24 +8,46 @@ module AvatarsHelper })) end + def user_avatar_url_for(options = {}) + if options[:url] + options[:url] + elsif options[:user] + avatar_icon_for_user(options[:user], options[:size]) + else + avatar_icon_for_email(options[:user_email], options[:size]) + end + end + 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_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size) - data_attributes = { container: 'body' } + + avatar_url = user_avatar_url_for(options.merge(size: avatar_size)) + + has_tooltip = options[:has_tooltip].nil? ? true : options[:has_tooltip] + data_attributes = options[:data] || {} + css_class = %W[avatar s#{avatar_size}].push(*options[:css_class]) + + if has_tooltip + css_class.push('has-tooltip') + data_attributes[:container] = 'body' + end if options[:lazy] + css_class << 'lazy' data_attributes[:src] = avatar_url + avatar_url = LazyImageTagHelper.placeholder_image end - image_tag( - options[:lazy] ? '' : avatar_url, - class: "avatar has-tooltip s#{avatar_size} #{css_class}", - alt: "#{user_name}'s avatar", - title: user_name, - data: data_attributes - ) + image_options = { + alt: "#{user_name}'s avatar", + src: avatar_url, + data: data_attributes, + class: css_class, + title: user_name + } + + tag(:img, image_options) end def user_avatar(options = {}) diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb index 024cf38469e..86b19368cfd 100644 --- a/app/helpers/award_emoji_helper.rb +++ b/app/helpers/award_emoji_helper.rb @@ -7,7 +7,7 @@ module AwardEmojiHelper if awardable.for_personal_snippet? toggle_award_emoji_snippet_note_path(awardable.noteable, awardable) else - toggle_award_emoji_namespace_project_note_path(@project.namespace, @project, awardable.id) + toggle_award_emoji_project_note_path(@project, awardable.id) end else url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) diff --git a/app/helpers/blame_helper.rb b/app/helpers/blame_helper.rb index d1dc4d94560..089d9e3e387 100644 --- a/app/helpers/blame_helper.rb +++ b/app/helpers/blame_helper.rb @@ -11,11 +11,15 @@ module BlameHelper 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}" + if duration[:started_days_ago] == 0 + "blame-commit-age-0" + else + 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 end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 3efa7c36057..2b440e4d584 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -1,6 +1,8 @@ module BlobHelper def highlight(blob_name, blob_content, repository: nil, plain: false) + plain ||= blob_content.length > Blob::MAXIMUM_TEXT_HIGHLIGHT_SIZE highlighted = Gitlab::Highlight.highlight(blob_name, blob_content, plain: plain, repository: repository) + raw %(<pre class="code highlight"><code>#{highlighted}</code></pre>) end @@ -8,40 +10,41 @@ module BlobHelper %w(credits changelog news copying copyright license authors) end - def edit_path(project = @project, ref = @ref, path = @path, options = {}) - namespace_project_edit_blob_path(project.namespace, project, - tree_join(ref, path), - options[:link_opts]) + def edit_blob_path(project = @project, ref = @ref, path = @path, options = {}) + project_edit_blob_path(project, + tree_join(ref, path), + options[:link_opts]) end - def edit_blob_link(project = @project, ref = @ref, path = @path, options = {}) - blob = options.delete(:blob) - blob ||= project.repository.blob_at(ref, path) rescue nil + def ide_edit_path(project = @project, ref = @ref, path = @path, options = {}) + "#{ide_path}/project#{edit_blob_path(project, ref, path, options)}" + end - return unless blob && blob.readable_text? + def edit_blob_button(project = @project, ref = @ref, path = @path, options = {}) + return unless blob = readable_blob(options, path, project, ref) common_classes = "btn js-edit-blob #{options[:extra_class]}" - if !on_top_of_branch?(project, ref) - button_tag 'Edit', class: "#{common_classes} disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } - # This condition applies to anonymous or users who can edit directly - elsif !current_user || (current_user && can_modify_blob?(blob, project, ref)) - link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm" - elsif current_user && can?(current_user, :fork_project, project) - continue_params = { - to: edit_path(project, ref, path, options), - notice: edit_in_new_fork_notice, - notice_now: edit_in_new_fork_notice_now - } - fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) - - button_tag 'Edit', - class: "#{common_classes} js-edit-blob-link-fork-toggler", - data: { action: 'edit', fork_path: fork_path } - end + edit_button_tag(blob, + common_classes, + _('Edit'), + edit_blob_path(project, ref, path, options), + project, + ref) + end + + def ide_edit_button(project = @project, ref = @ref, path = @path, options = {}) + return unless blob = readable_blob(options, path, project, ref) + + edit_button_tag(blob, + 'btn btn-default', + _('Web IDE'), + ide_edit_path(project, ref, path, options), + project, + ref) end - def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:) + def modify_file_button(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:) return unless current_user blob = project.repository.blob_at(ref, path) rescue nil @@ -57,21 +60,12 @@ module BlobHelper elsif can_modify_blob?(blob, project, ref) button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' elsif can?(current_user, :fork_project, project) - continue_params = { - to: request.fullpath, - 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) - - button_tag label, - class: "#{common_classes} js-edit-blob-link-fork-toggler", - data: { action: action, fork_path: fork_path } + edit_fork_button_tag(common_classes, project, label, edit_modify_file_fork_params(action), action) end end def replace_blob_link(project = @project, ref = @ref, path = @path) - modify_file_link( + modify_file_button( project, ref, path, @@ -83,7 +77,7 @@ module BlobHelper end def delete_blob_link(project = @project, ref = @ref, path = @path) - modify_file_link( + modify_file_button( project, ref, path, @@ -118,20 +112,24 @@ module BlobHelper icon("#{file_type_icon_class('file', mode, name)} fw") end - def blob_raw_url + def blob_raw_url(only_path: false) if @build && @entry - raw_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path: @entry.path) + raw_project_job_artifacts_url(@project, @build, path: @entry.path, only_path: only_path) elsif @snippet if @snippet.project_id - raw_namespace_project_snippet_path(@project.namespace, @project, @snippet) + raw_project_snippet_url(@project, @snippet, only_path: only_path) else - raw_snippet_path(@snippet) + raw_snippet_url(@snippet, only_path: only_path) end elsif @blob - namespace_project_raw_path(@project.namespace, @project, @id) + project_raw_url(@project, @id, only_path: only_path) end end + def blob_raw_path + blob_raw_url(only_path: true) + 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. @@ -228,14 +226,14 @@ module BlobHelper return if blob.empty? if blob.raw_binary? || blob.stored_externally? - icon = icon('download') + icon = sprite_icon('download') title = 'Download' else icon = icon('file-code-o') title = 'Open raw' end - link_to icon, blob_raw_url, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' } + 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) @@ -270,7 +268,7 @@ module BlobHelper options << link_to('view the source', '#', class: 'js-blob-viewer-switch-btn', data: { viewer: 'simple' }) end - options << link_to('download it', blob_raw_url, target: '_blank', rel: 'noopener noreferrer') + options << link_to('download it', blob_raw_path, target: '_blank', rel: 'noopener noreferrer') options end @@ -279,14 +277,65 @@ module BlobHelper options = [] if can?(current_user, :create_issue, project) - options << link_to("submit an issue", new_namespace_project_issue_path(project.namespace, 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", new_namespace_project_merge_request_path(project.namespace, project)) + options << link_to("create a merge request", project_new_merge_request_path(project)) end options end + + def readable_blob(options, path, project, ref) + blob = options.delete(:blob) + blob ||= project.repository.blob_at(ref, path) rescue nil + + blob if blob&.readable_text? + end + + def edit_blob_fork_params(path) + { + to: path, + notice: edit_in_new_fork_notice, + notice_now: edit_in_new_fork_notice_now + } + end + + def edit_modify_file_fork_params(action) + { + to: request.fullpath, + notice: edit_in_new_fork_notice_action(action), + notice_now: edit_in_new_fork_notice_now + } + end + + def edit_fork_button_tag(common_classes, project, label, params, action = 'edit') + fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: params) + + button_tag label, + class: "#{common_classes} js-edit-blob-link-fork-toggler", + data: { action: action, fork_path: fork_path } + end + + def edit_disabled_button_tag(button_text, common_classes) + button_tag(button_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' }) + end + + def edit_link_tag(link_text, edit_path, common_classes) + link_to link_text, edit_path, class: "#{common_classes} btn-sm" + end + + def edit_button_tag(blob, common_classes, text, edit_path, project, ref) + if !on_top_of_branch?(project, ref) + edit_disabled_button_tag(text, common_classes) + # This condition only applies to users who are logged in + # Web IDE (Beta) requires the user to have this feature enabled + elsif !current_user || (current_user && can_modify_blob?(blob, project, ref)) + edit_link_tag(text, edit_path, common_classes) + elsif current_user && can?(current_user, :fork_project, project) + edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path)) + end + end end diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index e2df52e3833..275e892b2e6 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -1,15 +1,87 @@ module BoardsHelper - def board_data - board = @board || @boards.first + def board + @board ||= @board || @boards.first + end + def board_data { - endpoint: namespace_project_boards_path(@project.namespace, @project), + boards_endpoint: @boards_endpoint, + lists_endpoint: board_lists_path(board), board_id: board.id, - disabled: "#{!can?(current_user, :admin_list, @project)}", - issue_link_base: namespace_project_issues_path(@project.namespace, @project), + disabled: "#{!can?(current_user, :admin_list, current_board_parent)}", + issue_link_base: build_issue_link_base, root_path: root_path, - bulk_update_path: bulk_update_namespace_project_issues_path(@project.namespace, @project), + bulk_update_path: @bulk_issues_path, default_avatar: image_path(default_avatar) } end + + def build_issue_link_base + if board.group_board? + "#{group_path(@board.group)}/:project_path/issues" + else + project_issues_path(@project) + end + end + + def board_base_url + if board.group_board? + group_boards_url(@group) + else + project_boards_path(@project) + end + end + + def multiple_boards_available? + current_board_parent.multiple_issue_boards_available? + end + + def current_board_path(board) + @current_board_path ||= if board.group_board? + group_board_path(current_board_parent, board) + else + project_board_path(current_board_parent, board) + end + end + + def current_board_parent + @current_board_parent ||= @group || @project + end + + def can_admin_issue? + can?(current_user, :admin_issue, current_board_parent) + end + + def board_list_data + { + toggle: "dropdown", + list_labels_path: labels_filter_path(true), + labels: labels_filter_path(true), + labels_endpoint: @labels_endpoint, + namespace_path: @namespace_path, + project_path: @project&.path, + group_path: @group&.path + } + end + + def board_sidebar_user_data + dropdown_options = issue_assignees_dropdown_options + + { + toggle: 'dropdown', + field_name: 'issue[assignee_ids][]', + first_user: current_user&.username, + current_user: 'true', + project_id: @project&.id, + group_id: @group&.id, + null_user: 'true', + multi_select: 'true', + 'dropdown-header': dropdown_options[:data][:'dropdown-header'], + 'max-select': dropdown_options[:data][:'max-select'] + } + end + + def boards_link_text + s_("IssueBoards|Board") + end end diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index 59519c1335b..07b1fc3d7cf 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -1,21 +1,4 @@ module BranchesHelper - def filter_branches_path(options = {}) - exist_opts = { - search: params[:search], - sort: params[:sort] - } - - options = exist_opts.merge(options) - - namespace_project_branches_path(@project.namespace, @project, @id, options) - end - - def can_push_branch?(project, branch_name) - return false unless project.repository.branch_exists?(branch_name) - - ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch_name) - end - def project_branches options_for_select(@project.repository.branch_names, @project.default_branch) end @@ -23,4 +6,12 @@ module BranchesHelper def protected_branch?(project, branch) ProtectedBranch.protected?(project, branch.name) end + + def diverging_count_label(count) + if count >= Repository::MAX_DIVERGING_COUNT + "#{Repository::MAX_DIVERGING_COUNT - 1}+" + else + count.to_s + end + end end diff --git a/app/helpers/breadcrumbs_helper.rb b/app/helpers/breadcrumbs_helper.rb new file mode 100644 index 00000000000..e88fe6bcd7e --- /dev/null +++ b/app/helpers/breadcrumbs_helper.rb @@ -0,0 +1,33 @@ +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 + + request.path + end + + def breadcrumb_title(title) + return if defined?(@breadcrumb_title) + + @breadcrumb_title = title + end + + def breadcrumb_list_item(link) + content_tag "li" do + link + sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle") + end + end + + def add_to_breadcrumb_dropdown(link, location: :before) + @breadcrumb_dropdown_links ||= {} + @breadcrumb_dropdown_links[location] ||= [] + @breadcrumb_dropdown_links[location] << link + end +end diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index f0a0d245dc0..4ec63fdaffc 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -20,8 +20,7 @@ module BuildsHelper def javascript_build_options { - page_url: namespace_project_job_url(@project.namespace, @project, @build), - build_url: namespace_project_job_url(@project.namespace, @project, @build, :json), + page_path: project_job_path(@project, @build), build_status: @build.status, build_stage: @build.stage, log_state: '' @@ -30,8 +29,8 @@ module BuildsHelper def build_failed_issue_options { - title: "Build Failed ##{@build.id}", - description: namespace_project_job_url(@project.namespace, @project, @build) + title: "Job Failed ##{@build.id}", + description: project_job_url(@project, @build) } end end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index ba84dbe4a7a..3605d6a3c95 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -20,6 +20,9 @@ module ButtonHelper def clipboard_button(data = {}) css_class = data[:class] || 'btn-clipboard btn-transparent' title = data[:title] || 'Copy to clipboard' + button_text = data[:button_text] || '' + hide_tooltip = data[:hide_tooltip] || false + hide_button_icon = data[:hide_button_icon] || false # This supports code in app/assets/javascripts/copy_to_clipboard.js that # works around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM. @@ -35,55 +38,54 @@ module ButtonHelper target = data.delete(:target) data[:clipboard_target] = target if target - data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) + unless hide_tooltip + data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) + end - content_tag :button, - icon('clipboard', 'aria-hidden': 'true'), + button_attributes = { class: "btn #{css_class}", data: data, type: :button, title: title, - aria: { - label: title - } - end + aria: { label: title } + } - def http_clone_button(project, placement = 'right', append_link: true) - klass = 'http-selector' - klass << ' has-tooltip' if current_user.try(:require_password?) || current_user.try(:require_personal_access_token?) + content_tag :button, button_attributes do + concat(icon('clipboard', 'aria-hidden': 'true')) unless hide_button_icon + concat(button_text) + end + end + def http_clone_button(project, append_link: true) protocol = gitlab_config.protocol.upcase + dropdown_description = http_dropdown_description(protocol) + append_url = project.http_url_to_repo if append_link + + dropdown_item_with_description(protocol, dropdown_description, href: append_url) + end + + def http_dropdown_description(protocol) + if current_user.try(:require_password_creation_for_git?) + _("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 + end - tooltip_title = - if current_user.try(:require_password?) - _("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 + def ssh_clone_button(project, append_link: true) + dropdown_description = _("You won't be able to pull or push project code via SSH until you add an SSH key to your profile") if current_user.try(:require_ssh_key?) + append_url = project.ssh_url_to_repo if append_link - content_tag (append_link ? :a : :span), protocol, - class: klass, - href: (project.http_url_to_repo if append_link), - data: { - html: true, - placement: placement, - container: 'body', - title: tooltip_title - } + dropdown_item_with_description('SSH', dropdown_description, href: append_url) end - def ssh_clone_button(project, placement = 'right', append_link: true) - klass = 'ssh-selector' - klass << ' has-tooltip' if current_user.try(:require_ssh_key?) + def dropdown_item_with_description(title, description, href: nil) + button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title') + button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description - content_tag (append_link ? :a : :span), 'SSH', - class: klass, - href: (project.ssh_url_to_repo if append_link), - data: { - html: true, - placement: placement, - container: 'body', - title: _('Add an SSH key to your profile to pull or push via SSH.') - } + content_tag (href ? :a : :span), + (href ? button_content : title), + class: "#{title.downcase}-selector", + href: (href if href) end end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 21c0eb8b54c..636316da80a 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -6,11 +6,6 @@ # See 'detailed_status?` method and `Gitlab::Ci::Status` module. # module CiStatusHelper - def ci_status_path(pipeline) - project = pipeline.project - namespace_project_pipeline_path(project.namespace, project, pipeline) - end - def ci_label_for_status(status) if detailed_status?(status) return status.label @@ -63,34 +58,34 @@ module CiStatusHelper def ci_icon_for_status(status) if detailed_status?(status) - return custom_icon(status.icon) + return sprite_icon(status.icon) end icon_name = case status when 'success' - 'icon_status_success' + 'status_success' when 'success_with_warnings' - 'icon_status_warning' + 'status_warning' when 'failed' - 'icon_status_failed' + 'status_failed' when 'pending' - 'icon_status_pending' + 'status_pending' when 'running' - 'icon_status_running' + 'status_running' when 'play' - 'icon_play' + 'play' when 'created' - 'icon_status_created' + 'status_created' when 'skipped' - 'icon_status_skipped' + 'status_skipped' when 'manual' - 'icon_status_manual' + 'status_manual' else - 'icon_status_canceled' + 'status_canceled' end - custom_icon(icon_name) + sprite_icon(icon_name, size: 16) end def pipeline_status_cache_key(pipeline_status) @@ -99,10 +94,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', @@ -113,10 +105,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', @@ -127,7 +116,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/clusters_helper.rb b/app/helpers/clusters_helper.rb new file mode 100644 index 00000000000..7e4eb06b99d --- /dev/null +++ b/app/helpers/clusters_helper.rb @@ -0,0 +1,5 @@ +module ClustersHelper + def has_multiple_clusters?(project) + false + end +end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 0accd1f8d77..0333c29e2fd 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -30,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 @@ -42,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('/')) ) @@ -61,23 +60,33 @@ module CommitsHelper branches.include?(project.default_branch) ? branches.delete(project.default_branch) : branches.pop end + # Returns a link formatted as a commit branch link + def commit_branch_link(url, text) + link_to(url, class: 'label label-gray ref-name branch-link') do + sprite_icon('fork', size: 16, css_class: 'fork-svg') + "#{text}" + end + end + # Returns the sorted alphabetically links to branches, separated by a comma def commit_branches_links(project, branches) branches.sort.map do |branch| - link_to(project_ref_path(project, branch), class: "label label-gray ref-name") do - icon('code-fork') + " #{branch}" - end - end.join(" ").html_safe + commit_branch_link(project_ref_path(project, branch), branch) + end.join(' ').html_safe + end + + # Returns a link formatted as a commit tag link + def commit_tag_link(url, text) + link_to(url, class: 'label label-gray ref-name') do + icon('tag', class: 'append-right-5') + "#{text}" + end end # Returns the sorted links to tags, separated by a comma def commit_tags_links(project, tags) sorted = VersionSorter.rsort(tags) sorted.map do |tag| - link_to(project_ref_path(project, tag), class: "label label-gray ref-name") do - icon('tag') + " #{tag}" - end - end.join(" ").html_safe + commit_tag_link(project_ref_path(project, tag), tag) + end.join(' ').html_safe end def link_to_browse_code(project, commit) @@ -86,20 +95,20 @@ module CommitsHelper if @path.blank? return link_to( _("Browse Files"), - namespace_project_tree_path(project.namespace, project, commit), + project_tree_path(project, commit), class: "btn btn-default" ) elsif @repo.blob_at(commit.id, @path) return link_to( _("Browse File"), - namespace_project_blob_path(project.namespace, project, + 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, + project_tree_path(project, tree_join(commit.id, @path)), class: "btn btn-default" ) @@ -114,6 +123,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 gpg-status-box) + Array(additional_classes) + end + protected # Private: Returns a link to a person. If the person has a matching user and @@ -125,16 +138,16 @@ 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 text = if options[:avatar] - %Q{<span class="commit-#{options[:source]}-name">#{person_name}</span>} + content_tag(:span, person_name, class: "commit-#{options[:source]}-name") else person_name end @@ -145,9 +158,9 @@ module CommitsHelper } if user.nil? - mail_to(source_email, text.html_safe, options) + mail_to(source_email, text, options) else - link_to(text.html_safe, user_path(user), options) + link_to(text, user_path(user), options) end end @@ -165,7 +178,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) @@ -173,13 +186,15 @@ module CommitsHelper end end - def view_file_button(commit_sha, diff_new_path, project) + def view_file_button(commit_sha, diff_new_path, project, replaced: false) + title = replaced ? _('View replaced file @ ') : _('View file @ ') + 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.truncate_sha(commit_sha), + raw(title) + content_tag(:span, Commit.truncate_sha(commit_sha), class: 'commit-sha') end end @@ -213,4 +228,12 @@ module CommitsHelper [commits, 0] end end + + def commit_path(project, commit, merge_request: nil) + if merge_request&.persisted? + diffs_project_merge_request_path(project, merge_request, commit_id: commit.id) + else + project_commit_path(project, commit) + end + end end diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb index 2aa0449c46e..8bf96c0905f 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -4,13 +4,12 @@ module CompareHelper to.present? && from != to && can?(current_user, :create_merge_request, project) && - project.repository.branch_names.include?(from) && - project.repository.branch_names.include?(to) + project.repository.branch_exists?(from) && + project.repository.branch_exists?(to) 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/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index c25b54eadc6..19aa55a8d49 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -6,4 +6,28 @@ module DashboardHelper def assigned_mrs_dashboard_path merge_requests_dashboard_path(assignee_id: current_user.id) end + + def dashboard_nav_links + @dashboard_nav_links ||= get_dashboard_nav_links + end + + def dashboard_nav_link?(link) + dashboard_nav_links.include?(link) + end + + def any_dashboard_nav_link?(links) + links.any? { |link| dashboard_nav_link?(link) } + end + + private + + def get_dashboard_nav_links + links = [:projects, :groups, :snippets] + + if can?(current_user, :read_cross_project) + links += [:activity, :milestones] + end + + links + 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 16a99addd0b..b5ca39711bc 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -15,7 +15,7 @@ module DiffHelper 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 @@ -33,19 +33,21 @@ module DiffHelper end def diff_match_line(old_pos, new_pos, text: '', view: :inline, bottom: false) - content = content_tag :td, text, class: "line_content match #{view == :inline ? '' : view}" - cls = ['diff-line-num', 'unfold', 'js-unfold'] - cls << 'js-unfold-bottom' if bottom + content_line_class = %w[line_content match] + content_line_class << 'parallel' if view == :parallel + + line_num_class = %w[diff-line-num unfold js-unfold] + line_num_class << 'js-unfold-bottom' if bottom html = '' if old_pos - html << content_tag(:td, '...', class: cls + ['old_line'], data: { linenumber: old_pos }) - html << content unless view == :inline + html << content_tag(:td, '...', class: [*line_num_class, 'old_line'], data: { linenumber: old_pos }) + html << content_tag(:td, text, class: [*content_line_class, 'left-side']) if view == :parallel end if new_pos - html << content_tag(:td, '...', class: cls + ['new_line'], data: { linenumber: new_pos }) - html << content + html << content_tag(:td, '...', class: [*line_num_class, 'new_line'], data: { linenumber: new_pos }) + html << content_tag(:td, text, class: [*content_line_class, ('right-side' if view == :parallel)]) end html.html_safe @@ -88,33 +90,42 @@ 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: 'commit-sha') ].join(' ').html_safe end - def diff_file_blob_raw_path(diff_file) - namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.content_sha, diff_file.file_path)) + def diff_file_blob_raw_url(diff_file, only_path: false) + project_raw_url(@project, tree_join(diff_file.content_sha, diff_file.file_path), only_path: only_path) end - def diff_file_old_blob_raw_path(diff_file) + def diff_file_old_blob_raw_url(diff_file, only_path: false) sha = diff_file.old_content_sha return unless sha - namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.old_content_sha, diff_file.old_path)) + + project_raw_url(@project, tree_join(diff_file.old_content_sha, diff_file.old_path), only_path: only_path) + end + + def diff_file_blob_raw_path(diff_file) + diff_file_blob_raw_url(diff_file, only_path: true) + end + + def diff_file_old_blob_raw_path(diff_file) + diff_file_old_blob_raw_url(diff_file, only_path: true) 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 } @@ -142,12 +153,30 @@ module DiffHelper diff_file = viewer.diff_file options = [] - blob_url = namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.content_sha, diff_file.file_path)) + 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? + "file-deletion" + elsif diff_file.new_file? + "file-addition" + else + "file-modified" + 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 def diff_btn(title, name, selected) @@ -163,17 +192,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 @@ -197,4 +226,12 @@ module DiffHelper diffs.overflow? end + + def diff_file_path_text(diff_file, max: 60) + path = diff_file.new_path + + return path unless path.size > max && max > 3 + + "...#{path[-(max - 3)..-1]}" + end end diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index ac8c518ac84..5089da519df 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -48,11 +48,11 @@ module DropdownsHelper 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' @@ -86,9 +97,11 @@ module DropdownsHelper end end - def dropdown_footer(&block) + def dropdown_footer(add_content_class: false, &block) content_tag(:div, class: "dropdown-footer") do - if block + if add_content_class + content_tag(:div, capture(&block), class: "dropdown-footer-content") + else capture(&block) end end diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index fdbca789d21..4ddc1dbed49 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -24,6 +24,7 @@ module EmailsHelper def action_title(url) return unless url + %w(merge_requests issues commit).each do |action| if url.split("/").include?(action) return "View #{action.humanize.singularize}" @@ -61,8 +62,8 @@ module EmailsHelper else image_tag( image_url('mailers/gitlab_header_logo.gif'), - size: "55x50", - alt: "GitLab" + size: '55x50', + alt: 'GitLab' ) end end @@ -79,4 +80,20 @@ module EmailsHelper 'text-align:center' ].join(';') end + + # "You are receiving this email because #{reason}" + def notification_reason_text(reason) + string = case reason + when NotificationReason::OWN_ACTIVITY + 'of your activity' + when NotificationReason::ASSIGNED + 'you have been assigned an item' + when NotificationReason::MENTIONED + 'you have been mentioned' + else + 'of your account' + end + + "#{string} on #{Gitlab.config.gitlab.host}" + 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 751d61955b7..079b3cd3aa0 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -1,13 +1,15 @@ module EventsHelper ICON_NAMES_BY_EVENT_TYPE = { - 'pushed to' => 'icon_commit', - 'pushed new' => 'icon_commit', - 'created' => 'icon_status_open', - 'opened' => 'icon_status_open', - 'closed' => 'icon_status_closed', - 'accepted' => 'icon_code_fork', - 'commented on' => 'icon_comment_o', - 'deleted' => 'icon_trash_o' + 'pushed to' => 'commit', + 'pushed new' => 'commit', + 'created' => 'status_open', + 'opened' => 'status_open', + 'closed' => 'status_closed', + 'accepted' => 'fork', + 'commented on' => 'comment', + 'deleted' => 'remove', + 'imported' => 'import', + 'joined' => 'users' }.freeze def link_to_author(event, self_added: false) @@ -35,18 +37,18 @@ module EventsHelper [event.action_name, target].join(" ") end - def event_filter_link(key, tooltip) + def event_filter_link(key, text, tooltip) key = key.to_s active = 'active' if @event_filter.active?(key) link_opts = { - class: "event-filter-link", + class: "event-filter-link has-tooltip", id: "#{key}_event_filter", - title: "Filter by #{tooltip.downcase}" + title: tooltip } content_tag :li, class: active do link_to request.path, link_opts do - content_tag(:span, ' ' + tooltip) + content_tag(:span, ' ' + text) end end end @@ -99,13 +101,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 @@ -119,15 +120,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 @@ -146,15 +147,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], @@ -177,17 +172,8 @@ module EventsHelper end end - def event_note(text, options = {}) - text = first_line_in_markdown(text, 150, options) - - sanitize( - text, - tags: %w(a img gl-emoji b pre code p span), - attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-name', 'data-unicode-version'] - ) - end - def event_commit_title(message) + message ||= '' (message.split("\n").first || "").truncate(70) rescue "--broken encoding" @@ -203,7 +189,7 @@ module EventsHelper def icon_for_event(note) icon_name = ICON_NAMES_BY_EVENT_TYPE[note] - custom_icon(icon_name) if icon_name + sprite_icon(icon_name) if icon_name end def icon_for_profile_event(event) diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb index b981a1e8242..f062a91a166 100644 --- a/app/helpers/explore_helper.rb +++ b/app/helpers/explore_helper.rb @@ -25,8 +25,24 @@ module ExploreHelper controller.class.name.split("::").first == "Explore" end + def explore_nav_links + @explore_nav_links ||= get_explore_nav_links + end + + def explore_nav_link?(link) + explore_nav_links.include?(link) + end + + def any_explore_nav_link?(links) + links.any? { |link| explore_nav_link?(link) } + end + private + def get_explore_nav_links + [:projects, :groups, :snippets] + end + def request_path_with_options(options = {}) request.path + "?#{options.to_param}" end 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 8ceb5c36bda..905e2002592 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -1,9 +1,9 @@ module FormHelper - def form_errors(model) + def form_errors(model, type: 'form') return unless model.errors.any? pluralized = 'error'.pluralize(model.errors.count) - headline = "The form contains the following #{pluralized}:" + headline = "The #{type} contains the following #{pluralized}:" content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do content_tag(:h4, headline) << @@ -16,8 +16,8 @@ module FormHelper end end - def issue_dropdown_options(issuable, has_multiple_assignees = true) - options = { + def issue_assignees_dropdown_options + { toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data', title: 'Select assignee', filter: true, @@ -27,24 +27,16 @@ module FormHelper first_user: current_user&.username, null_user: true, current_user: true, - project_id: issuable.project.try(:id), - field_name: "#{issuable.class.model_name.param_key}[assignee_ids][]", + 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]) + current_user_info: UserSerializer.new.represent(current_user) } } - - if has_multiple_assignees - options[:title] = 'Select assignee(s)' - options[:data][:'dropdown-header'] = 'Assignee(s)' - options[:data].delete(:'max-select') - end - - options end end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 8c7af62e199..7f3c118c7ab 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -1,144 +1,91 @@ -# 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 - - def edit_project_path(project, *args) - edit_namespace_project_path(project.namespace, project, *args) - end - - def edit_project_url(project, *args) - edit_namespace_project_url(project.namespace, project, *args) - end + extend ActiveSupport::Concern - def project_files_path(project, *args) - namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref) + included do + Gitlab::Routing.includes_helpers(self) 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) + # 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_jobs_path(project, *args) - namespace_project_jobs_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 project_ref_path(project, ref_name, *args) - namespace_project_commits_path(project.namespace, project, ref_name, *args) - end - - def project_container_registry_path(project, *args) - namespace_project_container_registry_index_path(project.namespace, project, *args) - end - - def activity_project_path(project, *args) - activity_namespace_project_path(project.namespace, project, *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_job_url(pipeline, build, *args) - namespace_project_job_url(pipeline.project.namespace, pipeline.project, build.id, *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) - end + def preview_markdown_path(parent, *args) + return group_preview_markdown_path(parent) if parent.is_a?(Group) - def preview_markdown_path(project, *args) if @snippet.is_a?(PersonalSnippet) preview_markdown_snippets_path else - preview_markdown_namespace_project_path(project.namespace, project, *args) + preview_markdown_project_path(parent, *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 @@ -152,32 +99,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 @@ -211,50 +153,42 @@ 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_job_artifacts_path(*args) + download_project_job_artifacts_path(*args) when 'browse' - browse_namespace_project_job_artifacts_path(*args) + browse_project_job_artifacts_path(*args) when 'file' - file_namespace_project_job_artifacts_path(*args) + file_project_job_artifacts_path(*args) when 'raw' - raw_namespace_project_job_artifacts_path(*args) + raw_project_job_artifacts_path(*args) end end # Pipeline Schedules def pipeline_schedules_path(project, *args) - namespace_project_pipeline_schedules_path(project.namespace, project, *args) + project_pipeline_schedules_path(project, *args) end def pipeline_schedule_path(schedule, *args) project = schedule.project - namespace_project_pipeline_schedule_path(project.namespace, project, schedule, *args) + project_pipeline_schedule_path(project, schedule, *args) end def edit_pipeline_schedule_path(schedule) project = schedule.project - edit_namespace_project_pipeline_schedule_path(project.namespace, project, schedule) + edit_project_pipeline_schedule_path(project, schedule) end - def take_ownership_pipeline_schedule_path(schedule, *args) + def play_pipeline_schedule_path(schedule, *args) project = schedule.project - take_ownership_namespace_project_pipeline_schedule_path(project.namespace, project, schedule, *args) - end - - # Settings - def project_settings_integrations_path(project, *args) - namespace_project_settings_integrations_path(project.namespace, project, *args) + play_project_pipeline_schedule_path(project, schedule, *args) end - def project_settings_members_path(project, *args) - namespace_project_settings_members_path(project.namespace, project, *args) - 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 2e9b72e9613..1022070ab6f 100644 --- a/app/helpers/graph_helper.rb +++ b/app/helpers/graph_helper.rb @@ -1,13 +1,10 @@ module GraphHelper - def get_refs(repo, commit) - 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/') } - refs << commit_refs.join(' ') + def refs(repo, commit) + refs = commit.ref_names(repo).join(' ') # append note count - refs << "[#{@graph.notes[commit.id]}]" if @graph.notes[commit.id] > 0 + notes_count = @graph.notes[commit.id] + refs << "[#{pluralize(notes_count, 'note')}]" if notes_count > 0 refs end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index eb45241615f..16eceb3f48f 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -1,9 +1,44 @@ module GroupsHelper + def group_nav_link_paths + %w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index] + end + + def group_sidebar_links + @group_sidebar_links ||= get_group_sidebar_links + end + + def group_sidebar_link?(link) + group_sidebar_links.include?(link) + end + def can_change_group_visibility_level?(group) can?(current_user, :change_visibility_level, group) end - def group_icon(group) + def can_change_share_with_group_lock?(group) + can?(current_user, :change_share_with_group_lock, group) + end + + def group_issues_count(state:) + IssuesFinder + .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true) + .execute + .count + end + + def group_merge_requests_count(state:) + MergeRequestsFinder + .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true) + .execute + .count + end + + def group_icon(group, options = {}) + img_path = group_icon_url(group, options) + image_tag img_path, options + end + + def group_icon_url(group, options = {}) if group.is_a?(String) group = Group.find_by_full_path(group) end @@ -15,17 +50,20 @@ module GroupsHelper @has_group_title = true full_title = '' - group.ancestors.reverse.each do |parent| - full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable') - full_title += '<span class="hidable"> / </span>'.html_safe + group.ancestors.reverse.each_with_index do |parent, index| + if index > 0 + add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true, for_dropdown: true), location: :before) + else + full_title += breadcrumb_list_item group_title_link(parent, hidable: false) + end end - full_title += link_to(simple_sanitize(group.name), group_path(group), class: 'group-path') - full_title += ' · '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name + full_title += render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :before, title: _("Show parent subgroups") - content_tag :span, class: 'group-title' do - full_title.html_safe - end + full_title += breadcrumb_list_item group_title_link(group) + full_title += ' · '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path breadcrumb-item-text js-breadcrumb-item-text') if name + + full_title.html_safe end def projects_lfs_status(group) @@ -53,7 +91,106 @@ module GroupsHelper end end - def group_issues(group) - IssuesFinder.new(current_user, group_id: group.id).execute + 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 + + def share_with_group_lock_help_text(group) + return default_help unless group.parent&.share_with_group_lock? + + if group.share_with_group_lock? + if can?(current_user, :change_share_with_group_lock, group.parent) + ancestor_locked_but_you_can_override(group) + else + ancestor_locked_so_ask_the_owner(group) + end + else + ancestor_locked_and_has_been_overridden(group) + end + end + + def parent_group_options(current_group) + groups = current_user.owned_groups.sort_by(&:human_name).map do |group| + { id: group.id, text: group.human_name } + end + + groups.delete_if { |group| group[:id] == current_group.id } + groups.to_json + end + + def supports_nested_groups? + Group.supports_nested_groups? + end + + private + + def get_group_sidebar_links + links = [:overview, :group_members] + + if can?(current_user, :read_cross_project) + links += [:activity, :issues, :boards, :labels, :milestones, :merge_requests] + end + + if can?(current_user, :admin_group, @group) + links << :settings + end + + links + end + + def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false) + link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do + output = + if (group.try(:avatar_url) || show_avatar) && !Rails.env.test? + group_icon(group, class: "avatar-tile", width: 15, height: 15) + else + "" + end + + output << simple_sanitize(group.name) + output.html_safe + end + end + + def ancestor_group(group) + ancestor = oldest_consecutively_locked_ancestor(group) + if can?(current_user, :read_group, ancestor) + link_to ancestor.name, group_path(ancestor) + else + ancestor.name + end + end + + def remove_the_share_with_group_lock_from_ancestor(group) + ancestor = oldest_consecutively_locked_ancestor(group) + text = s_("GroupSettings|remove the share with group lock from %{ancestor_group_name}") % { ancestor_group_name: ancestor.name } + if can?(current_user, :admin_group, ancestor) + link_to text, edit_group_path(ancestor) + else + text + end + end + + def oldest_consecutively_locked_ancestor(group) + group.ancestors.find do |group| + !group.has_parent? || !group.parent.share_with_group_lock? + end + end + + def default_help + s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually.") + end + + def ancestor_locked_but_you_can_override(group) + s_("GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}.").html_safe % { ancestor_group: ancestor_group(group), remove_ancestor_share_with_group_lock: remove_the_share_with_group_lock_from_ancestor(group) } + end + + def ancestor_locked_so_ask_the_owner(group) + s_("GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}.").html_safe % { ancestor_group: ancestor_group(group), remove_ancestor_share_with_group_lock: remove_the_share_with_group_lock_from_ancestor(group) } + end + + def ancestor_locked_and_has_been_overridden(group) + s_("GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup.").html_safe % { ancestor_group: ancestor_group(group) } 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 f29faeca22d..c5522ff7a69 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. @@ -16,6 +17,32 @@ module IconsHelper options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options) end + def custom_icon(icon_name, size: 16) + # We can't simply do the below, because there are some .erb SVGs. + # File.read(Rails.root.join("app/views/shared/icons/_#{icon_name}.svg")).html_safe + render "shared/icons/#{icon_name}.svg", size: size + end + + def sprite_icon_path + # SVG Sprites currently don't work across domains, so in the case of a CDN + # we have to set the current path deliberately to prevent addition of asset_host + sprite_base_url = Gitlab.config.gitlab.url if ActionController::Base.asset_host + ActionController::Base.helpers.image_path('icons.svg', host: sprite_base_url) + end + + def sprite_file_icons_path + # SVG Sprites currently don't work across domains, so in the case of a CDN + # we have to set the current path deliberately to prevent addition of asset_host + sprite_base_url = Gitlab.config.gitlab.url if ActionController::Base.asset_host + ActionController::Base.helpers.image_path('file_icons.svg', host: sprite_base_url) + end + + def sprite_icon(icon_name, size: nil, css_class: nil) + css_classes = size ? "s#{size}" : "" + css_classes << " #{css_class}" unless css_class.blank? + content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes) + end + def audit_icon(names, options = {}) case names when "standard" diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index a57b5a8fea5..4664b1728c4 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -1,29 +1,94 @@ module ImportHelper + include ::Gitlab::Utils::StrongMemoize + + def has_ci_cd_only_params? + false + end + def import_project_target(owner, name) namespace = current_user.can_create_group? ? owner : current_user.namespace_path "#{namespace}/#{name}" end - def provider_project_link(provider, path_with_namespace) - url = __send__("#{provider}_project_url", path_with_namespace) + def provider_project_link(provider, full_path) + url = __send__("#{provider}_project_url", full_path) # rubocop:disable GitlabSecurity/PublicSend + + link_to full_path, url, target: '_blank', rel: 'noopener noreferrer' + end + + def import_will_timeout_message(_ci_cd_only) + timeout = time_interval_in_words(Gitlab.config.gitlab_shell.git_timeout) + _('The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination.') % { timeout: timeout } + end + + def import_svn_message(_ci_cd_only) + svn_link = link_to _('this document'), help_page_path('user/project/import/svn') + _('To import an SVN repository, check out %{svn_link}.').html_safe % { svn_link: svn_link } + end + + def import_in_progress_title + if @project.forked? + _('Forking in progress') + else + _('Import in progress') + end + end + + def import_wait_and_refresh_message + _('Please wait while we import the repository for you. Refresh at will.') + end + + def import_github_title + _('Import repositories from GitHub') + end + + def import_github_authorize_message + _('To import GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:') + end + + def import_github_personal_access_token_message + personal_access_token_link = link_to _('Personal Access Token'), 'https://github.com/settings/tokens' + + if github_import_configured? + _('Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import.').html_safe % { personal_access_token_link: personal_access_token_link } + else + _('To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import.').html_safe % { personal_access_token_link: personal_access_token_link } + end + end + + def import_configure_github_admin_message + github_integration_link = link_to 'GitHub integration', help_page_path('integration/github') + + if current_user.admin? + _('Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token.').html_safe % { github_integration_link: github_integration_link } + else + _('Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token.').html_safe % { github_integration_link: github_integration_link } + end + end + + def import_githubish_choose_repository_message + _('Choose which repositories you want to import.') + end - link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer' + def import_all_githubish_repositories_button_label + _('Import all repositories') end private - def github_project_url(path_with_namespace) - "#{github_root_url}/#{path_with_namespace}" + def github_project_url(full_path) + URI.join(github_root_url, full_path).to_s end def github_root_url - return @github_url if defined?(@github_url) + strong_memoize(:github_url) do + provider = Gitlab::Auth::OAuth::Provider.config_for('github') - provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' } - @github_url = provider.fetch('url', 'https://github.com') if provider + provider&.dig('url').presence || 'https://github.com' + end end - def gitea_project_url(path_with_namespace) - "#{@gitea_host_url.sub(%r{/+\z}, '')}/#{path_with_namespace}" + def gitea_project_url(full_path) + URI.join(@gitea_host_url, full_path).to_s end end diff --git a/app/helpers/instance_configuration_helper.rb b/app/helpers/instance_configuration_helper.rb new file mode 100644 index 00000000000..cee319f20bc --- /dev/null +++ b/app/helpers/instance_configuration_helper.rb @@ -0,0 +1,18 @@ +module InstanceConfigurationHelper + def instance_configuration_cell_html(value, &block) + return '-' unless value.to_s.presence + + block_given? ? yield(value) : value + end + + def instance_configuration_host(host) + @instance_configuration_host ||= instance_configuration_cell_html(host).capitalize + end + + # Value must be in bytes + def instance_configuration_human_size_cell(value) + instance_configuration_cell_html(value) do |v| + number_to_human_size(v, strip_insignificant_zeros: true, significant: false) + end + end +end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 3259a9c1933..6d6b840f485 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -26,22 +26,24 @@ 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 - def serialize_issuable(issuable) - case issuable - when Issue - IssueSerializer.new.represent(issuable).to_json - when MergeRequest - MergeRequestSerializer - .new(current_user: current_user, project: issuable.project) - .represent(issuable) - .to_json - end + def serialize_issuable(issuable, serializer: nil) + serializer_klass = case issuable + when Issue + IssueSerializer + when MergeRequest + MergeRequestSerializer + end + + serializer_klass + .new(current_user: current_user, project: issuable.project) + .represent(issuable, serializer: serializer) + .to_json end def template_dropdown_tag(issuable, &block) @@ -97,7 +99,7 @@ module IssuablesHelper project = Project.find_by(id: project_id) if project - project.name_with_namespace + project.full_name else default_label end @@ -126,22 +128,20 @@ module IssuablesHelper end def issuable_meta(issuable, project, text) - output = content_tag(:strong, class: "identifier") do - concat("#{text} ") - concat(to_url_reference(issuable)) - end - - output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe + output = "" + output << "Opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe output << content_tag(:strong) do author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true) author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg") end output << " ".html_safe + output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip', title: _('1st contribution!')) + 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 + output.html_safe end def issuable_todo(issuable) @@ -151,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? @@ -165,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') @@ -177,8 +173,22 @@ module IssuablesHelper html.html_safe end + def issuable_first_contribution_icon + content_tag(:span, class: 'fa-stack') do + concat(icon('certificate', class: "fa-stack-2x")) + concat(content_tag(:strong, '1', class: 'fa-inverse fa-stack-1x')) + end + 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 @@ -201,18 +211,14 @@ module IssuablesHelper def issuable_initial_data(issuable) data = { - endpoint: namespace_project_issue_path(@project.namespace, @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, + endpoint: issuable_path(issuable), + updateEndpoint: "#{issuable_path(issuable)}.json", + canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable), + canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable), 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), + markdownPreviewPath: preview_markdown_path(parent), + markdownDocsPath: help_page_path('user/markdown'), 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), @@ -220,16 +226,22 @@ module IssuablesHelper initialTaskStatus: issuable.task_status } + if parent.is_a?(Group) + data[:groupPath] = parent.path + else + data.merge!(projectPath: ref_project.path, projectNamespace: ref_project.namespace.full_path) + end + data.merge!(updated_at_by(issuable)) - data.to_json + data end def updated_at_by(issuable) - return {} unless issuable.is_edited? + return {} unless issuable.edited? { - updatedAt: issuable.updated_at.to_time.iso8601, + updatedAt: issuable.last_edited_at.to_time.iso8601, updatedBy: { name: issuable.last_edited_by.name, path: user_path(issuable.last_edited_by) @@ -237,64 +249,85 @@ module IssuablesHelper } end - private + def issuables_count_for_state(issuable_type, state) + Gitlab::IssuablesCountForState.new(finder)[state] + end - def sidebar_gutter_collapsed? - cookies[:collapsed_gutter] == 'true' + def close_issuable_path(issuable) + issuable_path(issuable, close_reopen_params(issuable, :close)) end - def base_issuable_scope(issuable) - issuable.project.send(issuable.class.table_name).send(issuable_state_scope(issuable)) + def reopen_issuable_path(issuable) + issuable_path(issuable, close_reopen_params(issuable, :reopen)) end - def issuable_state_scope(issuable) - if issuable.respond_to?(:merged?) && issuable.merged? - :merged - else - issuable.open? ? :opened : :closed + def close_reopen_issuable_path(issuable, should_inverse = false) + issuable.closed? ^ should_inverse ? reopen_issuable_path(issuable) : close_issuable_path(issuable) + end + + def issuable_path(issuable, *options) + polymorphic_path(issuable, *options) + end + + def issuable_url(issuable, *options) + case issuable + when Issue + issue_url(issuable, *options) + when MergeRequest + merge_request_url(issuable, *options) end 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 issuable_button_visibility(issuable, closed) + case issuable + when Issue + issue_button_visibility(issuable, closed) + when MergeRequest + merge_request_button_visibility(issuable, closed) + end end - IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze - private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY + def issuable_close_reopen_button_method(issuable) + case issuable + when Issue + '' + when MergeRequest + 'put' + end + end + + def issuable_author_is_current_user(issuable) + issuable.author == current_user + end + + def issuable_display_type(issuable) + issuable.model_name.human.downcase + end + + def selected_labels + Array(params[:label_name]).map do |label_name| + Label.new(title: label_name) + end + end - def 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? } + private - hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-')) + def sidebar_gutter_collapsed? + cookies[:collapsed_gutter] == 'true' end def issuable_templates(issuable) @issuable_templates ||= case issuable when Issue - issue_template_names + ref_project.repository.issue_template_names when MergeRequest - merge_request_template_names - else - raise 'Unknown issuable type!' + ref_project.repository.merge_request_template_names end end - def merge_request_template_names - @merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project) - end - - def issue_template_names - @issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project) - 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) @@ -305,10 +338,50 @@ 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 labels_path + if @project + project_labels_path(@project) + elsif @group + group_labels_path(@group) + end + end + + def issuable_sidebar_options(issuable, can_edit_issuable) + { + endpoint: "#{issuable_json_path(issuable)}?serializer=sidebar", + toggleSubscriptionEndpoint: toggle_subscription_path(issuable), + moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable), + projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id), + editable: can_edit_issuable, + currentUser: UserSerializer.new.represent(current_user), + rootPath: root_path, + fullPath: @project.full_path + } + end + + def parent + @project || @group + end + + def issuable_milestone_tooltip_title(issuable) + if issuable.milestone + milestone_tooltip = milestone_tooltip_title(issuable.milestone) + _('Milestone') + (milestone_tooltip ? ': ' + milestone_tooltip : '') + end + end end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 82288f1da35..0f25d401406 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,41 +29,31 @@ module IssuesHelper '' end - def bulk_update_milestone_options - milestones = @project.milestones.active.reorder(due_date: :asc, title: :asc).to_a - milestones.unshift(Milestone::None) - - options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id]) + 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 milestone_options(object) - milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a - milestones.unshift(object.milestone) if object.milestone.present? && object.milestone.closed? - milestones.unshift(Milestone::None) + def url_for_internal_issue(issue_iid, project = @project, options = {}) + helpers = Gitlab::Routing.url_helpers - options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id) - end - - def project_options(issuable, current_user, ability: :read_project) - projects = current_user.authorized_projects - projects = projects.select do |project| - current_user.can?(ability, project) + 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 - - no_project = OpenStruct.new(id: 0, name_with_namespace: 'No project') - projects.unshift(no_project) - projects.delete(issuable.project) - - options_from_collection_for_select(projects, :id, :name_with_namespace) end def status_box_class(item) if item.try(:expired?) 'status-box-expired' elsif item.try(:merged?) - 'status-box-merged' + 'status-box-mr-merged' elsif item.closed? - 'status-box-closed' + 'status-box-mr-closed' elsif item.try(:upcoming?) 'status-box-upcoming' else @@ -75,14 +65,6 @@ module IssuesHelper return 'hidden' if issue.closed? == closed end - def merge_requests_sentence(merge_requests) - # Sorting based on the `!123` or `group/project!123` reference will sort - # local merge requests first. - merge_requests.map do |merge_request| - merge_request.to_reference(@project) - end.sort.to_sentence(last_word_connector: ', or ') - end - def confidential_icon(issue) icon('eye-slash') if issue.confidential? end @@ -119,7 +101,7 @@ module IssuesHelper end def awards_sort(awards) - awards.sort_by do |award, notes| + awards.sort_by do |award, award_emojis| if award == "thumbsup" 0 elsif award == "thumbsdown" @@ -130,18 +112,6 @@ module IssuesHelper end.to_h end - def due_date_options - options = [ - Issue::AnyDueDate, - Issue::NoDueDate, - Issue::DueThisWeek, - Issue::DueThisMonth, - Issue::Overdue - ] - - options_from_collection_for_select(options, 'name', 'title', params[:due_date]) - end - def link_to_discussions_to_resolve(merge_request, single_discussion = nil) link_text = merge_request.to_reference link_text += " (discussion #{single_discussion.first_note.id})" if single_discussion @@ -150,7 +120,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 +128,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/javascript_helper.rb b/app/helpers/javascript_helper.rb index d5e77c7e271..cd4075b340d 100644 --- a/app/helpers/javascript_helper.rb +++ b/app/helpers/javascript_helper.rb @@ -2,9 +2,4 @@ module JavascriptHelper def page_specific_javascript_tag(js) javascript_include_tag asset_path(js) end - - # deprecated; use webpack_bundle_tag directly instead - def page_specific_javascript_bundle_tag(bundle) - webpack_bundle_tag(bundle) - end end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 6baf6f31d8f..87ff607dc3f 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -1,6 +1,14 @@ module LabelsHelper + extend self include ActionView::Helpers::TagHelper + def show_label_issuables_link?(label, issuables_type, current_user: nil, project: nil) + return true if label.is_a?(GroupLabel) + return true unless project + + project.feature_available?(issuables_type, current_user) + end + # Link to a Label # # label - Label object to link to @@ -43,11 +51,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,14 +65,14 @@ 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 @@ -121,13 +129,14 @@ module LabelsHelper end end - def labels_filter_path - return group_labels_path(@group, :json) if @group - + def labels_filter_path(only_group_labels = false) project = @target_project || @project if project - namespace_project_labels_path(project.namespace, project, :json) + project_labels_path(project, :json) + elsif @group + options = { only_group_labels: only_group_labels } if only_group_labels + group_labels_path(@group, :json, options) else dashboard_labels_path(:json) end @@ -149,8 +158,8 @@ module LabelsHelper case label_subscription_status(label, project) when 'group-level' then toggle_subscription_group_label_path(label.group, label) - when 'project-level' then toggle_subscription_namespace_project_label_path(project.namespace, project, label) - when 'unsubscribed' then toggle_subscription_namespace_project_label_path(project.namespace, project, label) + when 'project-level' then toggle_subscription_project_label_path(project, label) + when 'unsubscribed' then toggle_subscription_project_label_path(project, label) end end @@ -165,6 +174,39 @@ module LabelsHelper end end + def create_label_title(subject) + case subject + when Group + _('Create group label') + when Project + _('Create project label') + else + _('Create new label') + end + end + + def manage_labels_title(subject) + case subject + when Group + _('Manage group labels') + when Project + _('Manage project labels') + else + _('Manage labels') + end + end + + def view_labels_title(subject) + case subject + when Group + _('View group labels') + when Project + _('View project labels') + else + _('View labels') + end + end + # Required for Banzai::Filter::LabelReferenceFilter module_function :render_colored_label, :text_color_for_bg, :escape_once 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..603b9438e35 --- /dev/null +++ b/app/helpers/lazy_image_tag_helper.rb @@ -0,0 +1,25 @@ +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 941cfce8370..2fe1927a189 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -21,25 +21,28 @@ module MarkupHelper end # Use this in places where you would normally use link_to(gfm(...), ...). - # + def link_to_markdown(body, url, html_options = {}) + return '' if body.blank? + + link_to_html(markdown(body, pipeline: :single_line), url, html_options) + end + + def link_to_markdown_field(object, field, url, html_options = {}) + rendered_field = markdown_field(object, field) + + link_to_html(rendered_field, url, html_options) + end + # It solves a problem occurring with nested links (i.e. # "<a>outer text <a>gfm ref</a> more outer text</a>"). This will not be # interpreted as intended. Browsers will parse something like # "<a>outer text </a><a>gfm ref</a> more outer text" (notice the last part is - # not linked any more). link_to_gfm corrects that. It wraps all parts to + # not linked any more). link_to_html corrects that. It wraps all parts to # explicitly produce the correct linking behavior (i.e. # "<a>outer text </a><a>gfm ref</a><a> more outer text</a>"). - def link_to_gfm(body, url, html_options = {}) - return '' if body.blank? - - context = { - project: @project, - current_user: (current_user if defined?(current_user)), - pipeline: :single_line - } - gfm_body = Banzai.render(body, context) + def link_to_html(redacted, url, html_options = {}) + fragment = Nokogiri::HTML::DocumentFragment.parse(redacted) - fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body) if fragment.children.size == 1 && fragment.children[0].name == 'a' # Fragment has only one node, and it's a link generated by `gfm`. # Replace it with our requested link. @@ -50,6 +53,7 @@ module MarkupHelper # text, wrapping anything found in the requested link fragment.children.each do |node| next unless node.text? + node.replace(link_to(node.text, url, html_options)) end end @@ -66,26 +70,39 @@ module MarkupHelper # as Markdown. HTML tags in the parsed output are not counted toward the # +max_chars+ limit. If the length limit falls within a tag's contents, then # the tag contents are truncated without removing the closing tag. - def first_line_in_markdown(text, max_chars = nil, options = {}) - md = markdown(text, options).strip + def first_line_in_markdown(object, attribute, max_chars = nil, options = {}) + md = markdown_field(object, attribute, options) + + text = truncate_visible(md, max_chars || md.length) if md.present? - truncate_visible(md, max_chars || md.length) if md.present? + sanitize( + text, + tags: %w(a img gl-emoji b pre code p span), + attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version'] + ) end def markdown(text, context = {}) return '' unless text.present? context[:project] ||= @project + context[:group] ||= @group + html = markdown_unsafe(text, context) prepare_for_rendering(html, context) end - def markdown_field(object, field) + def markdown_field(object, field, context = {}) object = object.for_display if object.respond_to?(:for_display) + redacted_field_html = object.try(:"redacted_#{field}_html") + return '' unless object.present? + return redacted_field_html if redacted_field_html - html = Banzai.render_field(object, field) - prepare_for_rendering(html, object.banzai_render_context(field)) + html = Banzai.render_field(object, field, context) + context.reverse_merge!(object.banzai_render_context(field)) if object.respond_to?(:banzai_render_context) + + prepare_for_rendering(html, context) end def markup(file_name, text, context = {}) @@ -98,7 +115,13 @@ module MarkupHelper text = wiki_page.content return '' unless text.present? - context = { pipeline: :wiki, project: @project, project_wiki: @project_wiki, page_slug: wiki_page.slug } + context = { + pipeline: :wiki, + project: @project, + project_wiki: @project_wiki, + page_slug: wiki_page.slug, + issuable_state_filter_enabled: true + } html = case wiki_page.format @@ -180,6 +203,7 @@ module MarkupHelper node.content = node.content.truncate(num_remaining) truncated = true end + content_length += node.content.length end @@ -207,12 +231,12 @@ module MarkupHelper data = options[:data].merge({ container: 'body' }) content_tag :button, type: 'button', - class: 'toolbar-btn js-md has-tooltip hidden-xs', + class: 'toolbar-btn js-md has-tooltip', tabindex: -1, data: data, title: options[:title], aria: { label: options[:title] } do - icon(options[:icon]) + sprite_icon(options[:icon]) end end diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index 41d471cc92f..a3129cac2b1 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -1,11 +1,4 @@ module MembersHelper - # Returns a `<action>_<source>_member` association, e.g.: - # - admin_project_member, update_project_member, destroy_project_member - # - admin_group_member, update_group_member, destroy_group_member - def action_member_permission(action, member) - "#{action}_#{member.type.underscore}".to_sym - end - def remove_member_message(member, user: nil) user = current_user if defined?(current_user) diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 39d30631646..fb4fe1c40b7 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.default_merge_request_target - new_namespace_project_merge_request_path( - event.project.namespace, + project_new_merge_request_path( event.project, new_mr_from_push_event(event, target_project) ) @@ -41,15 +40,15 @@ 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 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, @@ -74,7 +73,8 @@ module MergeRequestsHelper end def target_projects(project) - [project, project.default_merge_request_target].uniq + MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: project) + .execute end def merge_request_button_visibility(merge_request, closed) @@ -82,9 +82,7 @@ module MergeRequestsHelper 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) @@ -103,6 +101,43 @@ module MergeRequestsHelper }.merge(merge_params_ee(merge_request)) end + def tab_link_for(merge_request, tab, options = {}, &block) + data_attrs = { + action: tab.to_s, + target: "##{tab}", + toggle: options.fetch(:force_link, false) ? '' : 'tab' + } + + url = case tab + when :show + data_attrs[:target] = '#notes' + method(:project_merge_request_path) + when :commits + method(:commits_project_merge_request_path) + when :pipelines + method(:pipelines_project_merge_request_path) + when :diffs + method(:diffs_project_merge_request_path) + else + raise "Cannot create tab #{tab}." + end + + link_to(url[merge_request.project, merge_request], data: data_attrs, &block) + end + + def allow_maintainer_push_unavailable_reason(merge_request) + return if merge_request.can_allow_maintainer_to_push?(current_user) + + minimum_visibility = [merge_request.target_project.visibility_level, + merge_request.source_project.visibility_level].min + + if minimum_visibility < Gitlab::VisibilityLevel::INTERNAL + _('Not available for private projects') + elsif ProtectedBranch.protected?(merge_request.source_project, merge_request.source_branch) + _('Not available for protected branches') + end + end + def merge_params_ee(merge_request) {} end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index a230db22fa2..be8cb358de2 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,12 +86,20 @@ 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 end + def milestone_tooltip_title(milestone) + if milestone.due_date + [milestone.due_date.to_s(:medium), "(#{milestone_remaining_days(milestone)})"].join(' ') + end + end + def milestone_remaining_days(milestone) if milestone.expired? content_tag(:strong, 'Past due') @@ -118,7 +139,7 @@ module MilestonesHelper def milestone_merge_request_tab_path(milestone) if @project - merge_requests_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json) + 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 @@ -128,7 +149,7 @@ module MilestonesHelper def milestone_participants_tab_path(milestone) if @project - participants_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json) + participants_project_milestone_path(@project, milestone, format: :json) elsif @group participants_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json) else @@ -138,11 +159,21 @@ module MilestonesHelper def milestone_labels_tab_path(milestone) if @project - labels_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json) + 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.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..a0b2616f224 --- /dev/null +++ b/app/helpers/milestones_routing_helper.rb @@ -0,0 +1,17 @@ +module MilestonesRoutingHelper + def milestone_path(milestone, *args) + if milestone.group_milestone? + group_milestone_path(milestone.group, milestone, *args) + elsif milestone.project_milestone? + project_milestone_path(milestone.project, milestone, *args) + end + end + + def milestone_url(milestone, *args) + if milestone.group_milestone? + group_milestone_url(milestone.group, milestone, *args) + elsif milestone.project_milestone? + project_milestone_url(milestone.project, milestone, *args) + end + end +end diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index 7f656b8caae..40ca666f1bf 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -4,7 +4,11 @@ module NamespacesHelper end def namespaces_options(selected = :current_user, display_path: false, extra_group: nil) - groups = current_user.owned_groups + current_user.masters_groups + groups = current_user.manageable_groups + .joins(:route) + .includes(:route) + .order('routes.path') + users = [current_user.namespace] unless extra_group.nil? || extra_group.is_a?(Group) extra_group = Group.find(extra_group) if Namespace.find(extra_group).kind == 'group' @@ -14,22 +18,9 @@ module NamespacesHelper groups |= [extra_group] end - users = [current_user.namespace] - - data_attr_group = { 'data-options-parent' => 'groups' } - data_attr_users = { 'data-options-parent' => 'users' } - - group_opts = [ - "Groups", groups.sort_by(&:human_name).map { |g| [display_path ? g.full_path : g.human_name, g.id, data_attr_group] } - ] - - users_opts = [ - "Users", users.sort_by(&:human_name).map { |u| [display_path ? u.path : u.human_name, u.id, data_attr_users] } - ] - options = [] - options << group_opts - options << users_opts + options << options_for_group(groups, display_path: display_path, type: 'group') + options << options_for_group(users, display_path: display_path, type: 'user') if selected == :current_user && current_user.namespace selected = current_user.namespace.id @@ -42,7 +33,26 @@ module NamespacesHelper if namespace.is_a?(Group) group_icon(namespace) else - avatar_icon(namespace.owner.email, size) + avatar_icon_for_user(namespace.owner, size) end end + + private + + def options_for_group(namespaces, display_path:, type:) + group_label = type.pluralize + elements = namespaces.sort_by(&:human_name).map! do |n| + [display_path ? n.full_path : n.human_name, n.id, + data: { + options_parent: group_label, + visibility_level: n.visibility_level_value, + visibility: n.visibility, + name: n.name, + show_path: (type == 'group') ? group_path(n) : user_path(n), + edit_path: (type == 'group') ? edit_group_path(n) : nil + }] + end + + [group_label.camelize, elements] + end end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 833d3c36b28..56c88e6eab0 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -1,46 +1,73 @@ module NavHelper + def header_links + @header_links ||= get_header_links + end + + def header_link?(link) + header_links.include?(link) + end + + def page_with_sidebar_class + class_name = page_gutter_class + class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar + class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_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?('jobs#show') - "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 build-sidebar right-sidebar-expanded] + elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access') + %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_name << " with-peek" if peek_enabled? - - class_name + def nav_control_class + "nav-control" if current_user 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) + def user_dropdown_class + class_names = [] + class_names << 'header-user-dropdown-toggle' + class_names << 'impersonated-user' if session[:impersonator_id] - class_name + class_names end - def nav_control_class - "nav-control" if current_user + private + + def get_header_links + links = if current_user + [:user_dropdown] + else + [:sign_in] + end + + if can?(current_user, :read_cross_project) + links += [:issues, :merge_requests, :todos] if current_user.present? + end + + if @project&.persisted? || can?(current_user, :read_cross_project) + links << :search + end + + if session[:impersonator_id] + links << :admin_impersonation + end + + links end end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 64ad7b280cb..20aed60cb7a 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -11,7 +11,7 @@ module NotesHelper end def note_supports_quick_actions?(note) - Notes::QuickActionsService.supported?(note, current_user) + Notes::QuickActionsService.supported?(note) end def noteable_json(noteable) @@ -47,17 +47,33 @@ 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.reply_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' end def note_max_access_for_user(note) - note.project.team.human_max_access(note.author_id) + note.project.team.max_member_access(note.author_id) end def discussion_path(discussion) @@ -69,24 +85,21 @@ module NotesHelper path_params = version_params.merge(anchor: discussion.line_code) - diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, path_params) + 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 + def notes_url(params = {}) if @snippet.is_a?(PersonalSnippet) - snippet_notes_path(@snippet) + snippet_notes_path(@snippet, params) else - namespace_project_noteable_notes_path( - namespace_id: @project.namespace, - project_id: @project, - target_id: @noteable.id, - target_type: @noteable.class.name.underscore - ) + params.merge!(target_id: @noteable.id, target_type: @noteable.class.name.underscore) + + project_noteable_notes_path(@project, params) end end @@ -94,7 +107,7 @@ module NotesHelper if note.noteable.is_a?(PersonalSnippet) snippet_note_path(note.noteable, note) else - namespace_project_note_path(project.namespace, project, note) + project_note_path(project, note) end end @@ -117,10 +130,59 @@ module NotesHelper end def can_create_note? + issuable = @issue || @merge_request + if @snippet.is_a?(PersonalSnippet) can?(current_user, :comment_personal_snippet, @snippet) + elsif issuable + can?(current_user, :create_note, issuable) 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 + + def notes_data(issuable) + discussions_path = + if issuable.is_a?(Issue) + discussions_project_issue_path(@project, issuable, format: :json) + else + discussions_project_merge_request_path(@project, issuable, format: :json) + end + + { + discussionsPath: discussions_path, + registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), + newSessionPath: new_session_path(:user, redirect_to_referer: 'yes'), + markdownDocsPath: help_page_path('user/markdown'), + quickActionsDocsPath: help_page_path('user/project/quick_actions'), + closePath: close_issuable_path(issuable), + reopenPath: reopen_issuable_path(issuable), + notesPath: notes_url, + totalNotes: issuable.discussions.length, + lastFetchedAt: Time.now.to_i + + }.to_json + end + + def discussion_resolved_intro(discussion) + discussion.resolved_by_push? ? 'Automatically resolved' : 'Resolved' + end + + def has_vue_discussions_cookie? + cookies[:vue_mr_discussions] == 'true' + end + + def serialize_notes? + has_vue_discussions_cookie? && !params['html'] + end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index fde961e2da4..3e42063224e 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -78,6 +78,7 @@ module NotificationsHelper # Create hidden field to send notification setting source to controller def hidden_setting_source_input(notification_setting) return unless notification_setting.source_type + hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id end diff --git a/app/helpers/numbers_helper.rb b/app/helpers/numbers_helper.rb new file mode 100644 index 00000000000..45bd3606076 --- /dev/null +++ b/app/helpers/numbers_helper.rb @@ -0,0 +1,11 @@ +module NumbersHelper + def limited_counter_with_delimiter(resource, **options) + limit = options.fetch(:limit, 1000).to_i + count = resource.limit(limit + 1).count(:all) + if count > limit + number_with_delimiter(count - 1, options) + '+' + else + number_with_delimiter(count, options) + end + end +end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 7279d5fb2d4..bb782060e9b 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -4,8 +4,12 @@ module PageLayoutHelper @page_title.push(*titles.compact) if titles.any? + if titles.any? && !defined?(@breadcrumb_title) + @breadcrumb_title = @page_title.last + end + # Segments are seperated by middot - @page_title.join(" \u00b7 ") + @page_title.join(" · ") end # Define or get a description for the current page @@ -78,7 +82,9 @@ module PageLayoutHelper @header_title = title @header_title_url = title_url else - @header_title_url ? link_to(@header_title, @header_title_url) : @header_title + return @header_title unless @header_title_url + + breadcrumb_list_item(link_to(@header_title, @header_title_url)) end 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 index fee1edc2a1b..6edaf78de1b 100644 --- a/app/helpers/pipeline_schedules_helper.rb +++ b/app/helpers/pipeline_schedules_helper.rb @@ -1,10 +1,10 @@ module PipelineSchedulesHelper def timezone_data - ActiveSupport::TimeZone.all.map do |timezone| - { - name: timezone.name, - offset: timezone.utc_offset, - identifier: timezone.tzinfo.identifier + ActiveSupport::TimeZone.all.map do |timezone| + { + name: timezone.name, + offset: timezone.utc_offset, + identifier: timezone.tzinfo.identifier } end end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index d36bb4ab074..373dfd457f7 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -36,37 +36,16 @@ module PreferencesHelper def project_view_choices [ ['Files and Readme (default)', :files], - ['Activity', :activity] + ['Activity', :activity], + ['Readme', :readme] ] end - def user_color_scheme - Gitlab::ColorSchemes.for_user(current_user).css_class - end - - def default_project_view - return anonymous_project_view unless current_user - - user_view = current_user.project_view - - if can?(current_user, :download_code, @project) - user_view - elsif user_view == "activity" - "activity" - elsif @project.wiki_enabled? - "wiki" - elsif @project.feature_available?(:issues, current_user) - "projects/issues/issues" - else - "customize_workflow" - end + def user_application_theme + @user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class end - def anonymous_project_view - if !@project.empty_repo? && can?(current_user, :download_code, @project) - 'files' - else - 'activity' - end + def user_color_scheme + Gitlab::ColorSchemes.for_user(current_user).css_class end end diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb index 45238f12ac7..e7aa92e6e5c 100644 --- a/app/helpers/profiles_helper.rb +++ b/app/helpers/profiles_helper.rb @@ -1,7 +1,12 @@ 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" + def attribute_provider_label(attribute) + user_synced_attributes_metadata = current_user.user_synced_attributes_metadata + if user_synced_attributes_metadata&.synced?(attribute) + if user_synced_attributes_metadata.provider + Gitlab::Auth::OAuth::Provider.label_for(user_synced_attributes_metadata.provider) + else + 'LDAP' + end + end end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c04b1419a19..15f48e43a28 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -13,13 +13,38 @@ module ProjectsHelper end def link_to_member_avatar(author, opts = {}) - default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" } + default_opts = { size: 16, lazy_load: false } opts = default_opts.merge(opts) - image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt: '') if opts[:avatar] + + classes = %W[avatar avatar-inline s#{opts[:size]}] + classes << opts[:avatar_class] if opts[:avatar_class] + + avatar = avatar_icon_for_user(author, opts[:size]) + src = opts[:lazy_load] ? nil : avatar + + image_tag(src, width: opts[:size], class: classes, alt: '', "data-src" => avatar) + end + + def author_content_tag(author, opts = {}) + default_opts = { author_class: 'author', tooltip: false, by_username: false } + opts = default_opts.merge(opts) + + has_tooltip = !opts[:by_username] && opts[:tooltip] + + username = opts[:by_username] ? author.to_reference : author.name + name_tag_options = { class: [opts[:author_class]] } + + if has_tooltip + name_tag_options[:title] = author.to_reference + name_tag_options[:data] = { placement: 'top' } + name_tag_options[:class] << 'has-tooltip' + end + + content_tag(:span, sanitize(username), name_tag_options) end def link_to_member(project, author, opts = {}, &block) - default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name", tooltip: false } + default_opts = { avatar: true, name: true, title: ":name" } opts = default_opts.merge(opts) return "(deleted)" unless author @@ -27,15 +52,10 @@ module ProjectsHelper author_html = "" # Build avatar image tag - author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]} #{opts[:avatar_class] if opts[:avatar_class]}", alt: '') if opts[:avatar] + author_html << link_to_member_avatar(author, opts) if opts[:avatar] # Build name span tag - if opts[:by_username] - author_html << content_tag(:span, sanitize("@#{author.username}"), class: opts[:author_class]) if opts[:name] - else - tooltip_data = { placement: 'top' } - author_html << content_tag(:span, sanitize(author.name), class: [opts[:author_class], ('has-tooltip' if opts[:tooltip])], title: (author.to_reference if opts[:tooltip]), data: (tooltip_data if opts[:tooltip])) if opts[:name] - end + author_html << author_content_tag(author, opts) if opts[:name] author_html << capture(&block) if block @@ -52,36 +72,51 @@ module ProjectsHelper def project_title(project) namespace_link = if project.group - group_title(project.group) + group_title(project.group, nil, nil) else owner = project.namespace.owner 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) do + output = + if project.avatar_url && !Rails.env.test? + project_icon(project, alt: project.name, class: 'avatar-tile', width: 15, height: 15) + else + "" + 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 - icon("chevron-down") - end + output << content_tag("span", simple_sanitize(project.name), class: "breadcrumb-item-text js-breadcrumb-item-text") + output.html_safe end - "#{namespace_link} / #{project_link}".html_safe + namespace_link = breadcrumb_list_item(namespace_link) unless project.group + project_link = breadcrumb_list_item project_link + + "#{namespace_link} #{project_link}".html_safe end def remove_project_message(project) - _("You are going to remove %{project_name_with_namespace}.\nRemoved project CANNOT be restored!\nAre you ABSOLUTELY sure?") % - { project_name_with_namespace: project.name_with_namespace } + _("You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") % + { project_full_name: project.full_name } end def transfer_project_message(project) - _("You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?") % - { project_name_with_namespace: project.name_with_namespace } + _("You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?") % + { project_full_name: project.full_name } end def remove_fork_project_message(project) _("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 } + { forked_from_project: fork_source_name(project) } + end + + def fork_source_name(project) + if @project.fork_source + @project.fork_source.full_name + else + @project.fork_network&.deleted_root_project_name + end end def project_nav_tabs @@ -111,43 +146,31 @@ module ProjectsHelper def can_change_visibility_level?(project, current_user) return false unless can?(current_user, :change_visibility_level, project) - if project.forked? - project.forked_from_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE + if project.fork_source + project.fork_source.visibility_level > Gitlab::VisibilityLevel::PRIVATE else true end end - def license_short_name(project) - 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) - project_ids << fork.id - end - - current_user.recent_push(project_ids) + current_user&.recent_push(@project) end def project_feature_access_select(field) # 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) disabled_option = ProjectFeature::ENABLED highest_available_option = ProjectFeature::PRIVATE if level == disabled_option end options = options_for_select( options.invert, - selected: highest_available_option || @project.project_feature.public_send(field), + selected: highest_available_option || level, disabled: disabled_option ) @@ -184,8 +207,9 @@ module ProjectsHelper project.cache_key, controller.controller_name, controller.action_name, - current_application_settings.cache_key, - 'v2.4' + Gitlab::CurrentSettings.cache_key, + "cross-project:#{can?(current_user, :read_cross_project)}", + 'v2.5' ] key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status? @@ -204,17 +228,50 @@ module ProjectsHelper def show_no_password_message? cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && - ( current_user.require_password? || current_user.require_personal_access_token? ) + current_user.require_extra_setup_for_git_auth? end def link_to_set_password - if current_user.require_password? + if current_user.require_password_creation_for_git? 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 show_projects?(projects, params) + !!(params[:personal] || params[:name] || any_projects?(projects)) + end + + def push_to_create_project_command(user = current_user) + repository_url = + if Gitlab::CurrentSettings.current_application_settings.enabled_git_access_protocol == 'http' + user_url(user) + else + Gitlab.config.gitlab_shell.ssh_path_prefix + user.username + end + + "git push --set-upstream #{repository_url}/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)" + end + private def repo_children_classes(field) @@ -246,6 +303,10 @@ module ProjectsHelper nav_tabs << :pipelines end + if project.external_issue_tracker + nav_tabs << :external_issue_tracker + end + tab_ability_map.each do |tab, ability| if can?(current_user, ability, project) nav_tabs << tab @@ -257,15 +318,16 @@ module ProjectsHelper 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, - team: :read_project_member, - wiki: :read_wiki + environments: :read_environment, + milestones: :read_milestone, + snippets: :read_project_snippet, + settings: :admin_project, + builds: :read_build, + clusters: :read_cluster, + labels: :read_label, + issues: :read_issue, + project_members: :read_project_member, + wiki: :read_wiki } end @@ -292,7 +354,7 @@ module ProjectsHelper def git_user_name if current_user - current_user.name + current_user.name.gsub('"', '\"') else _("Your name") end @@ -335,107 +397,25 @@ module ProjectsHelper end end - def add_special_file_path(project, file_name:, commit_message: nil, branch_name: nil, context: nil) - commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name.downcase } - namespace_project_new_blob_path( - project.namespace, - project, - project.default_branch || 'master', - file_name: file_name, - 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, - project.default_branch || 'master', - file_name: '.koding.yml', - commit_message: "Add Koding stack script", - content: <<-CONTENT.strip_heredoc - provider: - aws: - access_key: '${var.aws_access_key}' - secret_key: '${var.aws_secret_key}' - resource: - aws_instance: - #{project.path}-vm: - instance_type: t2.nano - user_data: |- - - # Created by GitLab UI for :> - - echo _KD_NOTIFY_@Installing Base packages...@ - - apt-get update -y - apt-get install git -y - - echo _KD_NOTIFY_@Cloning #{project.name}...@ - - export KODING_USER=${var.koding_user_username} - export REPO_URL=#{root_url}${var.koding_queryString_repo}.git - export BRANCH=${var.koding_queryString_branch} - - sudo -i -u $KODING_USER git clone $REPO_URL -b $BRANCH - - echo _KD_NOTIFY_@#{project.name} cloned.@ - CONTENT - ) - end - def koding_project_url(project = nil, branch = nil, sha = nil) 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 path = "#{import_path}?repo=#{repo}&branch=#{branch}&sha=#{sha}" - return URI.join(current_application_settings.koding_url, path).to_s - end - - current_application_settings.koding_url - end - - def contribution_guide_path(project) - if project && contribution_guide = project.repository.contribution_guide - namespace_project_blob_path( - project.namespace, - project, - tree_join(project.default_branch, - contribution_guide.name) - ) + return URI.join(Gitlab::CurrentSettings.koding_url, path).to_s end - end - - def readme_path(project) - filename_path(project, :readme) - end - def changelog_path(project) - filename_path(project, :changelog) - end - - def license_path(project) - filename_path(project, :license_blob) - end - - def version_path(project) - filename_path(project, :version) - end - - def ci_configuration_path(project) - filename_path(project, :gitlab_ci_yml) + Gitlab::CurrentSettings.koding_url end 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) @@ -451,23 +431,13 @@ 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 @ref || @repository.try(:root_ref) end - def filename_path(project, filename) - if project && blob = project.repository.send(filename) - namespace_project_blob_path( - project.namespace, - project, - tree_join(project.default_branch, blob.name) - ) - end - end - def sanitize_repo_path(project, message) return '' unless message.present? @@ -510,6 +480,73 @@ module ProjectsHelper def restricted_levels return [] if current_user.admin? - current_application_settings.restricted_visibility_levels || [] + Gitlab::CurrentSettings.restricted_visibility_levels || [] + end + + def project_permissions_settings(project) + feature = project.project_feature + { + visibilityLevel: project.visibility_level, + requestAccessEnabled: !!project.request_access_enabled, + issuesAccessLevel: feature.issues_access_level, + repositoryAccessLevel: feature.repository_access_level, + mergeRequestsAccessLevel: feature.merge_requests_access_level, + buildsAccessLevel: feature.builds_access_level, + wikiAccessLevel: feature.wiki_access_level, + snippetsAccessLevel: feature.snippets_access_level, + containerRegistryEnabled: !!project.container_registry_enabled, + lfsEnabled: !!project.lfs_enabled + } + end + + def project_permissions_panel_data(project) + data = { + currentSettings: project_permissions_settings(project), + canChangeVisibilityLevel: can_change_visibility_level?(project, current_user), + allowedVisibilityOptions: project_allowed_visibility_levels(project), + visibilityHelpPath: help_page_path('public_access/public_access'), + registryAvailable: Gitlab.config.registry.enabled, + registryHelpPath: help_page_path('user/project/container_registry'), + lfsAvailable: Gitlab.config.lfs.enabled && current_user.admin?, + lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + } + + data.to_json.html_safe + end + + def project_allowed_visibility_levels(project) + Gitlab::VisibilityLevel.values.select do |level| + project.visibility_level_allowed?(level) && !restricted_levels.include?(level) + end + end + + def find_file_path + return unless @project && !@project.empty_repo? + + ref = @ref || @project.repository.root_ref + + project_find_file_path(@project, ref) + end + + def can_show_last_commit_in_list?(project) + can?(current_user, :read_cross_project) && project.commit + end + + def pages_https_only_disabled? + !@project.pages_domains.all?(&:https?) + end + + def pages_https_only_title + return unless pages_https_only_disabled? + + "You must enable HTTPS for all your domains first" + end + + def pages_https_only_label_class + if pages_https_only_disabled? + "list-label disabled" + else + "list-label" + end end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 8f15904f068..761c1252fc8 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -10,6 +10,7 @@ module SearchHelper search_pattern = Regexp.new(Regexp.escape(term), "i") generic_results = project_autocomplete + default_autocomplete + help_autocomplete + generic_results.concat(default_autocomplete_admin) if current_user.admin? generic_results.select! { |result| result[:label] =~ search_pattern } [ @@ -41,8 +42,14 @@ 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: "Dashboard", url: root_path } + ] + end + + # Autocomplete results for settings pages, for admins + def default_autocomplete_admin + [ + { category: "Settings", label: "Admin Section", url: admin_root_path } ] end @@ -67,16 +74,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 [] @@ -85,7 +92,7 @@ module SearchHelper # Autocomplete results for the current user's groups def groups_autocomplete(term, limit = 5) - current_user.authorized_groups.search(term).limit(limit).map do |group| + current_user.authorized_groups.order_id_desc.search(term).limit(limit).map do |group| { category: "Groups", id: group.id, @@ -97,14 +104,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) + current_user.authorized_projects.order_id_desc.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) + label: "#{search_result_sanitize(p.full_name)}", + url: project_path(p) } end end @@ -126,6 +133,29 @@ 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' => UserSerializer.new.represent(@users) + }, + autocomplete: 'off' + } + + if @project.present? + opts[:data]['project-id'] = @project.id + opts[:data]['base-endpoint'] = project_path(@project) + else + # Group context + opts[:data]['group-id'] = @group.id + 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) @@ -140,4 +170,8 @@ module SearchHelper # Truncato's filtered_tags and filtered_attributes are not quite the same sanitize(html, tags: %w(a p ol ul li pre code)) end + + def limited_count(count, limit = 1000) + count > limit ? "#{limit}+" : count + end end diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 1a4f1431bdc..6cefcde558a 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -73,7 +73,6 @@ module SelectsHelper email_user: opts[:email_user] || false, first_user: opts[:first_user] && current_user ? current_user.username : false, 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 } diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index 3707bb5ba36..f435c80c656 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -1,31 +1,19 @@ module ServicesHelper - def service_event_description(event) - case event - when "push", "push_events" - "Event will be triggered by a push to the repository" - when "tag_push", "tag_push_events" - "Event will be triggered when a new tag is pushed to the repository" - when "note", "note_events" - "Event will be triggered when someone adds a comment" - when "issue", "issue_events" - "Event will be triggered when an issue is created/updated/closed" - when "confidential_issue", "confidential_issue_events" - "Event will be triggered when a confidential issue is created/updated/closed" - when "merge_request", "merge_request_events" - "Event will be triggered when a merge request is created/updated/merged" - when "pipeline", "pipeline_events" - "Event will be triggered when a pipeline status changes" - when "wiki_page", "wiki_page_events" - "Event will be triggered when a wiki page is created/updated" - when "commit", "commit_events" - "Event will be triggered when a commit is created/updated" - end - end - def service_event_field_name(event) event = event.pluralize if %w[merge_request issue confidential_issue].include?(event) "#{event}_events" end + def service_save_button(service) + button_tag(class: 'btn btn-save', type: 'submit', disabled: service.deprecated?) do + icon('spinner spin', class: 'hidden js-btn-spinner') + + content_tag(:span, 'Save changes', class: 'js-btn-label') + end + end + + def disable_fields_service?(service) + !current_controller?("admin/services") && service.deprecated? + end + extend self end diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb index 55f4da0ef85..50aeb7f4b82 100644 --- a/app/helpers/sidekiq_helper.rb +++ b/app/helpers/sidekiq_helper.rb @@ -1,12 +1,12 @@ module SidekiqHelper - SIDEKIQ_PS_REGEXP = /\A + SIDEKIQ_PS_REGEXP = %r{\A (?<pid>\d+)\s+ (?<cpu>[\d\.,]+)\s+ (?<mem>[\d\.,]+)\s+ - (?<state>[DIEKNRSTVWXZNLpsl\+<>\/\d]+)\s+ + (?<state>[DIEKNRSTVWXZNLpsl\+<>/\d]+)\s+ (?<start>.+?)\s+ (?<command>(?:ruby\d+:\s+)?sidekiq.*\].*) - \z/x + \z}x def parse_sidekiq_ps(line) match = line.strip.match(SIDEKIQ_PS_REGEXP) diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 2fd64b3441e..00e7e4230b9 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -1,8 +1,7 @@ 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 @@ -10,7 +9,7 @@ module SnippetsHelper def download_snippet_path(snippet) if snippet.project_id - raw_namespace_project_snippet_path(@project.namespace, @project, snippet, inline: false) + raw_project_snippet_path(@project, snippet, inline: false) else raw_snippet_path(snippet, inline: false) end @@ -21,7 +20,7 @@ module SnippetsHelper # @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 @@ -90,6 +89,7 @@ module SnippetsHelper snippet_chunk = [lined_content[line_number]] snippet_start_line = line_number end + last_line = line_number end # Add final chunk to chunk array diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index b408ec0c6a4..36a311dfa8a 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -1,34 +1,38 @@ module SortingHelper def sort_options_hash { - sort_value_name => sort_title_name, - sort_value_name_desc => sort_title_name_desc, - sort_value_recently_updated => sort_title_recently_updated, - sort_value_oldest_updated => sort_title_oldest_updated, + sort_value_created_date => sort_title_created_date, + sort_value_downvotes => sort_title_downvotes, + sort_value_due_date => sort_title_due_date, + sort_value_due_date_later => sort_title_due_date_later, + sort_value_due_date_soon => sort_title_due_date_soon, + sort_value_label_priority => sort_title_label_priority, + sort_value_largest_group => sort_title_largest_group, + sort_value_largest_repo => sort_title_largest_repo, + sort_value_milestone => sort_title_milestone, + sort_value_milestone_later => sort_title_milestone_later, + sort_value_milestone_soon => sort_title_milestone_soon, + sort_value_name => sort_title_name, + sort_value_name_desc => sort_title_name_desc, + sort_value_oldest_created => sort_title_oldest_created, + sort_value_oldest_signin => sort_title_oldest_signin, + sort_value_oldest_updated => sort_title_oldest_updated, sort_value_recently_created => sort_title_recently_created, - sort_value_oldest_created => sort_title_oldest_created, - sort_value_milestone_soon => sort_title_milestone_soon, - sort_value_milestone_later => sort_title_milestone_later, - sort_value_due_date_soon => sort_title_due_date_soon, - sort_value_due_date_later => sort_title_due_date_later, - sort_value_largest_repo => sort_title_largest_repo, - sort_value_largest_group => sort_title_largest_group, - sort_value_recently_signin => sort_title_recently_signin, - sort_value_oldest_signin => sort_title_oldest_signin, - sort_value_downvotes => sort_title_downvotes, - sort_value_upvotes => sort_title_upvotes, - sort_value_priority => sort_title_priority, - sort_value_label_priority => sort_title_label_priority + sort_value_recently_signin => sort_title_recently_signin, + sort_value_recently_updated => sort_title_recently_updated, + sort_value_popularity => sort_title_popularity, + sort_value_priority => sort_title_priority, + sort_value_upvotes => sort_title_upvotes } end def projects_sort_options_hash options = { - sort_value_name => sort_title_name, - sort_value_latest_activity => sort_title_latest_activity, - sort_value_oldest_activity => sort_title_oldest_activity, - sort_value_recently_created => sort_title_recently_created, - sort_value_oldest_created => sort_title_oldest_created + sort_value_latest_activity => sort_title_latest_activity, + sort_value_name => sort_title_name, + sort_value_oldest_activity => sort_title_oldest_activity, + sort_value_oldest_created => sort_title_oldest_created, + sort_value_recently_created => sort_title_recently_created } if current_controller?('admin/projects') @@ -38,162 +42,193 @@ module SortingHelper options end + def groups_sort_options_hash + { + sort_value_name => sort_title_name, + sort_value_name_desc => sort_title_name_desc, + sort_value_recently_created => sort_title_recently_created, + sort_value_oldest_created => sort_title_oldest_created, + sort_value_recently_updated => sort_title_recently_updated, + sort_value_oldest_updated => sort_title_oldest_updated + } + end + + def admin_groups_sort_options_hash + groups_sort_options_hash.merge( + sort_value_largest_group => sort_title_largest_group + ) + end + def member_sort_options_hash { - sort_value_access_level_asc => sort_title_access_level_asc, + sort_value_access_level_asc => sort_title_access_level_asc, sort_value_access_level_desc => sort_title_access_level_desc, - sort_value_last_joined => sort_title_last_joined, - sort_value_oldest_joined => sort_title_oldest_joined, - sort_value_name => sort_title_name_asc, - sort_value_name_desc => sort_title_name_desc, - sort_value_recently_signin => sort_title_recently_signin, - sort_value_oldest_signin => sort_title_oldest_signin + sort_value_last_joined => sort_title_last_joined, + sort_value_name => sort_title_name_asc, + sort_value_name_desc => sort_title_name_desc, + sort_value_oldest_joined => sort_title_oldest_joined, + sort_value_oldest_signin => sort_title_oldest_signin, + sort_value_recently_signin => sort_title_recently_signin } end def milestone_sort_options_hash { - sort_value_name => sort_title_name_asc, - sort_value_name_desc => sort_title_name_desc, - 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_name => sort_title_name_asc, + sort_value_name_desc => sort_title_name_desc, + sort_value_due_date_later => sort_title_due_date_later, + sort_value_due_date_soon => sort_title_due_date_soon, + sort_value_start_date_later => sort_title_start_date_later, + sort_value_start_date_soon => sort_title_start_date_soon } end def branches_sort_options_hash { - sort_value_name => sort_title_name, - sort_value_recently_updated => sort_title_recently_updated, - sort_value_oldest_updated => sort_title_oldest_updated + sort_value_name => sort_title_name, + sort_value_oldest_updated => sort_title_oldest_updated, + sort_value_recently_updated => sort_title_recently_updated } end def tags_sort_options_hash { - sort_value_name => sort_title_name, - sort_value_recently_updated => sort_title_recently_updated, - sort_value_oldest_updated => sort_title_oldest_updated + sort_value_name => sort_title_name, + sort_value_oldest_updated => sort_title_oldest_updated, + sort_value_recently_updated => sort_title_recently_updated } end - def sort_title_priority - 'Priority' + def sortable_item(item, path, sorted_by) + link_to item, path, class: sorted_by == item ? 'is-active' : '' end - def sort_title_label_priority - 'Label priority' + # Titles. + def sort_title_access_level_asc + s_('SortOptions|Access level, ascending') end - def sort_title_oldest_updated - 'Oldest updated' + def sort_title_access_level_desc + s_('SortOptions|Access level, descending') end - def sort_title_recently_updated - 'Last updated' + def sort_title_created_date + s_('SortOptions|Created date') end - def sort_title_oldest_activity - 'Oldest updated' + def sort_title_downvotes + s_('SortOptions|Least popular') end - def sort_title_latest_activity - 'Last updated' + def sort_title_due_date + s_('SortOptions|Due date') end - def sort_title_oldest_created - 'Oldest created' + def sort_title_due_date_later + s_('SortOptions|Due later') end - def sort_title_recently_created - 'Last created' + def sort_title_due_date_soon + s_('SortOptions|Due soon') end - def sort_title_milestone_soon - 'Milestone due soon' + def sort_title_label_priority + s_('SortOptions|Label priority') end - def sort_title_milestone_later - 'Milestone due later' + def sort_title_largest_group + s_('SortOptions|Largest group') end - def sort_title_due_date_soon - 'Due soon' + def sort_title_largest_repo + s_('SortOptions|Largest repository') end - def sort_title_due_date_later - 'Due later' + def sort_title_last_joined + s_('SortOptions|Last joined') end - def sort_title_start_date_soon - 'Start soon' + def sort_title_latest_activity + s_('SortOptions|Last updated') end - def sort_title_start_date_later - 'Start later' + def sort_title_milestone + s_('SortOptions|Milestone') + end + + def sort_title_milestone_later + s_('SortOptions|Milestone due later') + end + + def sort_title_milestone_soon + s_('SortOptions|Milestone due soon') end def sort_title_name - 'Name' + s_('SortOptions|Name') end - def sort_title_largest_repo - 'Largest repository' + def sort_title_name_asc + s_('SortOptions|Name, ascending') end - def sort_title_largest_group - 'Largest group' + def sort_title_name_desc + s_('SortOptions|Name, descending') end - def sort_title_recently_signin - 'Recent sign in' + def sort_title_oldest_activity + s_('SortOptions|Oldest updated') end - def sort_title_oldest_signin - 'Oldest sign in' + def sort_title_oldest_created + s_('SortOptions|Oldest created') end - def sort_title_downvotes - 'Least popular' + def sort_title_oldest_joined + s_('SortOptions|Oldest joined') end - def sort_title_upvotes - 'Most popular' + def sort_title_oldest_signin + s_('SortOptions|Oldest sign in') end - def sort_title_last_joined - 'Last joined' + def sort_title_oldest_updated + s_('SortOptions|Oldest updated') end - def sort_title_oldest_joined - 'Oldest joined' + def sort_title_popularity + s_('SortOptions|Popularity') end - def sort_title_access_level_asc - 'Access level, ascending' + def sort_title_priority + s_('SortOptions|Priority') end - def sort_title_access_level_desc - 'Access level, descending' + def sort_title_recently_created + s_('SortOptions|Last created') end - def sort_title_name_asc - 'Name, ascending' + def sort_title_recently_signin + s_('SortOptions|Recent sign in') end - def sort_title_name_desc - 'Name, descending' + def sort_title_recently_updated + s_('SortOptions|Last updated') end - def sort_value_last_joined - 'last_joined' + def sort_title_start_date_later + s_('SortOptions|Start later') end - def sort_value_oldest_joined - 'oldest_joined' + def sort_title_start_date_soon + s_('SortOptions|Start soon') end + def sort_title_upvotes + s_('SortOptions|Most popular') + end + + # Values. def sort_value_access_level_asc 'access_level_asc' end @@ -202,88 +237,112 @@ module SortingHelper 'access_level_desc' end - def sort_value_name_desc - 'name_desc' + def sort_value_created_date + 'created_date' end - def sort_value_priority - 'priority' + def sort_value_downvotes + 'downvotes_desc' + end + + def sort_value_due_date + 'due_date' + end + + def sort_value_due_date_later + 'due_date_desc' + end + + def sort_value_due_date_soon + 'due_date_asc' end def sort_value_label_priority 'label_priority' end - def sort_value_oldest_updated - 'updated_asc' + def sort_value_largest_group + 'storage_size_desc' end - def sort_value_recently_updated - 'updated_desc' + def sort_value_largest_repo + 'storage_size_desc' end - def sort_value_oldest_activity - 'latest_activity_asc' + def sort_value_last_joined + 'last_joined' end def sort_value_latest_activity 'latest_activity_desc' end - def sort_value_oldest_created - 'created_asc' + def sort_value_milestone + 'milestone' end - def sort_value_recently_created - 'created_desc' + def sort_value_milestone_later + 'milestone_due_desc' end def sort_value_milestone_soon 'milestone_due_asc' end - def sort_value_milestone_later - 'milestone_due_desc' + def sort_value_name + 'name_asc' end - def sort_value_due_date_soon - 'due_date_asc' + def sort_value_name_desc + 'name_desc' end - def sort_value_due_date_later - 'due_date_desc' + def sort_value_oldest_activity + 'latest_activity_asc' end - def sort_value_start_date_soon - 'start_date_asc' + def sort_value_oldest_created + 'created_asc' end - def sort_value_start_date_later - 'start_date_desc' + def sort_value_oldest_signin + 'oldest_sign_in' end - def sort_value_name - 'name_asc' + def sort_value_oldest_joined + 'oldest_joined' end - def sort_value_largest_repo - 'storage_size_desc' + def sort_value_oldest_updated + 'updated_asc' end - def sort_value_largest_group - 'storage_size_desc' + def sort_value_popularity + 'popularity' + end + + def sort_value_priority + 'priority' + end + + def sort_value_recently_created + 'created_desc' end def sort_value_recently_signin 'recent_sign_in' end - def sort_value_oldest_signin - 'oldest_sign_in' + def sort_value_recently_updated + 'updated_desc' end - def sort_value_downvotes - 'downvotes_desc' + def sort_value_start_date_later + 'start_date_desc' + end + + def sort_value_start_date_soon + 'start_date_asc' end def sort_value_upvotes diff --git a/app/helpers/storage_health_helper.rb b/app/helpers/storage_health_helper.rb new file mode 100644 index 00000000000..b76c1228220 --- /dev/null +++ b/app/helpers/storage_health_helper.rb @@ -0,0 +1,32 @@ +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 + + translation_params = { number_of_failures: current_failures, + maximum_failures: maximum_failures } + + if circuit_breaker.circuit_broken? + s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\ + "retry automatically. Reset storage information when the problem is "\ + "resolved.") % 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 8e0a1e2ecdf..9151543dfdc 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -1,5 +1,5 @@ module SubmoduleHelper - include Gitlab::ShellAdapter + extend self VALID_SUBMODULE_PROTOCOLS = %w[http https git ssh].freeze @@ -11,7 +11,7 @@ module SubmoduleHelper url = File.join(Gitlab.config.gitlab.url, @project.full_path) end - if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/ + if url =~ %r{([^/:]+)/([^/]+(?:\.git)?)\Z} namespace, project = $1, $2 gitlab_hosts = [Gitlab.config.gitlab.url, Gitlab.config.gitlab_shell.ssh_path_prefix] @@ -23,7 +23,7 @@ module SubmoduleHelper end end - namespace.sub!(/\A\//, '') + namespace.sub!(%r{\A/}, '') project.rstrip! project.sub!(/\.git\z/, '') @@ -47,24 +47,25 @@ module SubmoduleHelper protected def github_dot_com_url?(url) - url =~ /github\.com[\/:][^\/]+\/[^\/]+\Z/ + url =~ %r{github\.com[/:][^/]+/[^/]+\Z} end def gitlab_dot_com_url?(url) - url =~ /gitlab\.com[\/:][^\/]+\/[^\/]+\Z/ + url =~ %r{gitlab\.com[/:][^/]+/[^/]+\Z} end def self_url?(url, namespace, project) url_no_dotgit = url.chomp('.git') return true if url_no_dotgit == [Gitlab.config.gitlab.url, '/', namespace, '/', project].join('') + url_with_dotgit = url_no_dotgit + '.git' - url_with_dotgit == gitlab_shell.url_to_repo([namespace, '/', project].join('')) + url_with_dotgit == Gitlab::Shell.new.url_to_repo([namespace, '/', project].join('')) end def relative_self_url?(url) # (./)?(../repo.git) || (./)?(../../project/repo.git) ) - url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*(\.git)?\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*(\.git)?\z/ + url =~ %r{\A((\./)?(\.\./))(?!(\.\.)|(.*/)).*(\.git)?\z} || url =~ %r{\A((\./)?(\.\./){2})(?!(\.\.))([^/]*)/(?!(\.\.)|(.*/)).*(\.git)?\z} end def standard_links(host, namespace, project, commit) @@ -73,6 +74,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 @@ -86,10 +88,14 @@ module SubmoduleHelper namespace = @project.namespace.full_path end - [ - namespace_project_path(namespace, base), - namespace_project_tree_path(namespace, base, commit) - ] + begin + [ + namespace_project_path(namespace, base), + namespace_project_tree_path(namespace, base, commit) + ] + rescue ActionController::UrlGenerationError + [nil, nil] + end end def sanitize_submodule_url(url) diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb index 209bd56b78a..00fe67d6ffb 100644 --- a/app/helpers/system_note_helper.rb +++ b/app/helpers/system_note_helper.rb @@ -1,28 +1,37 @@ module SystemNoteHelper ICON_NAMES_BY_ACTION = { - 'commit' => 'icon_commit', - 'description' => 'icon_edit', - 'merge' => 'icon_merge', - 'merged' => 'icon_merged', - 'opened' => 'icon_status_open', - 'closed' => 'icon_status_closed', - 'time_tracking' => 'icon_stopwatch', - 'assignee' => 'icon_user', - 'title' => 'icon_edit', - 'task' => 'icon_check_square_o', - 'label' => 'icon_tags', - 'cross_reference' => 'icon_random', - 'branch' => 'icon_code_fork', - 'confidential' => 'icon_eye_slash', - 'visible' => 'icon_eye', - 'milestone' => 'icon_clock_o', - 'discussion' => 'icon_comment_o', - 'moved' => 'icon_arrow_circle_o_right', - 'outdated' => 'icon_edit' + 'commit' => 'commit', + 'description' => 'pencil', + 'merge' => 'git-merge', + 'merged' => 'git-merge', + 'opened' => 'issue-open', + 'closed' => 'issue-close', + 'time_tracking' => 'timer', + 'assignee' => 'user', + 'title' => 'pencil', + 'task' => 'task-done', + 'label' => 'label', + 'cross_reference' => 'comment-dots', + 'branch' => 'fork', + 'confidential' => 'eye-slash', + 'visible' => 'eye', + 'milestone' => 'clock', + 'discussion' => 'comment', + 'moved' => 'arrow-right', + 'outdated' => 'pencil', + 'duplicate' => 'issue-duplicate', + 'locked' => 'lock', + 'unlocked' => 'lock-open' }.freeze + def system_note_icon_name(note) + ICON_NAMES_BY_ACTION[note.system_note_metadata&.action] + end + def icon_for_system_note(note) - icon_name = ICON_NAMES_BY_ACTION[note.system_note_metadata&.action] - custom_icon(icon_name) if icon_name + icon_name = system_note_icon_name(note) + sprite_icon(icon_name) if icon_name end + + extend self end 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 3d1b3a4711a..f7620e0b6b8 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -30,6 +30,7 @@ module TodosHelper else todo.target_reference end + link_to text, todo_target_path(todo), class: 'has-tooltip', title: todo.target.title end @@ -39,7 +40,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] @@ -53,8 +54,16 @@ module TodosHelper def todo_target_state_pill(todo) return unless show_todo_state?(todo) + type = + case todo.target + when MergeRequest + 'mr' + when Issue + 'issue' + end + content_tag(:span, nil, class: 'target-status') do - content_tag(:span, nil, class: "status-box status-box-#{todo.target.state.dasherize}") do + content_tag(:span, nil, class: "status-box status-box-#{type}-#{todo.target.state.dasherize}") do todo.target.state.capitalize end end @@ -105,7 +114,7 @@ module TodosHelper projects = current_user.authorized_projects.sorted_by_activity.non_archived.with_route projects = projects.map do |project| - { id: project.id, text: project.name_with_namespace } + { id: project.id, text: project.full_name } end projects.unshift({ id: '', text: 'Any Project' }).to_json diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index e0d3e9b88f3..b64be89c181 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -1,14 +1,23 @@ module TreeHelper + FILE_LIMIT = 1_000 + # Sorts a repository's tree so that folders are before files and renders # their corresponding partials # - # contents - A Grit::Tree object for the current tree + # tree - A `Tree` object for the current tree def render_tree(tree) # Sort submodules and folders together by name ahead of files folders, files, submodules = tree.trees, tree.blobs, tree.submodules - tree = "" + tree = '' items = (folders + submodules).sort_by(&:name) + files - tree << render(partial: "projects/tree/tree_row", collection: items) if items.present? + + if items.size > FILE_LIMIT + tree << render(partial: 'projects/tree/truncated_notice_tree_row', + locals: { limit: FILE_LIMIT, total: items.size }) + items = items.take(FILE_LIMIT) + end + + tree << render(partial: 'projects/tree/tree_row', collection: items) if items.present? tree.html_safe end @@ -40,13 +49,13 @@ module TreeHelper return false unless on_top_of_branch?(project, ref) - can_collaborate_with_project?(project) + can_collaborate_with_project?(project, ref: ref) end def tree_edit_branch(project = @project, ref = @ref) return unless can_edit_tree?(project, ref) - if can_push_branch?(project, ref) + if user_access(project).can_push_to_branch?(ref) ref else project = tree_edit_project(project) @@ -72,8 +81,21 @@ module TreeHelper " A fork of this project has been created that you can make changes in, so you can submit a merge request." end + def edit_in_new_fork_notice_action(action) + edit_in_new_fork_notice + " Try to #{action} this file again." + end + def commit_in_fork_help - "A new branch will be created in your fork and a new merge request will be started." + _("A new branch will be created in your fork and a new merge request will be started.") + end + + def commit_in_single_accessible_branch + branch_name = html_escape(selected_branch) + + message = _("Your changes can be committed to %{branch_name} because a merge "\ + "request is open.") % { branch_name: "<strong>#{branch_name}</strong>" } + + message.html_safe end def path_breadcrumbs(max_links = 6) @@ -88,6 +110,7 @@ module TreeHelper part_path = part if part_path.empty? next if parts.count > max_links && !parts.last(2).include?(part) + yield(part, part_path) end end @@ -99,12 +122,18 @@ module TreeHelper end # returns the relative path of the first subdir that doesn't have only one directory descendant - def flatten_tree(tree) + def flatten_tree(root_path, tree) + return tree.flat_path.sub(%r{\A#{root_path}/}, '') if tree.flat_path.present? + subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path) if subtree.count == 1 && subtree.first.dir? - return tree_join(tree.name, flatten_tree(subtree.first)) + return tree_join(tree.name, flatten_tree(root_path, subtree.first)) else return tree.name end end + + def selected_branch + @branch_name || tree_edit_branch + 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 deleted file mode 100644 index 81bfe5d4eeb..00000000000 --- a/app/helpers/u2f_helper.rb +++ /dev/null @@ -1,5 +0,0 @@ -module U2fHelper - def inject_u2f_api? - ((browser.chrome? && browser.version.to_i >= 41) || (browser.opera? && browser.version.to_i >= 40)) && !browser.device.mobile? - end -end diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb new file mode 100644 index 00000000000..36abfaf19a5 --- /dev/null +++ b/app/helpers/user_callouts_helper.rb @@ -0,0 +1,14 @@ +module UserCalloutsHelper + GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze + + def show_gke_cluster_integration_callout?(project) + can?(current_user, :create_cluster, project) && + !user_dismissed?(GKE_CLUSTER_INTEGRATION) + end + + private + + def user_dismissed?(feature_name) + current_user&.callouts&.find_by(feature_name: UserCallout.feature_names[feature_name]) + end +end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index b5f54d3e154..01af68088df 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -14,4 +14,18 @@ module UsersHelper content_tag(:strong) { user.unconfirmed_email } + h('.') + content_tag(:p) { confirmation_link } end + + def profile_tabs + @profile_tabs ||= get_profile_tabs + end + + def profile_tab?(tab) + profile_tabs.include?(tab) + end + + private + + def get_profile_tabs + [:activity, :groups, :contributed, :projects, :snippets] + end end diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb index 456598b4c28..c20753ece72 100644 --- a/app/helpers/version_check_helper.rb +++ b/app/helpers/version_check_helper.rb @@ -1,6 +1,6 @@ module VersionCheckHelper def version_status_badge - if Rails.env.production? && current_application_settings.version_check_enabled + if Rails.env.production? && Gitlab::CurrentSettings.version_check_enabled image_url = VersionCheck.new.url image_tag image_url, class: 'js-version-status-badge' end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 35755bc149b..e395cda03d3 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -63,6 +63,68 @@ module VisibilityLevelHelper end end + def restricted_visibility_level_description(level) + level_name = Gitlab::VisibilityLevel.level_name(level) + "#{level_name.capitalize} visibility has been restricted by the administrator." + end + + def disallowed_visibility_level_description(level, form_model) + case form_model + when Project + disallowed_project_visibility_level_description(level, form_model) + when Group + disallowed_group_visibility_level_description(level, form_model) + end + end + + # Note: these messages closely mirror the form validation strings found in the project + # model and any changes or additons to these may also need to be made there. + def disallowed_project_visibility_level_description(level, project) + level_name = Gitlab::VisibilityLevel.level_name(level).downcase + reasons = [] + instructions = '' + + unless project.visibility_level_allowed_as_fork?(level) + reasons << "the fork source project has lower visibility" + end + + unless project.visibility_level_allowed_by_group?(level) + errors = visibility_level_errors_for_group(project.group, level_name) + + reasons << errors[:reason] + instructions << errors[:instruction] + end + + reasons = reasons.any? ? ' because ' + reasons.to_sentence : '' + "This project cannot be #{level_name}#{reasons}.#{instructions}".html_safe + end + + # Note: these messages closely mirror the form validation strings found in the group + # model and any changes or additons to these may also need to be made there. + def disallowed_group_visibility_level_description(level, group) + level_name = Gitlab::VisibilityLevel.level_name(level).downcase + reasons = [] + instructions = '' + + unless group.visibility_level_allowed_by_projects?(level) + reasons << "it contains projects with higher visibility" + end + + unless group.visibility_level_allowed_by_sub_groups?(level) + reasons << "it contains sub-groups with higher visibility" + end + + unless group.visibility_level_allowed_by_parent?(level) + errors = visibility_level_errors_for_group(group.parent, level_name) + + reasons << errors[:reason] + instructions << errors[:instruction] + end + + reasons = reasons.any? ? ' because ' + reasons.to_sentence : '' + "This group cannot be #{level_name}#{reasons}.#{instructions}".html_safe + end + def visibility_icon_description(form_model) case form_model when Project @@ -88,14 +150,27 @@ module VisibilityLevelHelper def restricted_visibility_levels(show_all = false) return [] if current_user.admin? && !show_all - current_application_settings.restricted_visibility_levels || [] + + Gitlab::CurrentSettings.restricted_visibility_levels || [] end delegate :default_project_visibility, :default_group_visibility, - to: :current_application_settings + to: :'Gitlab::CurrentSettings.current_application_settings' + + def disallowed_visibility_level?(form_model, level) + return false unless form_model.respond_to?(:visibility_level_allowed?) + + !form_model.visibility_level_allowed?(level) + end + + private + + def visibility_level_errors_for_group(group, level_name) + group_name = link_to group.name, group_path(group) + change_visiblity = link_to 'change the visibility', edit_group_path(group) - def skip_level?(form_model, level) - form_model.is_a?(Project) && !form_model.visibility_level_allowed?(level) + { reason: "the visibility of #{group_name} is #{group.visibility}", + instruction: " To make this group #{level_name}, you must first #{change_visiblity} of the parent group." } end end diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb index 6bacda9fe75..8bcced70d63 100644 --- a/app/helpers/webpack_helper.rb +++ b/app/helpers/webpack_helper.rb @@ -1,30 +1,68 @@ require 'webpack/rails/manifest' module WebpackHelper - def webpack_bundle_tag(bundle) - javascript_include_tag(*gitlab_webpack_asset_paths(bundle)) + def webpack_bundle_tag(bundle, force_same_domain: false) + javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: force_same_domain)) + end + + def webpack_controller_bundle_tags + bundles = [] + + action = case controller.action_name + when 'create' then 'new' + when 'update' then 'edit' + else controller.action_name + end + + route = [*controller.controller_path.split('/'), action].compact + + until route.empty? + begin + asset_paths = gitlab_webpack_asset_paths("pages.#{route.join('.')}", extension: 'js') + bundles.unshift(*asset_paths) + rescue Webpack::Rails::Manifest::EntryPointMissingError + # no bundle exists for this path + end + + route.pop + end + + javascript_include_tag(*bundles) end # override webpack-rails gem helper until changes can make it upstream - def gitlab_webpack_asset_paths(source, extension: nil) + def gitlab_webpack_asset_paths(source, extension: nil, force_same_domain: false) return "" unless source.present? 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 + unless force_same_domain + force_host = webpack_public_host + if force_host + paths.map! { |p| "#{force_host}#{p}" } + end + 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 99212a3438f..41f9eedd4bd 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -10,4 +10,33 @@ module WikiHelper .map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize } .join(' / ') end + + def wiki_breadcrumb_dropdown_links(page_slug) + page_slug_split = page_slug.split('/') + page_slug_split.pop(1) + current_slug = "" + page_slug_split + .map do |dir_or_page| + current_slug = "#{current_slug}#{dir_or_page}/" + add_to_breadcrumb_dropdown link_to(WikiPage.unhyphenize(dir_or_page).capitalize, project_wiki_path(@project, current_slug)), location: :after + end + end + + def wiki_page_errors(error) + return unless error + + content_tag(:div, class: 'alert alert-danger') do + case error + when WikiPage::PageChangedError + page_link = link_to s_("WikiPageConflictMessage|the page"), project_wiki_path(@project, @page), target: "_blank" + concat( + (s_("WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs.") % { page_link: page_link }).html_safe + ) + when WikiPage::PageRenameError + s_("WikiEdit|There is already a page with the same title in that path.") + else + error.message + end + end + end end |