diff options
Diffstat (limited to 'app/helpers')
80 files changed, 1176 insertions, 511 deletions
diff --git a/app/helpers/abuse_reports_helper.rb b/app/helpers/abuse_reports_helper.rb new file mode 100644 index 00000000000..c18c78b26c7 --- /dev/null +++ b/app/helpers/abuse_reports_helper.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module AbuseReportsHelper + def valid_image_mimetypes + Gitlab::FileTypeDetection::SAFE_IMAGE_EXT + .map { |extension| "image/#{extension}" } + .to_sentence(last_word_connector: ' or ') + end +end diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb index a4f19480539..0f6c81f5238 100644 --- a/app/helpers/accounts_helper.rb +++ b/app/helpers/accounts_helper.rb @@ -2,6 +2,6 @@ module AccountsHelper def incoming_email_token_enabled? - current_user.incoming_email_token && Gitlab::IncomingEmail.supports_issue_creation? + current_user.incoming_email_token && Gitlab::Email::IncomingEmail.supports_issue_creation? end end diff --git a/app/helpers/admin/abuse_reports_helper.rb b/app/helpers/admin/abuse_reports_helper.rb new file mode 100644 index 00000000000..275bed406f1 --- /dev/null +++ b/app/helpers/admin/abuse_reports_helper.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Admin + module AbuseReportsHelper + def abuse_reports_list_data(reports) + { + abuse_reports_data: { + categories: AbuseReport.categories.keys, + reports: Admin::AbuseReportSerializer.new.represent(reports), + pagination: { + current_page: reports.current_page, + per_page: reports.limit_value, + total_items: reports.total_count + } + }.to_json + } + end + + def abuse_report_data(report) + { + abuse_report_data: Admin::AbuseReportDetailsSerializer.new.represent(report).to_json + } + end + end +end diff --git a/app/helpers/admin/application_settings/settings_helper.rb b/app/helpers/admin/application_settings/settings_helper.rb index bd83ed19705..1741d6a953a 100644 --- a/app/helpers/admin/application_settings/settings_helper.rb +++ b/app/helpers/admin/application_settings/settings_helper.rb @@ -11,6 +11,10 @@ module Admin inactive_projects_send_warning_email_after_months: settings.inactive_projects_send_warning_email_after_months } end + + def project_missing_pipeline_yaml?(project) + project.repository&.gitlab_ci_yml.blank? + end end end end diff --git a/app/helpers/admin/background_migrations_helper.rb b/app/helpers/admin/background_migrations_helper.rb index 79bb13810bb..cea9cd704c3 100644 --- a/app/helpers/admin/background_migrations_helper.rb +++ b/app/helpers/admin/background_migrations_helper.rb @@ -5,6 +5,7 @@ module Admin def batched_migration_status_badge_variant(migration) variants = { active: :info, + finalizing: :info, paused: :warning, failed: :danger, finished: :success diff --git a/app/helpers/analytics/cycle_analytics_helper.rb b/app/helpers/analytics/cycle_analytics_helper.rb deleted file mode 100644 index 35a5d4f469d..00000000000 --- a/app/helpers/analytics/cycle_analytics_helper.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module Analytics - module CycleAnalyticsHelper - def cycle_analytics_default_stage_config - Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map do |stage_params| - Analytics::CycleAnalytics::StagePresenter.new(stage_params) - end - end - - def cycle_analytics_initial_data(project, group = nil) - base_data = { project_id: project.id, group_path: project.group&.path, request_path: project_cycle_analytics_path(project), full_path: project.full_path } - svgs = { empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_access_svg_path: image_path("illustrations/analytics/no-access.svg") } - api_paths = group.present? ? cycle_analytics_group_api_paths(group) : cycle_analytics_project_api_paths(project) - - base_data.merge(svgs, api_paths) - end - - private - - def cycle_analytics_group_api_paths(group) - { milestones_path: group_milestones_path(group, format: :json), labels_path: group_labels_path(group, format: :json), group_path: group_path(group), group_id: group&.id } - end - - def cycle_analytics_project_api_paths(project) - { milestones_path: project_milestones_path(project, format: :json), labels_path: project_labels_path(project, format: :json), group_path: project.parent&.path, group_id: project.parent&.id } - end - end -end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 93b7c8c0b94..71f8478544b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -181,14 +181,14 @@ module ApplicationHelper css_classes << html_class unless html_class.blank? content_tag :time, l(time, format: "%b %d, %Y"), - class: css_classes.join(' '), - title: l(time.to_time.in_time_zone, format: :timeago_tooltip), - datetime: time.to_time.getutc.iso8601, - data: { - toggle: 'tooltip', - placement: placement, - container: 'body' - } + class: css_classes.join(' '), + title: l(time.to_time.in_time_zone, format: :timeago_tooltip), + datetime: time.to_time.getutc.iso8601, + data: { + toggle: 'tooltip', + placement: placement, + container: 'body' + } end def edited_time_ago_with_tooltip(object, placement: 'top', html_class: 'time_ago', exclude_author: false) @@ -200,7 +200,7 @@ module ApplicationHelper if !exclude_author && object.last_edited_by output << content_tag(:span, ' by ') - output << link_to_member(object.project, object.last_edited_by, avatar: false, author_class: nil) + output << link_to_member(object.project, object.last_edited_by, avatar: false, extra_class: 'gl-hover-text-decoration-underline', author_class: nil) end output @@ -276,12 +276,16 @@ module ApplicationHelper if startup_css_enabled? stylesheet_link_tag(path, media: "print", crossorigin: ActionController::Base.asset_host ? 'anonymous' : nil) else - stylesheet_link_tag(path, crossorigin: ActionController::Base.asset_host ? 'anonymous' : nil) + stylesheet_link_tag(path, media: "all", crossorigin: ActionController::Base.asset_host ? 'anonymous' : nil) end end def startup_css_enabled? - !params.has_key?(:no_startup_css) + !Feature.enabled?(:remove_startup_css) && !params.has_key?(:no_startup_css) + end + + def sign_in_with_redirect? + current_page?(new_user_session_path) && session[:user_return_to].present? end def outdated_browser? @@ -316,6 +320,7 @@ module ApplicationHelper class_names << 'issue-boards-page gl-overflow-auto' if current_controller?(:boards) class_names << 'epic-boards-page gl-overflow-auto' if current_controller?(:epic_boards) class_names << 'with-performance-bar' if performance_bar_enabled? + class_names << 'with-top-bar' if show_super_sidebar? && !@hide_top_bar class_names << system_message_class class_names << 'logged-out-marketing-header' if !current_user && ::Gitlab.com? @@ -374,6 +379,12 @@ module ApplicationHelper cookies["sidebar_collapsed"] == "true" end + def collapsed_super_sidebar? + return false if @force_desktop_expanded_sidebar + + cookies["super_sidebar_collapsed"] == "true" + end + def locale_path asset_path("locale/#{Gitlab::I18n.locale}/app.js") end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 3abaae98c29..dab682d88e0 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -4,11 +4,11 @@ module ApplicationSettingsHelper extend self delegate :allow_signup?, - :gravatar_enabled?, - :password_authentication_enabled_for_web?, - :akismet_enabled?, - :spam_check_endpoint_enabled?, - to: :'Gitlab::CurrentSettings.current_application_settings' + :gravatar_enabled?, + :password_authentication_enabled_for_web?, + :akismet_enabled?, + :spam_check_endpoint_enabled?, + to: :'Gitlab::CurrentSettings.current_application_settings' def user_oauth_applications? Gitlab::CurrentSettings.user_oauth_applications @@ -248,7 +248,9 @@ module ApplicationSettingsHelper :default_project_visibility, :default_projects_limit, :default_snippet_visibility, + :default_syntax_highlighting_theme, :delete_inactive_projects, + :deny_all_requests_except_allowed, :disable_admin_oauth_scopes, :disable_feed_token, :disabled_oauth_sign_in_sources, @@ -347,6 +349,7 @@ module ApplicationSettingsHelper :repository_storages_weighted, :require_admin_approval_after_user_signup, :require_two_factor_authentication, + :remember_me_enabled, :restricted_visibility_levels, :rsa_key_restriction, :session_expire_delay, @@ -354,6 +357,12 @@ module ApplicationSettingsHelper :shared_runners_text, :sign_in_text, :signup_enabled, + :silent_mode_enabled, + :slack_app_enabled, + :slack_app_id, + :slack_app_secret, + :slack_app_signing_secret, + :slack_app_verification_token, :sourcegraph_enabled, :sourcegraph_url, :sourcegraph_public_only, @@ -401,6 +410,7 @@ module ApplicationSettingsHelper :protected_paths_raw, :time_tracking_limit_to_hours, :two_factor_grace_period, + :update_runner_versions_enabled, :unique_ips_limit_enabled, :unique_ips_limit_per_user, :unique_ips_limit_time_window, @@ -478,7 +488,10 @@ module ApplicationSettingsHelper :bulk_import_enabled, :allow_runner_registration_token, :user_defaults_to_private_profile, - :deactivation_email_additional_text + :deactivation_email_additional_text, + :projects_api_rate_limit_unauthenticated, + :gitlab_dedicated_instance, + :ci_max_includes ].tap do |settings| next if Gitlab.com? @@ -535,28 +548,6 @@ module ApplicationSettingsHelper Rack::Attack.throttles.key?('protected paths') end - def self_monitoring_project_data - { - 'create_self_monitoring_project_path' => - create_self_monitoring_project_admin_application_settings_path, - - 'status_create_self_monitoring_project_path' => - status_create_self_monitoring_project_admin_application_settings_path, - - 'delete_self_monitoring_project_path' => - delete_self_monitoring_project_admin_application_settings_path, - - 'status_delete_self_monitoring_project_path' => - status_delete_self_monitoring_project_admin_application_settings_path, - - 'self_monitoring_project_exists' => - Gitlab::CurrentSettings.self_monitoring_project.present?.to_s, - - 'self_monitoring_project_full_path' => - Gitlab::CurrentSettings.self_monitoring_project&.full_path - } - end - def valid_runner_registrars Gitlab::CurrentSettings.valid_runner_registrars end @@ -595,9 +586,7 @@ module ApplicationSettingsHelper supported_syntax_link_url: 'https://github.com/google/re2/wiki/Syntax', email_restrictions: @application_setting.email_restrictions.to_s, after_sign_up_text: @application_setting[:after_sign_up_text].to_s, - pending_user_count: pending_user_count, - project_sharing_help_link: help_page_path('user/group/access_and_permissions', anchor: 'prevent-a-project-from-being-shared-with-groups'), - group_sharing_help_link: help_page_path('user/group/access_and_permissions', anchor: 'prevent-group-sharing-outside-the-group-hierarchy') + pending_user_count: pending_user_count } end end diff --git a/app/helpers/artifacts_helper.rb b/app/helpers/artifacts_helper.rb index df0432105d5..f90d59409ed 100644 --- a/app/helpers/artifacts_helper.rb +++ b/app/helpers/artifacts_helper.rb @@ -4,6 +4,7 @@ module ArtifactsHelper def artifacts_app_data(project) { project_path: project.full_path, + project_id: project.id, can_destroy_artifacts: can?(current_user, :destroy_artifacts, project).to_s, artifacts_management_feedback_image_path: image_path('illustrations/chat-bubble-sm.svg') } diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index e2e89c9abca..0ee08ba1820 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -21,6 +21,8 @@ module AuthHelper LDAP_PROVIDER = /\Aldap/.freeze POPULAR_PROVIDERS = %w(google_oauth2 github).freeze + delegate :slack_app_id, to: :'Gitlab::CurrentSettings.current_application_settings' + def ldap_enabled? Gitlab::Auth::Ldap::Config.enabled? end @@ -45,9 +47,11 @@ module AuthHelper provider_has_builtin_icon?(name) || provider_has_custom_icon?(name) end - def qa_class_for_provider(provider) + def qa_selector_for_provider(provider) { - saml: 'qa-saml-login-button' + saml: 'saml_login_button', + openid_connect: 'oidc_login_button', + github: 'github_login_button' }[provider.to_sym] end diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index 57075a44d0f..17f995ec0ad 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -28,7 +28,7 @@ module AvatarsHelper end def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true) - return gravatar_icon(email, size, scale) if email.nil? + return default_avatar if email.blank? Gitlab::AvatarCache.by_email(email, size, scale, only_path) do avatar_icon_by_user_email_or_gravatar(email, size, scale, only_path: only_path) diff --git a/app/helpers/blame_helper.rb b/app/helpers/blame_helper.rb index 5117f7c6d9c..56d651a8b65 100644 --- a/app/helpers/blame_helper.rb +++ b/app/helpers/blame_helper.rb @@ -39,4 +39,12 @@ module BlameHelper row_height_exp = line_count == 1 ? COMMIT_BLOCK_HEIGHT_EXP : total_line_height_exp "contain-intrinsic-size: 1px calc(#{row_height_exp})" end + + def blame_pages_streaming_url(id, project) + namespace_project_blame_page_url(namespace_id: project.namespace, project_id: project, id: id, streaming: true) + end + + def entire_blame_path(id, project) + namespace_project_blame_streaming_path(namespace_id: project.namespace, project_id: project, id: id) + end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 281d5c923d0..02f69327dff 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -2,9 +2,7 @@ module BlobHelper def edit_blob_path(project = @project, ref = @ref, path = @path, options = {}) - project_edit_blob_path(project, - tree_join(ref, path), - options[:link_opts]) + project_edit_blob_path(project, tree_join(ref, path), options[:link_opts]) end def ide_edit_path(project = @project, ref = @ref, path = @path) @@ -52,9 +50,11 @@ module BlobHelper def fork_path_for_current_user(project, path, with_notice: true) return unless current_user - project_forks_path(project, - namespace_key: current_user.namespace&.id, - continue: edit_blob_fork_params(path, with_notice: with_notice)) + project_forks_path( + project, + namespace_key: current_user.namespace&.id, + continue: edit_blob_fork_params(path, with_notice: with_notice) + ) end def encode_ide_path(path) @@ -66,12 +66,14 @@ module BlobHelper common_classes = "btn gl-button btn-confirm js-edit-blob gl-ml-3 #{options[:extra_class]}" - edit_button_tag(blob, - common_classes, - _('Edit'), - edit_blob_path(project, ref, path, options), - project, - ref) + edit_button_tag( + blob, + common_classes, + _('Edit'), + edit_blob_path(project, ref, path, options), + project, + ref + ) end def can_modify_blob?(blob, project = @project, ref = @ref) @@ -282,8 +284,8 @@ module BlobHelper 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 } + 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) @@ -328,4 +330,17 @@ module BlobHelper @path.to_s.end_with?(Ci::Pipeline::CONFIG_EXTENSION) || @path.to_s == @project.ci_config_path_or_default end + + def vue_blob_app_data(project, blob, ref) + { + blob_path: blob.path, + project_path: project.full_path, + resource_id: project.to_global_id, + user_id: current_user.present? ? current_user.to_global_id : '', + target_branch: project.empty_repo? ? ref : @ref, + original_branch: @ref + } + end end + +BlobHelper.prepend_mod_with('BlobHelper') diff --git a/app/helpers/breadcrumbs_helper.rb b/app/helpers/breadcrumbs_helper.rb index 38ed6e95a44..6996c7a1766 100644 --- a/app/helpers/breadcrumbs_helper.rb +++ b/app/helpers/breadcrumbs_helper.rb @@ -12,7 +12,7 @@ module BreadcrumbsHelper def breadcrumb_title_link return @breadcrumb_link if @breadcrumb_link - request.path + request.fullpath end def breadcrumb_title(title) diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb index 1c5a601de25..2f14c907b12 100644 --- a/app/helpers/broadcast_messages_helper.rb +++ b/app/helpers/broadcast_messages_helper.rb @@ -67,7 +67,10 @@ module BroadcastMessagesHelper { id: message.id, status: broadcast_message_status(message), - preview: broadcast_message(message, preview: true), + message: message.message, + theme: message.theme, + broadcast_type: message.broadcast_type, + dismissable: message.dismissable, starts_at: message.starts_at.iso8601, ends_at: message.ends_at.iso8601, target_roles: target_access_levels_display(message.target_access_levels), @@ -79,10 +82,26 @@ module BroadcastMessagesHelper end.to_json end + def broadcast_message_data(broadcast_message) + { + id: broadcast_message.id, + message: broadcast_message.message, + broadcast_type: broadcast_message.broadcast_type, + theme: broadcast_message.theme, + dismissable: broadcast_message.dismissable.to_s, + target_access_levels: broadcast_message.target_access_levels, + messages_path: admin_broadcast_messages_path, + preview_path: preview_admin_broadcast_messages_path, + target_path: broadcast_message.target_path, + starts_at: broadcast_message.starts_at.iso8601, + ends_at: broadcast_message.ends_at.iso8601, + target_access_level_options: target_access_level_options.to_json + } + end + private def current_user_access_level_for_project_or_group - return if Feature.disabled?(:role_targeted_broadcast_messages) return unless current_user.present? strong_memoize(:current_user_access_level_for_project_or_group) do diff --git a/app/helpers/ci/builds_helper.rb b/app/helpers/ci/builds_helper.rb index afd0af18ba7..8a00c0f3eb0 100644 --- a/app/helpers/ci/builds_helper.rb +++ b/app/helpers/ci/builds_helper.rb @@ -2,18 +2,6 @@ module Ci module BuildsHelper - def build_summary(build, skip: false) - if build.has_trace? - if skip - link_to _('View job log'), pipeline_job_url(build.pipeline, build) - else - build.trace.html(last_lines: 10).html_safe - end - else - _('No job log') - end - end - def sidebar_build_class(build, current_build) build_class = [] build_class << 'active' if build.id === current_build.id @@ -36,15 +24,5 @@ module Ci description: project_job_url(@project, @build) } end - - def prepare_failed_jobs_summary_data(failed_builds) - failed_builds.map do |build| - { - id: build.id, - failure: build.present.callout_failure_message, - failure_summary: build_summary(build) - } - end.to_json - end end end diff --git a/app/helpers/ci/catalog/resources_helper.rb b/app/helpers/ci/catalog/resources_helper.rb new file mode 100644 index 00000000000..9f70410f17f --- /dev/null +++ b/app/helpers/ci/catalog/resources_helper.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Ci + module Catalog + module ResourcesHelper + def can_view_namespace_catalog?(_project) + false + end + + def js_ci_catalog_data(_project) + {} + end + end + end +end + +Ci::Catalog::ResourcesHelper.prepend_mod_with('Ci::Catalog::ResourcesHelper') diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb index 424e5920fed..a7e1de173bd 100644 --- a/app/helpers/ci/jobs_helper.rb +++ b/app/helpers/ci/jobs_helper.rb @@ -18,24 +18,6 @@ module Ci } end - def bridge_data(build, project) - { - "build_id" => build.id, - "empty-state-illustration-path" => image_path('illustrations/job-trigger-md.svg'), - "pipeline_iid" => build.pipeline.iid, - "project_full_path" => project.full_path - } - end - - def job_counts - { - "all" => limited_counter_with_delimiter(@all_builds), - "pending" => limited_counter_with_delimiter(@all_builds.pending), - "running" => limited_counter_with_delimiter(@all_builds.running), - "finished" => limited_counter_with_delimiter(@all_builds.finished) - } - end - def job_statuses statuses = Ci::HasStatus::AVAILABLE_STATUSES diff --git a/app/helpers/ci/pipeline_editor_helper.rb b/app/helpers/ci/pipeline_editor_helper.rb index 99a92ba9b59..4d1bdf5fa7f 100644 --- a/app/helpers/ci/pipeline_editor_helper.rb +++ b/app/helpers/ci/pipeline_editor_helper.rb @@ -18,12 +18,12 @@ module Ci "ci-examples-help-page-path" => help_page_path('ci/examples/index'), "ci-help-page-path" => help_page_path('ci/index'), "ci-lint-path" => project_ci_lint_path(project), + "ci-troubleshooting-path" => help_page_path('ci/troubleshooting', anchor: 'common-cicd-issues'), "default-branch" => project.default_branch_or_main, "empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'), "initial-branch-name" => initial_branch, "includes-help-page-path" => help_page_path('ci/yaml/includes'), "lint-help-page-path" => help_page_path('ci/lint', anchor: 'check-cicd-syntax'), - "lint-unavailable-help-page-path" => help_page_path('ci/pipeline_editor/index', anchor: 'configuration-validation-currently-not-available-message'), "needs-help-page-path" => help_page_path('ci/yaml/index', anchor: 'needs'), "new-merge-request-path" => namespace_project_new_merge_request_path, "pipeline_etag" => latest_commit ? graphql_etag_pipeline_sha_path(latest_commit.sha) : '', diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb index 823332c3d1d..6b15f0c9e20 100644 --- a/app/helpers/ci/pipelines_helper.rb +++ b/app/helpers/ci/pipelines_helper.rb @@ -91,7 +91,7 @@ module Ci artifacts_endpoint: downloadable_artifacts_project_pipeline_path(project, artifacts_endpoint_placeholder, format: :json), artifacts_endpoint_placeholder: artifacts_endpoint_placeholder, pipeline_schedule_url: pipeline_schedules_path(project), - empty_state_svg_path: image_path('illustrations/pipelines_empty.svg'), + empty_state_svg_path: image_path('illustrations/empty-state/empty-pipeline-md.svg'), error_state_svg_path: image_path('illustrations/pipelines_failed.svg'), no_pipelines_svg_path: image_path('illustrations/pipelines_pending.svg'), can_create_pipeline: can?(current_user, :create_pipeline, project).to_s, @@ -101,13 +101,9 @@ module Ci has_gitlab_ci: has_gitlab_ci?(project).to_s, pipeline_editor_path: can?(current_user, :create_pipeline, project) && project_ci_pipeline_editor_path(project), suggested_ci_templates: suggested_ci_templates.to_json, - ci_runner_settings_path: project_settings_ci_cd_path(project, anchor: 'js-runners-settings') + full_path: project.full_path } - experiment(:runners_availability_section, namespace: project.root_ancestor) do |e| - e.candidate { data[:any_runners_available] = project.active_runners.exists?.to_s } - end - experiment(:ios_specific_templates, actor: current_user, project: project, sticky_to: project) do |e| e.candidate do data[:registration_token] = project.runners_token if can?(current_user, :register_project_runners, project) diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb index 41ef0bd20a8..7177ddd3f31 100644 --- a/app/helpers/ci/runners_helper.rb +++ b/app/helpers/ci/runners_helper.rb @@ -35,6 +35,10 @@ module Ci end end + def runner_short_name(runner) + "##{runner.id} (#{runner.short_sha})" + end + def runner_link(runner) display_name = truncate(runner.display_name, length: 15) id = "\##{runner.id}" @@ -66,15 +70,15 @@ module Ci new_runner_path: new_admin_runner_path, registration_token: Gitlab::CurrentSettings.runners_registration_token, online_contact_timeout_secs: ::Ci::Runner::ONLINE_CONTACT_TIMEOUT.to_i, - stale_timeout_secs: ::Ci::Runner::STALE_TIMEOUT.to_i, - empty_state_svg_path: image_path('illustrations/pipelines_empty.svg'), - empty_state_filtered_svg_path: image_path('illustrations/magnifying-glass.svg') + stale_timeout_secs: ::Ci::Runner::STALE_TIMEOUT.to_i } end def group_shared_runners_settings_data(group) { group_id: group.id, + group_name: group.name, + group_is_empty: (group.projects.empty? && group.children.empty?).to_s, shared_runners_setting: group.shared_runners_setting, parent_shared_runners_setting: group.parent&.shared_runners_setting, runner_enabled_value: Namespace::SR_ENABLED, @@ -89,9 +93,7 @@ module Ci group_full_path: group.full_path, runner_install_help_page: 'https://docs.gitlab.com/runner/install/', online_contact_timeout_secs: ::Ci::Runner::ONLINE_CONTACT_TIMEOUT.to_i, - stale_timeout_secs: ::Ci::Runner::STALE_TIMEOUT.to_i, - empty_state_svg_path: image_path('illustrations/pipelines_empty.svg'), - empty_state_filtered_svg_path: image_path('illustrations/magnifying-glass.svg') + stale_timeout_secs: ::Ci::Runner::STALE_TIMEOUT.to_i } end diff --git a/app/helpers/ci/status_helper.rb b/app/helpers/ci/status_helper.rb index bca49324a19..ea5b613cb78 100644 --- a/app/helpers/ci/status_helper.rb +++ b/app/helpers/ci/status_helper.rb @@ -131,10 +131,10 @@ module Ci if path link_to ci_icon_for_status(status, size: icon_size), path, - class: klass, title: title, data: data + class: klass, title: title, data: data else content_tag :span, ci_icon_for_status(status, size: icon_size), - class: klass, title: title, data: data + class: klass, title: title, data: data end end diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index 8449bccd285..b1d61474700 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -37,7 +37,6 @@ module ClustersHelper editable: can_edit.to_s, environment_scope: cluster.environment_scope, base_domain: cluster.base_domain, - application_ingress_external_ip: cluster.application_ingress_external_ip, auto_devops_help_path: help_page_path('topics/autodevops/index'), external_endpoint_help_path: help_page_path('user/project/clusters/gitlab_managed_clusters.md', anchor: 'base-domain') } @@ -57,11 +56,19 @@ module ClustersHelper when 'environments' render_if_exists 'clusters/clusters/environments' when 'health' - render_if_exists 'clusters/clusters/health' + if Feature.enabled?(:remove_monitor_metrics) + render('details', expanded: expanded) + else + render_if_exists 'clusters/clusters/health' + end when 'apps' render 'applications' when 'integrations' - render 'integrations' + if Feature.enabled?(:remove_monitor_metrics) + render('details', expanded: expanded) + else + render 'integrations' + end when 'settings' render 'advanced_settings_container' else diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index f75d3657986..ee86553d75d 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -27,12 +27,11 @@ module CommitsHelper end def commit_to_html(commit, ref, project) - render partial: 'projects/commits/commit', formats: :html, - locals: { - commit: commit, - ref: ref, - project: project - } + render partial: 'projects/commits/commit', formats: :html, locals: { + commit: commit, + ref: ref, + project: project + } end # Breadcrumb links for a Project and, if applicable, a tree path @@ -170,7 +169,8 @@ module CommitsHelper pipeline_status: commit.detailed_status_for(ref)&.cache_key, xhr: request.xhr?, controller: controller.controller_path, - path: @path # referred to in #link_to_browse_code + path: @path, # referred to in #link_to_browse_code + referenced_by: tag_checksum(commit.referenced_by) } ] end @@ -188,16 +188,22 @@ module CommitsHelper entity = mode == 'raw' ? 'rawButton' : 'renderedButton' title = "Display #{mode} diff" - link_to("##{mode}-diff-#{file_hash}", - class: "btn gl-button btn-default btn-file-option has-tooltip btn-show-#{mode}-diff", - title: title, - data: { file_hash: file_hash, diff_toggle_entity: entity }) do + link_to( + "##{mode}-diff-#{file_hash}", + class: "btn gl-button btn-default btn-file-option has-tooltip btn-show-#{mode}-diff", + title: title, + data: { file_hash: file_hash, diff_toggle_entity: entity } + ) do sprite_icon(icon) end end protected + def tag_checksum(tags_array) + ::Zlib.crc32(tags_array.sort.join) + end + # Private: Returns a link to a person. If the person has a matching user and # is a member of the current @project it will link to the team member page. # Otherwise it will link to the person email as specified in the commit. diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index f0e1f252917..7c239f78088 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -40,9 +40,14 @@ module DashboardHelper end) if doc_href.present? - link_to_doc = link_to(sprite_icon('question'), doc_href, - class: 'gl-ml-2', title: _('Documentation'), - target: '_blank', rel: 'noopener noreferrer') + link_to_doc = link_to( + sprite_icon('question-o'), + doc_href, + class: 'gl-ml-2', + title: _('Documentation'), + target: '_blank', + rel: 'noopener noreferrer' + ) concat(link_to_doc) end @@ -52,7 +57,7 @@ module DashboardHelper private def get_dashboard_nav_links - links = [:projects, :groups, :snippets] + links = [:projects, :groups, :snippets, :your_work, :explore] if can?(current_user, :read_cross_project) links += [:activity, :milestones] diff --git a/app/helpers/device_registration_helper.rb b/app/helpers/device_registration_helper.rb new file mode 100644 index 00000000000..bbdcab76bf5 --- /dev/null +++ b/app/helpers/device_registration_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module DeviceRegistrationHelper + def device_registration_data(current_password_required:, target_path:, webauthn_error:) + { + initial_error: webauthn_error && webauthn_error[:message], + target_path: target_path, + password_required: current_password_required.to_s + } + end +end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index e0a1697cfa9..c5df53ec606 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -34,6 +34,12 @@ module DiffHelper options[:expanded] = true options[:paths] = params.values_at(:old_path, :new_path) options[:use_extra_viewer_as_main] = false + + if Feature.enabled?(:large_ipynb_diffs, @project) && params[:file_identifier]&.include?('.ipynb') + options[:max_patch_bytes_for_file_extension] = { + '.ipynb' => 1.megabyte + } + end end options diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 427cbe18fbf..475ba3dcba8 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -54,7 +54,7 @@ module DropdownsHelper default_label = data_attr[:default_label] content_tag(:button, disabled: options[:disabled], class: "dropdown-menu-toggle #{options[:toggle_class] if options.key?(:toggle_class)}", id: (options[:id] if options.key?(:id)), type: "button", data: data_attr) do output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}") - output << sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3") + output << sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon") output.html_safe end end diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb index 2b3700a9f21..00109212934 100644 --- a/app/helpers/environment_helper.rb +++ b/app/helpers/environment_helper.rb @@ -79,7 +79,7 @@ module EnvironmentHelper can_destroy_environment: can_destroy_environment?(environment), can_stop_environment: can?(current_user, :stop_environment, environment), can_admin_environment: can?(current_user, :admin_environment, project), - environment_metrics_path: project_metrics_dashboard_path(project, environment: environment), + **environment_metrics_path(project, environment), environments_fetch_path: project_environments_path(project, format: :json), environment_edit_path: edit_project_environment_path(project, environment), environment_stop_path: stop_project_environment_path(project, environment), @@ -88,11 +88,18 @@ module EnvironmentHelper environment_terminal_path: terminal_project_environment_path(project, environment), has_terminals: environment.has_terminals?, is_environment_available: environment.available?, - auto_stop_at: environment.auto_stop_at + auto_stop_at: environment.auto_stop_at, + graphql_etag_key: environment.etag_cache_key } end def environments_detail_data_json(user, project, environment) environments_detail_data(user, project, environment).to_json end + + def environment_metrics_path(project, environment) + return {} if Feature.enabled?(:remove_monitor_metrics) + + { environment_metrics_path: project_metrics_dashboard_path(project, environment: environment) } + end end diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index 5bf4fa2ffcc..525fdd3e9f6 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -22,6 +22,8 @@ module EnvironmentsHelper end def metrics_data(project, environment) + return {} if Feature.enabled?(:remove_monitor_metrics) + metrics_data = {} metrics_data.merge!(project_metrics_data(project)) if project metrics_data.merge!(environment_metrics_data(environment, project)) if environment diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index bef2da495b0..795d35ec81f 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -29,10 +29,11 @@ module EventsHelper opened: s_('Event|opened'), updated: s_('Event|updated'), 'removed due to membership expiration from': s_('Event|removed due to membership expiration from') - }.merge(localized_push_action_name_map, - localized_created_project_action_name_map, - localized_design_action_names - ).freeze + }.merge( + localized_push_action_name_map, + localized_created_project_action_name_map, + localized_design_action_names + ).freeze end def localized_push_action_name_map @@ -183,13 +184,11 @@ module EventsHelper def event_feed_url(event) if event.issue? - project_issue_url(event.project, - event.issue) + project_issue_url(event.project, event.issue) elsif event.merge_request? project_merge_request_url(event.project, event.merge_request) elsif event.commit_note? - project_commit_url(event.project, - event.note_target) + project_commit_url(event.project, event.note_target) elsif event.note? if event.note_target event_note_target_url(event) @@ -204,16 +203,12 @@ module EventsHelper def push_event_feed_url(event) if event.push_with_commits? && event.md_ref? if event.commits_count > 1 - project_compare_url(event.project, - from: event.commit_from, to: - event.commit_to) + project_compare_url(event.project, from: event.commit_from, to: event.commit_to) else - project_commit_url(event.project, - id: event.commit_to) + project_commit_url(event.project, id: event.commit_to) end elsif event.ref_name - project_commits_url(event.project, - event.ref_name) + project_commits_url(event.project, event.ref_name) end end @@ -241,26 +236,31 @@ module EventsHelper elsif event.design_note? design_url(event.note_target, anchor: dom_id(event.note)) else - polymorphic_url([event.project, event.note_target], - anchor: dom_id(event.target)) + polymorphic_url([event.project, event.note_target], anchor: dom_id(event.target)) end end def event_wiki_title_html(event) capture do concat content_tag(:span, _('wiki page'), class: "event-target-type gl-mr-2") - concat link_to(event.target_title, event_wiki_page_target_url(event), - title: event.target_title, - class: 'has-tooltip event-target-link gl-mr-2') + concat link_to( + event.target_title, + event_wiki_page_target_url(event), + title: event.target_title, + class: 'has-tooltip event-target-link gl-mr-2' + ) end end def event_design_title_html(event) capture do concat content_tag(:span, _('design'), class: "event-target-type gl-mr-2") - concat link_to(event.design.reference_link_text, design_url(event.design), - title: event.target_title, - class: 'has-tooltip event-design event-target-link gl-mr-2') + concat link_to( + event.design.reference_link_text, + design_url(event.design), + title: event.target_title, + class: 'has-tooltip event-design event-target-link gl-mr-2' + ) end end diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb index 2967501f628..ed24f2509e8 100644 --- a/app/helpers/explore_helper.rb +++ b/app/helpers/explore_helper.rb @@ -57,7 +57,7 @@ module ExploreHelper private def get_explore_nav_links - [:projects, :groups, :snippets] + [:projects, :groups, :topics, :snippets] end def request_path_with_options(options = {}) diff --git a/app/helpers/feature_flags_helper.rb b/app/helpers/feature_flags_helper.rb index 3dde29dce91..fe8d8e6b5d9 100644 --- a/app/helpers/feature_flags_helper.rb +++ b/app/helpers/feature_flags_helper.rb @@ -18,8 +18,10 @@ module FeatureFlagsHelper feature_flags_path: project_feature_flags_path(@project), environments_endpoint: search_project_environments_path(@project, format: :json), strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'), - environments_scope_docs_path: help_page_path('ci/environments/index.md', - anchor: 'limit-the-environment-scope-of-a-cicd-variable') + environments_scope_docs_path: help_page_path( + 'ci/environments/index.md', + anchor: 'limit-the-environment-scope-of-a-cicd-variable' + ) } end end diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index a4d90716129..ed8cca20241 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -72,7 +72,8 @@ module FormHelper multi_select: true, 'input-meta': 'name', 'always-show-selectbox': true, - current_user_info: UserSerializer.new.represent(current_user) + current_user_info: UserSerializer.new.represent(current_user), + testid: 'assignee-ids-dropdown-toggle' } } diff --git a/app/helpers/groups/observability_helper.rb b/app/helpers/groups/observability_helper.rb index 6cd6566cee1..7661817da7b 100644 --- a/app/helpers/groups/observability_helper.rb +++ b/app/helpers/groups/observability_helper.rb @@ -22,19 +22,8 @@ module Groups }.freeze def observability_iframe_src(group) - # Format: https://observe.gitlab.com/GROUP_ID - - # When running Observability UI in standalone mode (i.e. not backed by Observability Backend) - # the group-id is not required. This is mostly used for local dev - base_url = ENV['STANDALONE_OBSERVABILITY_UI'] == 'true' ? observability_url : "#{observability_url}/-/#{group.id}" - - sanitized_path = if params[:observability_path] && sanitize(params[:observability_path]) != '' - CGI.unescapeHTML(sanitize(params[:observability_path])) - else - observability_config_for(params).fetch(:path) - end - - "#{base_url}#{sanitized_path}" + Gitlab::Observability.build_full_url(group, params[:observability_path], + observability_config_for(params).fetch(:path)) end def observability_page_title @@ -43,10 +32,6 @@ module Groups private - def observability_url - Gitlab::Observability.observability_url - end - def observability_config_for(params) ACTION_TO_PATH.fetch(params[:action], ACTION_TO_PATH['dashboards']) end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 129871ca3fd..2ced1bec5e9 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -126,21 +126,12 @@ module GroupsHelper def subgroup_creation_data(group) { + parent_group_url: group.parent && group_url(group.parent), parent_group_name: group.parent&.name, import_existing_group_path: new_group_path(parent_id: group.parent_id, anchor: 'import-group-pane') } end - def verification_for_group_creation_data - # overridden in EE - {} - end - - def require_verification_for_namespace_creation_enabled? - # overridden in EE - false - end - def group_name_and_path_app_data { base_path: root_url, @@ -160,6 +151,7 @@ module GroupsHelper new_project_path: new_project_path(namespace_id: group.id), new_subgroup_illustration: image_path('illustrations/subgroup-create-new-sm.svg'), new_project_illustration: image_path('illustrations/project-create-new-sm.svg'), + empty_projects_illustration: image_path('illustrations/empty-state/empty-projects-md.svg'), empty_subgroup_illustration: image_path('illustrations/empty-state/empty-subgroup-md.svg'), render_empty_state: 'true', can_create_subgroups: can?(current_user, :create_subgroup, group).to_s, @@ -167,6 +159,26 @@ module GroupsHelper } end + def group_readme_app_data(group_readme) + { + web_path: group_readme.present.web_path, + name: group_readme.present.name + } + end + + def show_group_readme?(group) + group.group_readme + end + + def group_settings_readme_app_data(group) + { + group_readme_path: group.group_readme&.present&.web_path, + readme_project_path: group.readme_project&.present&.path_with_namespace, + group_path: group.full_path, + group_id: group.id + } + end + def enabled_git_access_protocol_options_for_group case ::Gitlab::CurrentSettings.enabled_git_access_protocol when nil, "" diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb index c5be044a27b..448909543c4 100644 --- a/app/helpers/ide_helper.rb +++ b/app/helpers/ide_helper.rb @@ -1,28 +1,30 @@ # frozen_string_literal: true module IdeHelper - def ide_data(project:, branch:, path:, merge_request:, fork_info:) - { - 'can-use-new-web-ide' => can_use_new_web_ide?.to_s, + # Overridden in EE + def ide_data(project:, fork_info:, params:) + base_data = { 'use-new-web-ide' => use_new_web_ide?.to_s, 'new-web-ide-help-page-path' => help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'), + 'sign-in-path' => new_session_path(current_user), 'user-preferences-path' => profile_preferences_path, - 'branch-name' => branch, - 'file-path' => path, - 'fork-info' => fork_info&.to_json, 'editor-font-src-url' => font_url('jetbrains-mono/JetBrainsMono.woff2'), 'editor-font-family' => 'JetBrains Mono', - 'editor-font-format' => 'woff2', - 'merge-request' => merge_request + 'editor-font-format' => 'woff2' }.merge(use_new_web_ide? ? new_ide_data(project: project) : legacy_ide_data(project: project)) - end - def can_use_new_web_ide? - Feature.enabled?(:vscode_web_ide, current_user) + return base_data unless project + + base_data.merge( + 'fork-info' => fork_info&.to_json, + 'branch-name' => params[:branch], + 'file-path' => params[:path], + 'merge-request' => params[:merge_request_id] + ) end def use_new_web_ide? - can_use_new_web_ide? && !current_user.use_legacy_web_ide + Feature.enabled?(:vscode_web_ide, current_user) end private @@ -41,7 +43,7 @@ module IdeHelper 'empty-state-svg-path' => image_path('illustrations/multi_file_editor_empty.svg'), 'no-changes-state-svg-path' => image_path('illustrations/multi-editor_no_changes_empty.svg'), 'committed-state-svg-path' => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'), - 'pipelines-empty-state-svg-path': image_path('illustrations/pipelines_empty.svg'), + 'pipelines-empty-state-svg-path': image_path('illustrations/empty-state/empty-pipeline-md.svg'), 'switch-editor-svg-path': image_path('illustrations/rocket-launch-md.svg'), 'promotion-svg-path': image_path('illustrations/web-ide_promotion.svg'), 'ci-help-page-path' => help_page_path('ci/quick_start/index'), diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 46d2d2c42d9..3796d8f0210 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -12,8 +12,8 @@ module IssuablesHelper end end - def sidebar_gutter_collapsed_class - return "right-sidebar-expanded" if moved_mr_sidebar_enabled? + def sidebar_gutter_collapsed_class(is_merge_request_with_flag) + return "right-sidebar-expanded" if is_merge_request_with_flag "right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}" end @@ -144,7 +144,7 @@ module IssuablesHelper def issuable_meta(issuable, project) output = [] - if issuable.respond_to?(:work_item_type) && WorkItems::Type::WI_TYPES_WITH_CREATED_HEADER.include?(issuable.work_item_type.base_type) + if issuable.respond_to?(:work_item_type) && WorkItems::Type::WI_TYPES_WITH_CREATED_HEADER.include?(issuable.issue_type) output << content_tag(:span, sprite_icon(issuable.work_item_type.icon_name.to_s, css_class: 'gl-icon gl-vertical-align-middle gl-text-gray-500'), class: 'gl-mr-2', aria: { hidden: 'true' }) output << content_tag(:span, s_('IssuableStatus|%{wi_type} created %{created_at} by ').html_safe % { wi_type: IntegrationsHelper.integration_issue_type(issuable.issue_type), created_at: time_ago_with_tooltip(issuable.created_at) }, class: 'gl-mr-2') else @@ -156,7 +156,7 @@ module IssuablesHelper end output << content_tag(:strong) do - author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline") + author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline-block") author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-inline d-sm-none") author_output << issuable_meta_author_slot(issuable.author, css_class: 'ml-1') @@ -173,9 +173,6 @@ module IssuablesHelper output << content_tag(:span, (sprite_icon('first-contribution', css_class: 'gl-icon gl-vertical-align-middle') if issuable.first_contribution?), class: 'has-tooltip gl-ml-2', title: _('1st contribution!')) - output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-md-inline-block gl-ml-3") - output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none") - output.join.html_safe end @@ -252,7 +249,7 @@ module IssuablesHelper initialTitleText: issuable.title, initialDescriptionHtml: markdown_field(issuable, :description), initialDescriptionText: issuable.description, - initialTaskStatus: issuable.task_status + initialTaskCompletionStatus: issuable.task_completion_status } data.merge!(issue_only_initial_data(issuable)) data.merge!(path_data(parent)) @@ -277,11 +274,13 @@ module IssuablesHelper end def incident_only_initial_data(issue) - return {} unless issue.incident? + return {} unless issue.incident_type_issue? { hasLinkedAlerts: issue.alert_management_alerts.any?, - canUpdateTimelineEvent: can?(current_user, :admin_incident_management_timeline_event, issue) + canUpdateTimelineEvent: can?(current_user, :admin_incident_management_timeline_event, issue), + currentPath: url_for(safe_params), + currentTab: safe_params[:incident_tab] } end @@ -378,13 +377,54 @@ module IssuablesHelper end def hidden_issuable_icon(issuable) - title = format(_('This %{issuable} is hidden because its author has been banned'), - issuable: issuable.is_a?(Issue) ? _('issue') : _('merge request')) + title = format( + _('This %{issuable} is hidden because its author has been banned'), + issuable: issuable.is_a?(Issue) ? _('issue') : _('merge request') + ) content_tag(:span, class: 'has-tooltip', title: title) do sprite_icon('spam', css_class: 'gl-vertical-align-text-bottom') end end + def issuable_type_selector_data(issuable) + { + selected_type: issuable.issue_type, + is_issue_allowed: create_issue_type_allowed?(@project, :issue).to_s, + is_incident_allowed: create_issue_type_allowed?(@project, :incident).to_s, + issue_path: new_project_issue_path(@project), + incident_path: new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }) + } + end + + def issuable_label_selector_data(project, issuable) + initial_labels = issuable.labels.map do |label| + { + __typename: "Label", + id: label.id, + title: label.title, + description: label.description, + color: label.color, + text_color: label.text_color + } + end + + filter_base_path = + if issuable.issuable_type == "merge_request" + project_merge_requests_path(project) + else + project_issues_path(project) + end + + { + field_name: "#{issuable.class.model_name.param_key}[label_ids][]", + full_path: project.full_path, + initial_labels: initial_labels.to_json, + issuable_type: issuable.issuable_type, + labels_filter_base_path: filter_base_path, + labels_manage_path: project_labels_path(project) + } + end + private def sidebar_gutter_collapsed? @@ -434,7 +474,7 @@ module IssuablesHelper toggleSubscriptionEndpoint: issuable[:toggle_subscription_path], moveIssueEndpoint: issuable[:move_issue_path], projectsAutocompleteEndpoint: issuable[:projects_autocomplete_path], - editable: issuable.dig(:current_user, :can_edit), + editable: issuable.dig(:current_user, :can_edit).to_s, currentUser: issuable[:current_user], rootPath: root_path, fullPath: issuable[:project_full_path], diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 39399c2919b..fae8d86098e 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -44,14 +44,6 @@ module IssuesHelper end end - def work_item_type_icon(issue_type) - if WorkItems::Type.base_types.include?(issue_type) - "issue-type-#{issue_type.to_s.dasherize}" - else - 'issue-type-issue' - end - end - def confidential_icon(issue) sprite_icon('eye-slash', css_class: 'gl-vertical-align-text-bottom') if issue.confidential? end @@ -161,9 +153,9 @@ module IssuesHelper issue.moved_from.project.service_desk_enabled? && !issue.project.service_desk_enabled? end - def issue_header_actions_data(project, issuable, current_user) + def issue_header_actions_data(project, issuable, current_user, issuable_sidebar) new_issuable_params = { issue: {}, add_related_issue: issuable.iid } - if issuable.incident? + if issuable.work_item_type&.incident? new_issuable_params[:issuable_template] = 'incident' new_issuable_params[:issue][:issue_type] = 'incident' end @@ -176,6 +168,7 @@ module IssuesHelper can_report_spam: issuable.submittable_as_spam_by?(current_user).to_s, can_update_issue: can?(current_user, :update_issue, issuable).to_s, iid: issuable.iid, + issuable_id: issuable.id, is_issue_author: (issuable.author == current_user).to_s, issue_path: issuable_path(issuable), issue_type: issuable_display_type(issuable), @@ -184,7 +177,8 @@ module IssuesHelper report_abuse_path: add_category_abuse_reports_path, reported_user_id: issuable.author.id, reported_from_url: issue_url(issuable), - submit_as_spam_path: mark_as_spam_project_issue_path(project, issuable) + submit_as_spam_path: mark_as_spam_project_issue_path(project, issuable), + issuable_email_address: issuable_sidebar.nil? ? '' : issuable_sidebar[:create_note_email] } end diff --git a/app/helpers/jira_connect_helper.rb b/app/helpers/jira_connect_helper.rb index 50e3c3cc5fe..5cf68db0611 100644 --- a/app/helpers/jira_connect_helper.rb +++ b/app/helpers/jira_connect_helper.rb @@ -7,12 +7,10 @@ module JiraConnectHelper { groups_path: api_v4_groups_path(params: { min_access_level: Gitlab::Access::MAINTAINER, skip_groups: skip_groups }), subscriptions: subscriptions.map { |s| serialize_subscription(s) }.to_json, - add_subscriptions_path: jira_connect_subscriptions_path, subscriptions_path: jira_connect_subscriptions_path(format: :json), - users_path: current_user ? nil : jira_connect_users_path, # users_path is used to determine if user is signed in gitlab_user_path: current_user ? user_path(current_user) : nil, - oauth_metadata: Feature.enabled?(:jira_connect_oauth, current_user) ? jira_connect_oauth_data(installation).to_json : nil, - public_key_storage_enabled: Gitlab.config.jira_connect.enable_public_keys_storage || Gitlab::CurrentSettings.jira_connect_public_key_storage_enabled? + oauth_metadata: jira_connect_oauth_data(installation).to_json, + public_key_storage_enabled: Gitlab::CurrentSettings.jira_connect_public_key_storage_enabled? } end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 8c069bc828b..c4967a42a45 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -80,27 +80,20 @@ module LabelsHelper def suggested_colors { + '#cc338b' => s_('SuggestedColors|Magenta-pink'), + '#dc143c' => s_('SuggestedColors|Crimson'), + '#c21e56' => s_('SuggestedColors|Rose red'), + '#cd5b45' => s_('SuggestedColors|Dark coral'), + '#ed9121' => s_('SuggestedColors|Carrot orange'), + '#eee600' => s_('SuggestedColors|Titanium yellow'), '#009966' => s_('SuggestedColors|Green-cyan'), '#8fbc8f' => s_('SuggestedColors|Dark sea green'), - '#3cb371' => s_('SuggestedColors|Medium sea green'), - '#00b140' => s_('SuggestedColors|Green screen'), - '#013220' => s_('SuggestedColors|Dark green'), '#6699cc' => s_('SuggestedColors|Blue-gray'), - '#0000ff' => s_('SuggestedColors|Blue'), '#e6e6fa' => s_('SuggestedColors|Lavender'), '#9400d3' => s_('SuggestedColors|Dark violet'), '#330066' => s_('SuggestedColors|Deep violet'), - '#808080' => s_('SuggestedColors|Gray'), '#36454f' => s_('SuggestedColors|Charcoal grey'), - '#f7e7ce' => s_('SuggestedColors|Champagne'), - '#c21e56' => s_('SuggestedColors|Rose red'), - '#cc338b' => s_('SuggestedColors|Magenta-pink'), - '#dc143c' => s_('SuggestedColors|Crimson'), - '#ff0000' => s_('SuggestedColors|Red'), - '#cd5b45' => s_('SuggestedColors|Dark coral'), - '#eee600' => s_('SuggestedColors|Titanium yellow'), - '#ed9121' => s_('SuggestedColors|Carrot orange'), - '#c39953' => s_('SuggestedColors|Aztec Gold') + '#808080' => s_('SuggestedColors|Gray') } end diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index 4a5720e757d..91fce6d6820 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -80,9 +80,7 @@ module MarkupHelper ) ) - # since <img> tags are stripped, this can leave empty <a> tags hanging around - # (as our markdown wraps images in links) - strip_empty_link_tags(text).html_safe + render_links(text) end def markdown(text, context = {}) @@ -171,9 +169,22 @@ module MarkupHelper { project: wiki.container } end - def strip_empty_link_tags(text) + # Sanitize and style user references links + # + # @param String text the string to be sanitized + # + # 1. Remove empty <a> tags which are caused by the <img> tags being stripped + # (as our markdown wraps images in links) + # 2. Strip all link tags, except user references, leaving just the link text + # 3. Add a highlight class for current user's references + # + # @return sanitized HTML string + def render_links(text) scrubber = Loofah::Scrubber.new do |node| - node.remove if node.name == 'a' && node.children.empty? + next unless node.name == 'a' + next node.remove if node.children.empty? + next node.replace(node.children) if node['data-reference-type'] != 'user' + next node.append_class('current-user') if current_user && node['data-user'] == current_user.id.to_s end sanitize text, scrubber: scrubber @@ -181,7 +192,7 @@ module MarkupHelper def markdown_toolbar_button(options = {}) data = options[:data].merge({ container: 'body' }) - css_classes = %w[gl-button btn btn-default-tertiary btn-icon js-md has-tooltip] << options[:css_class].to_s + css_classes = %w[gl-button btn btn-default-tertiary btn-icon btn-sm js-md has-tooltip] << options[:css_class].to_s content_tag :button, type: 'button', class: css_classes.join(' '), diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index ec395baef9e..15901e13c1a 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -2,6 +2,7 @@ module MergeRequestsHelper include Gitlab::Utils::StrongMemoize + include CompareHelper def create_mr_button_from_event?(event) create_mr_button?(from: event.branch_name, source_project: event.project) @@ -178,6 +179,10 @@ module MergeRequestsHelper end end + def moved_mr_sidebar_enabled? + Feature.enabled?(:moved_mr_sidebar, @project) + end + def diffs_tab_pane_data(project, merge_request, params) { "is-locked": merge_request.discussion_locked?, @@ -185,6 +190,7 @@ module MergeRequestsHelper endpoint_metadata: @endpoint_metadata_url, endpoint_batch: diffs_batch_project_json_merge_request_path(project, merge_request, 'json', params), endpoint_coverage: @coverage_path, + endpoint_diff_for_path: diff_for_path_namespace_project_merge_request_path(format: 'json', id: merge_request.iid, namespace_id: project.namespace.to_param, project_id: project.path), help_page_path: help_page_path('user/project/merge_requests/reviews/suggestions.md'), current_user_data: @current_user_data, update_current_user_path: @update_current_user_path, @@ -195,10 +201,11 @@ module MergeRequestsHelper show_suggest_popover: show_suggest_popover?.to_s, show_whitespace_default: @show_whitespace_default.to_s, file_by_file_default: @file_by_file_default.to_s, - default_suggestion_commit_message: default_suggestion_commit_message, - source_project_default_url: @merge_request.source_project && default_url_to_repo(@merge_request.source_project), - source_project_full_path: @merge_request.source_project&.full_path, - is_forked: @project.forked?.to_s + default_suggestion_commit_message: default_suggestion_commit_message(project), + source_project_default_url: merge_request.source_project && default_url_to_repo(merge_request.source_project), + source_project_full_path: merge_request.source_project&.full_path, + is_forked: project.forked?.to_s, + new_comment_template_path: profile_comment_templates_path } end @@ -225,36 +232,52 @@ module MergeRequestsHelper current_user.review_requested_open_merge_requests_count end - def default_suggestion_commit_message - @project.suggestion_commit_message.presence || Gitlab::Suggestions::CommitMessage::DEFAULT_SUGGESTION_COMMIT_MESSAGE + def default_suggestion_commit_message(project) + project.suggestion_commit_message.presence || Gitlab::Suggestions::CommitMessage::DEFAULT_SUGGESTION_COMMIT_MESSAGE end def merge_request_source_branch(merge_request) + fork_icon = if merge_request.for_fork? + title = _('The source project is a fork') + content_tag(:span, class: 'gl-vertical-align-middle gl-mr-n2 has-tooltip', title: title) do + sprite_icon('fork', size: 12, css_class: 'gl-ml-1 has-tooltip') + end + else + '' + end + branch = if merge_request.for_fork? - "#{merge_request.source_project_path}:#{merge_request.source_branch}" + _('%{fork_icon} %{source_project_path}:%{source_branch}').html_safe % { fork_icon: fork_icon.html_safe, source_project_path: merge_request.source_project_path.html_safe, source_branch: merge_request.source_branch.html_safe } else merge_request.source_branch end + branch_title = if merge_request.for_fork? + _('%{source_project_path}:%{source_branch}').html_safe % { source_project_path: merge_request.source_project_path.html_safe, source_branch: merge_request.source_branch.html_safe } + else + merge_request.source_branch + end + branch_path = if merge_request.source_project project_tree_path(merge_request.source_project, merge_request.source_branch) else '' end - link_to branch, branch_path, title: branch, class: 'gl-text-blue-500! gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mx-2' + link_to branch, branch_path, title: branch_title, class: 'gl-text-blue-500! gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mx-2' end def merge_request_header(project, merge_request) link_to_author = link_to_member(project, merge_request.author, size: 24, extra_class: 'gl-font-weight-bold gl-mr-2', avatar: false) copy_button = clipboard_button(text: merge_request.source_branch, title: _('Copy branch name'), class: 'btn btn-default btn-sm gl-button btn-default-tertiary btn-icon gl-display-none! gl-md-display-inline-block! js-source-branch-copy') + target_branch = link_to merge_request.target_branch, project_tree_path(merge_request.target_project, merge_request.target_branch), title: merge_request.target_branch, class: 'gl-text-blue-500! gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mx-2' _('%{author} requested to merge %{source_branch} %{copy_button} into %{target_branch} %{created_at}').html_safe % { author: link_to_author.html_safe, source_branch: merge_request_source_branch(merge_request).html_safe, copy_button: copy_button.html_safe, target_branch: target_branch.html_safe, created_at: time_ago_with_tooltip(merge_request.created_at, html_class: 'gl-display-inline-block').html_safe } end - def moved_mr_sidebar_enabled? - Feature.enabled?(:moved_mr_sidebar, @project) && defined?(@merge_request) + def single_file_file_by_file? + Feature.enabled?(:single_file_file_by_file, @project) end def sticky_header_data @@ -282,6 +305,10 @@ module MergeRequestsHelper hidden_issuable_icon(merge_request) end + + def tab_count_display(merge_request, count) + merge_request.preparing? ? "-" : count + end end MergeRequestsHelper.prepend_mod_with('MergeRequestsHelper') diff --git a/app/helpers/mirror_helper.rb b/app/helpers/mirror_helper.rb index 3dfd30f07db..06deaeb5e9e 100644 --- a/app/helpers/mirror_helper.rb +++ b/app/helpers/mirror_helper.rb @@ -13,7 +13,7 @@ module MirrorHelper docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: docs_link_url } html_escape(_('Git LFS objects will be synced if LFS is %{docs_link_start}enabled for the project%{docs_link_end}. Push mirrors will %{strong_open}not%{strong_close} sync LFS objects over SSH.')) % - { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe, strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe } + { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe, strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe } end end diff --git a/app/helpers/nav/new_dropdown_helper.rb b/app/helpers/nav/new_dropdown_helper.rb index ddd6469a9e4..201007863b2 100644 --- a/app/helpers/nav/new_dropdown_helper.rb +++ b/app/helpers/nav/new_dropdown_helper.rb @@ -6,19 +6,19 @@ module Nav return unless current_user menu_sections = [] + data = { title: _('Create new...') } - if group&.persisted? - menu_sections.push(group_menu_section(group)) - elsif project&.persisted? + if project&.persisted? menu_sections.push(project_menu_section(project)) + elsif group&.persisted? + menu_sections.push(group_menu_section(group)) end menu_sections.push(general_menu_section) - { - title: _("Create new..."), - menu_sections: menu_sections.select { |x| x.fetch(:menu_items).any? } - } + data[:menu_sections] = menu_sections.select { |x| x.fetch(:menu_items).any? } + + data end private @@ -51,11 +51,7 @@ module Nav menu_items.push(create_epic_menu_item(group)) if can?(current_user, :admin_group_member, group) - menu_items.push( - invite_members_menu_item( - href: group_group_members_path(group) - ) - ) + menu_items.push(invite_members_menu_item(partial: 'groups/invite_members_top_nav_link')) end { @@ -102,11 +98,7 @@ module Nav end if can_admin_project_member?(project) - menu_items.push( - invite_members_menu_item( - href: project_project_members_path(project) - ) - ) + menu_items.push(invite_members_menu_item(partial: 'projects/invite_members_top_nav_link')) end { @@ -157,16 +149,16 @@ module Nav } end - def invite_members_menu_item(href:) + def invite_members_menu_item(partial:) ::Gitlab::Nav::TopNavMenuItem.build( id: 'invite', title: s_('InviteMember|Invite members'), - emoji: 'shaking_hands', - href: href, + icon: 'shaking_hands', + partial: partial, + component: 'invite_members', data: { - track_action: 'click_link_invite_members', - track_label: 'plus_menu_dropdown', - track_property: 'navigation_top' + trigger_source: 'top-nav', + trigger_element: 'text-emoji' } ) end diff --git a/app/helpers/nav/top_nav_helper.rb b/app/helpers/nav/top_nav_helper.rb index fb11c183aeb..c41cf7f500f 100644 --- a/app/helpers/nav/top_nav_helper.rb +++ b/app/helpers/nav/top_nav_helper.rb @@ -64,7 +64,6 @@ module Nav end def build_anonymous_view_model(builder:) - # These come from `app/views/layouts/nav/_explore.html.ham` if explore_nav_link?(:projects) builder.add_primary_menu_item_with_shortcut( header: top_nav_localized_headers[:explore], @@ -83,6 +82,15 @@ module Nav ) end + if explore_nav_link?(:topics) + builder.add_primary_menu_item_with_shortcut( + header: top_nav_localized_headers[:explore], + active: active_nav_link?(page: topics_explore_projects_path, path: 'projects#topic'), + href: topics_explore_projects_path, + **topics_menu_item_attrs + ) + end + if explore_nav_link?(:snippets) builder.add_primary_menu_item_with_shortcut( header: top_nav_localized_headers[:explore], @@ -123,39 +131,54 @@ module Nav builder.add_view(GROUPS_VIEW, container_view_props(namespace: 'groups', current_item: current_item, submenu: groups_submenu)) end + if dashboard_nav_link?(:your_work) + builder.add_primary_menu_item( + id: 'your-work', + header: top_nav_localized_headers[:switch_to], + title: _('Your work'), + href: dashboard_projects_path, + active: active_nav_link?(controller: []), + icon: 'work', + data: { **menu_data_tracking_attrs('your-work') } + ) + end + + if dashboard_nav_link?(:explore) + builder.add_primary_menu_item( + id: 'explore', + header: top_nav_localized_headers[:switch_to], + title: _('Explore'), + href: explore_projects_path, + active: active_nav_link?(controller: ["explore/groups", "explore/snippets"], page: ["/explore/projects", "/explore", "/explore/projects/topics"], path: ["projects#topic"]), + icon: 'compass', + data: { **menu_data_tracking_attrs('explore') } + ) + end + if dashboard_nav_link?(:milestones) - builder.add_primary_menu_item_with_shortcut( - id: 'milestones', - header: top_nav_localized_headers[:explore], + builder.add_shortcut( + id: 'milestones-shortcut', title: _('Milestones'), href: dashboard_milestones_path, - active: active_nav_link?(controller: 'dashboard/milestones'), - icon: 'clock', - data: { **menu_data_tracking_attrs('milestones') }, - shortcut_class: 'dashboard-shortcuts-milestones' + css_class: 'dashboard-shortcuts-milestones' ) end if dashboard_nav_link?(:snippets) - builder.add_primary_menu_item_with_shortcut( - header: top_nav_localized_headers[:explore], - active: active_nav_link?(controller: 'dashboard/snippets'), - data: { qa_selector: 'snippets_link', **menu_data_tracking_attrs('snippets') }, + builder.add_shortcut( + id: 'snippets-shortcut', + title: _('Snippets'), href: dashboard_snippets_path, - **snippets_menu_item_attrs + css_class: 'dashboard-shortcuts-snippets' ) end if dashboard_nav_link?(:activity) - builder.add_primary_menu_item_with_shortcut( - id: 'activity', - header: top_nav_localized_headers[:explore], + builder.add_shortcut( + id: 'activity-shortcut', title: _('Activity'), href: activity_dashboard_path, - active: active_nav_link?(path: 'dashboard#activity'), - icon: 'history', - data: { **menu_data_tracking_attrs('activity') }, - shortcut_class: 'dashboard-shortcuts-activity' + css_class: 'dashboard-shortcuts-activity' ) end @@ -180,14 +203,14 @@ module Nav if header_link?(:admin_mode) builder.add_secondary_menu_item( id: 'leave_admin_mode', - title: _('Leave Admin Mode'), + title: _('Leave admin mode'), active: active_nav_link?(controller: 'admin/sessions'), icon: 'lock-open', href: destroy_admin_session_path, data: { method: 'post', **menu_data_tracking_attrs('leave_admin_mode') } ) elsif current_user.admin? - title = _('Enter Admin Mode') + title = _('Enter admin mode') builder.add_secondary_menu_item( id: 'enter_admin_mode', @@ -220,6 +243,15 @@ module Nav } end + def topics_menu_item_attrs + { + id: 'topics', + title: _('Topics'), + icon: 'labels', + shortcut_class: 'dashboard-shortcuts-topics' + } + end + def snippets_menu_item_attrs { id: 'snippets', diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index d0421cd5184..4f30b555ba0 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module NavHelper + extend self + def header_links @header_links ||= get_header_links end @@ -9,10 +11,29 @@ module NavHelper header_links.include?(link) end + def page_has_sidebar? + defined?(@left_sidebar) && @left_sidebar + end + + def page_has_collapsed_sidebar? + page_has_sidebar? && collapsed_sidebar? + end + + def page_has_collapsed_super_sidebar? + page_has_sidebar? && collapsed_super_sidebar? + 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 + + if show_super_sidebar? + class_name << 'page-with-super-sidebar' if page_has_sidebar? + class_name << 'page-with-super-sidebar-collapsed' if page_has_collapsed_super_sidebar? + else + class_name << 'page-with-contextual-sidebar' if page_has_sidebar? + class_name << 'page-with-icon-sidebar' if page_has_collapsed_sidebar? + end + class_name -= ['right-sidebar-expanded'] if defined?(@right_sidebar) && !@right_sidebar class_name @@ -65,8 +86,21 @@ module NavHelper %w(dev_ops_report usage_trends) end - def show_super_sidebar? - Feature.enabled?(:super_sidebar_nav, current_user) && current_user&.use_new_navigation + def show_super_sidebar?(user = current_user) + return false unless Feature.enabled?(:super_sidebar_nav, user) + + # The new sidebar is not enabled for anonymous use + # Once we enable the new sidebar by default, this + # should return true + return false unless user + + # Users who got the special `super_sidebar_nav_enrolled` enabled, + # see the new nav as long as they don't explicitly opt-out via the toggle + if user.use_new_navigation.nil? && Feature.enabled?(:super_sidebar_nav_enrolled, user) + true + else + !!user.use_new_navigation + end end private diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index b47f4633348..3e8872dc199 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -77,8 +77,10 @@ module NotesHelper line_type: line_type } - button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', - data: data, title: 'Add a reply' + 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) @@ -151,7 +153,6 @@ module NotesHelper def initial_notes_data(autocomplete) { notesUrl: notes_url, - notesIds: @noteable.notes.pluck(:id), # rubocop: disable CodeReuse/ActiveRecord now: Time.now.to_i, diffView: diff_view, enableGFM: { diff --git a/app/helpers/operations_helper.rb b/app/helpers/operations_helper.rb index baeb9a477c3..8528f5f04f7 100644 --- a/app/helpers/operations_helper.rb +++ b/app/helpers/operations_helper.rb @@ -12,7 +12,7 @@ module OperationsHelper def alerts_settings_data(disabled: false) setting = project_incident_management_setting - templates = setting.available_issue_templates.map { |t| { key: t.key, name: t.name } } + templates = setting.available_issue_templates.map { |t| { value: t.key, text: t.name } } { 'prometheus_activated' => prometheus_integration.manual_configuration?.to_s, diff --git a/app/helpers/packages_helper.rb b/app/helpers/packages_helper.rb index f9ec20bdd01..8861f1ffe9a 100644 --- a/app/helpers/packages_helper.rb +++ b/app/helpers/packages_helper.rb @@ -27,9 +27,14 @@ module PackagesHelper presenter.detail_view.to_json end - def pypi_registry_url(project_id) - full_url = expose_url(api_v4_projects_packages_pypi_simple_package_name_path({ id: project_id, package_name: '' }, true)) - full_url.sub!('://', '://__token__:<your_personal_token>@') + def pypi_registry_url(project) + full_url = expose_url(api_v4_projects_packages_pypi_simple_package_name_path({ id: project.id, package_name: '' }, true)) + + if project.project_feature.public_packages? + full_url + else + full_url.sub!('://', '://__token__:<your_personal_token>@') + end end def composer_registry_url(group_id) @@ -64,6 +69,11 @@ module PackagesHelper Ability.allowed?(current_user, :admin_package, project) end + def show_group_package_registry_settings(group) + group.packages_feature_enabled? && + Ability.allowed?(current_user, :admin_group, group) + end + def cleanup_settings_data { project_id: @project.id, diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 4a218984af1..9bcabd7d9c6 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -175,7 +175,7 @@ module PageLayoutHelper current_emoji: user.status.emoji.to_s, current_message: user.status.message.to_s, current_availability: user.status.availability.to_s, - current_clear_status_after: user.status.clear_status_at&.to_s(:iso8601) + current_clear_status_after: user_clear_status_at(user) }) end diff --git a/app/helpers/plan_limits_helper.rb b/app/helpers/plan_limits_helper.rb new file mode 100644 index 00000000000..74653ad3511 --- /dev/null +++ b/app/helpers/plan_limits_helper.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module PlanLimitsHelper + def plan_limit_setting_description(limit_name) + case limit_name + when :ci_pipeline_size + s_('AdminSettings|Maximum number of jobs in a single pipeline') + when :ci_active_jobs + s_('AdminSettings|Total number of jobs in currently active pipelines') + when :ci_project_subscriptions + s_('AdminSettings|Maximum number of pipeline subscriptions to and from a project') + when :ci_pipeline_schedules + s_('AdminSettings|Maximum number of pipeline schedules') + when :ci_needs_size_limit + s_('AdminSettings|Maximum number of DAG dependencies that a job can have') + when :ci_registered_group_runners + s_('AdminSettings|Maximum number of runners registered per group') + when :ci_registered_project_runners + s_('AdminSettings|Maximum number of runners registered per project') + when :pipeline_hierarchy_size + s_("AdminSettings|Maximum number of downstream pipelines in a pipeline's hierarchy tree") + else + raise ArgumentError, "No description available for plan limit #{limit_name}" + end + end +end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 2442856d7fe..f2fa82aebdb 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -132,7 +132,7 @@ module PreferencesHelper Gitlab::CurrentSettings.gitpod_url.presence || 'https://gitpod.io/' end - # Ensure that anyone adding new options updates `DASHBOARD_CHOICES` too + # Ensure that anyone adding new options updates `localized_dashboard_choices` too def validate_dashboard_choices!(user_dashboards) if user_dashboards.size != localized_dashboard_choices.size raise "`User` defines #{user_dashboards.size} dashboard choices," \ diff --git a/app/helpers/product_analytics_helper.rb b/app/helpers/product_analytics_helper.rb deleted file mode 100644 index b040a8581b2..00000000000 --- a/app/helpers/product_analytics_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module ProductAnalyticsHelper - def product_analytics_tracker_url - ProductAnalytics::Tracker::URL - end - - def product_analytics_tracker_collector_url - ProductAnalytics::Tracker::COLLECTOR_URL - end -end diff --git a/app/helpers/projects/error_tracking_helper.rb b/app/helpers/projects/error_tracking_helper.rb index 471565d162c..fc4ad10db21 100644 --- a/app/helpers/projects/error_tracking_helper.rb +++ b/app/helpers/projects/error_tracking_helper.rb @@ -5,8 +5,7 @@ module Projects::ErrorTrackingHelper error_tracking_enabled = !!project.error_tracking_setting&.enabled? { - 'index-path' => project_error_tracking_index_path(project, - format: :json), + 'index-path' => project_error_tracking_index_path(project, format: :json), 'user-can-enable-error-tracking' => can?(current_user, :admin_operations, project).to_s, 'enable-error-tracking-link' => project_settings_operations_path(project), 'error-tracking-enabled' => error_tracking_enabled.to_s, diff --git a/app/helpers/projects/ml/experiments_helper.rb b/app/helpers/projects/ml/experiments_helper.rb index 55216d412a5..7aef208447a 100644 --- a/app/helpers/projects/ml/experiments_helper.rb +++ b/app/helpers/projects/ml/experiments_helper.rb @@ -5,20 +5,10 @@ module Projects require 'json' include ActionView::Helpers::NumberHelper - def show_candidate_view_model(candidate) + def experiment_as_data(experiment) data = { - candidate: { - params: candidate.params, - metrics: candidate.latest_metrics, - info: { - iid: candidate.iid, - path_to_artifact: link_to_artifact(candidate), - experiment_name: candidate.experiment.name, - path_to_experiment: link_to_experiment(candidate.project, candidate.experiment), - status: candidate.status - }, - metadata: candidate.metadata - } + name: experiment.name, + path: link_to_experiment(experiment.project, experiment) } Gitlab::Json.generate(data) @@ -29,6 +19,7 @@ module Projects { **candidate.params.to_h { |p| [p.name, p.value] }, **candidate.latest_metrics.to_h { |m| [m.name, number_with_precision(m.value, precision: 4)] }, + ci_job: job_info(candidate), artifact: link_to_artifact(candidate), details: link_to_details(candidate), name: candidate.name, @@ -81,6 +72,17 @@ module Projects project_ml_candidate_path(candidate.project, candidate.iid) end + def job_info(candidate) + return unless candidate.from_ci? + + build = candidate.ci_build + + { + path: project_job_path(build.project, build), + name: build.name + } + end + def link_to_experiment(project, experiment) project_ml_experiment_path(project, experiment.iid) end diff --git a/app/helpers/projects/pipeline_helper.rb b/app/helpers/projects/pipeline_helper.rb index 5c62920cd89..0239253d8f0 100644 --- a/app/helpers/projects/pipeline_helper.rb +++ b/app/helpers/projects/pipeline_helper.rb @@ -7,8 +7,7 @@ module Projects def js_pipeline_tabs_data(project, pipeline, _user) { failed_jobs_count: pipeline.failed_builds.count, - failed_jobs_summary: prepare_failed_jobs_summary_data(pipeline.failed_builds), - full_path: project.full_path, + project_path: project.full_path, graphql_resource_etag: graphql_etag_pipeline_path(pipeline), metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: project.namespace, project_id: project, format: :json), pipeline_iid: pipeline.iid, diff --git a/app/helpers/projects/settings/branch_rules_helper.rb b/app/helpers/projects/settings/branch_rules_helper.rb new file mode 100644 index 00000000000..e53275d8183 --- /dev/null +++ b/app/helpers/projects/settings/branch_rules_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Projects + module Settings + module BranchRulesHelper + def branch_rules_data(project) + { + project_path: project.full_path, + protected_branches_path: project_settings_repository_path(project, anchor: 'js-protected-branches-settings'), + approval_rules_path: project_settings_merge_requests_path(project, + anchor: 'js-merge-request-approval-settings'), + status_checks_path: project_settings_merge_requests_path(project, anchor: 'js-merge-request-settings'), + branches_path: project_branches_path(project), + show_status_checks: 'false', + show_approvers: 'false', + show_code_owners: 'false' + } + end + end + end +end + +Projects::Settings::BranchRulesHelper.prepend_mod diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 04190bc442b..1e87d2861d4 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -2,6 +2,8 @@ module ProjectsHelper include Gitlab::Utils::StrongMemoize + include CompareHelper + include Gitlab::Allowable def project_incident_management_setting @project_incident_management_setting ||= @project.incident_management_setting || @@ -130,12 +132,22 @@ module ProjectsHelper source_default_branch = source_project.default_branch + merge_request = + MergeRequest.opened + .from_project(project).of_projects(source_project.id).from_source_branches(ref).first + { + project_path: project.full_path, + selected_branch: ref, source_name: source_project.full_name, source_path: project_path(source_project), + source_default_branch: source_default_branch, + can_sync_branch: can_sync_branch?(project, ref).to_s, ahead_compare_path: project_compare_path( project, from: source_default_branch, to: ref, from_project_id: source_project.id ), + create_mr_path: create_merge_request_path(project, source_project, ref, merge_request), + view_mr_path: merge_request && project_merge_request_path(source_project, merge_request), behind_compare_path: project_compare_path( source_project, from: ref, to: source_default_branch, from_project_id: project.id ) @@ -217,6 +229,18 @@ module ProjectsHelper .load_in_batch_for_projects(projects) end + def last_pipeline_from_status_cache(project) + if Feature.enabled?(:last_pipeline_from_pipeline_status, project) + pipeline_status = project.pipeline_status + return unless pipeline_status.has_status? + + # commits have far more attributes than id, but last_pipeline only requires sha + return Commit.from_hash({ id: pipeline_status.sha }, project).last_pipeline + end + + project.last_pipeline + end + def show_no_ssh_key_message? Gitlab::CurrentSettings.user_show_add_ssh_key_message? && cookies[:hide_no_ssh_message].blank? && @@ -235,6 +259,14 @@ module ProjectsHelper cookies["hide_auto_devops_implicitly_enabled_banner_#{project.id}".to_sym].blank? end + def show_mobile_devops_project_promo?(project) + return false unless ::Feature.enabled?(:mobile_devops_projects_promo, project) + + return false unless (project.project_setting.target_platforms & ::ProjectSetting::ALLOWED_TARGET_PLATFORMS).any? + + cookies["hide_mobile_devops_promo_#{project.id}".to_sym].blank? + end + def no_password_message push_pull_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('topics/git/terminology', anchor: 'pull-and-push') } clone_with_https_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('gitlab-basics/start-using-git', anchor: 'clone-with-https') } @@ -474,7 +506,7 @@ module ProjectsHelper def clusters_deprecation_alert_message if has_active_license? - s_('ClusterIntegration|The certificate-based Kubernetes integration has been deprecated and will be turned off at the end of February 2023. Please %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd} or reach out to GitLab support.') + s_('ClusterIntegration|The certificate-based Kubernetes integration has been deprecated and will be turned off at the end of February 2023. Please %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd}. Contact GitLab Support if you have any additional questions.') else s_('ClusterIntegration|The certificate-based Kubernetes integration has been deprecated and will be turned off at the end of February 2023. Please %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd}.') end @@ -498,8 +530,38 @@ module ProjectsHelper format_cached_count(1000, number) end + def remote_mirror_setting_enabled? + false + end + + def http_clone_url_to_repo(project) + project.http_url_to_repo + end + + def ssh_clone_url_to_repo(project) + project.ssh_url_to_repo + end + private + def create_merge_request_path(project, source_project, ref, merge_request) + return if merge_request.present? + return unless can?(current_user, :create_merge_request_from, project) + return unless can?(current_user, :create_merge_request_in, source_project) + + create_mr_path( + from: ref, + source_project: project, + to: source_project.default_branch, + target_project: source_project) + end + + def can_sync_branch?(project, ref) + return false unless project.repository.branch_exists?(ref) + + ::Gitlab::UserAccess.new(current_user, container: project).can_push_to_branch?(ref) + end + def localized_access_names { Gitlab::Access::NO_ACCESS => _('No access'), @@ -753,7 +815,7 @@ module ProjectsHelper end def show_visibility_confirm_modal?(project) - project.unlink_forks_upon_visibility_decrease_enabled? && project.visibility_level > Gitlab::VisibilityLevel::PRIVATE && project.forks_count > 0 + project.visibility_level > Gitlab::VisibilityLevel::PRIVATE && project.forks_count > 0 end def confirm_reduce_visibility_message(project) @@ -824,4 +886,12 @@ def can_admin_group_clusters?(project) project.group && project.group.clusters.any? && can?(current_user, :admin_cluster, project.group) end +def can_view_branch_rules? + can?(current_user, :maintainer_access, @project) +end + +def branch_rules_path + project_settings_repository_path(@project, anchor: 'js-branch-rules') +end + ProjectsHelper.prepend_mod_with('ProjectsHelper') diff --git a/app/helpers/protected_branches_helper.rb b/app/helpers/protected_branches_helper.rb index 07b07bfd33c..bd2a4d1170d 100644 --- a/app/helpers/protected_branches_helper.rb +++ b/app/helpers/protected_branches_helper.rb @@ -17,3 +17,5 @@ module ProtectedBranchesHelper end end end + +ProtectedBranchesHelper.prepend_mod diff --git a/app/helpers/protected_refs_helper.rb b/app/helpers/protected_refs_helper.rb new file mode 100644 index 00000000000..60f298e0e8d --- /dev/null +++ b/app/helpers/protected_refs_helper.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module ProtectedRefsHelper + include Gitlab::Utils::StrongMemoize + + def protected_access_levels_for_dropdowns + { + create_access_levels: protected_access_level_dropdown_roles, + push_access_levels: protected_access_level_dropdown_roles, + merge_access_levels: protected_access_level_dropdown_roles + } + end + + def protected_access_level_dropdown_roles + roles = ProtectedRef::AccessLevel.human_access_levels.map do |id, text| + { id: id, text: text, before_divider: true } + end + + { roles: roles } + end + strong_memoize_attr(:protected_access_level_dropdown_roles) +end diff --git a/app/helpers/registrations_helper.rb b/app/helpers/registrations_helper.rb index 1724e11a6f1..fcd560dbe8c 100644 --- a/app/helpers/registrations_helper.rb +++ b/app/helpers/registrations_helper.rb @@ -11,8 +11,8 @@ module RegistrationsHelper } end - def arkose_labs_challenge_enabled? - false + def signup_box_template + 'devise/shared/signup_box' end end diff --git a/app/helpers/routing/projects_helper.rb b/app/helpers/routing/projects_helper.rb index f4732e398f0..9b4aafe49b4 100644 --- a/app/helpers/routing/projects_helper.rb +++ b/app/helpers/routing/projects_helper.rb @@ -43,14 +43,12 @@ module Routing end def work_item_url(entity, *args) - unless Feature.enabled?(:use_iid_in_work_items_path, entity.project.group) - return project_work_items_url(entity.project, entity.id, *args) - end - - options = args.first || {} - options[:iid_path] = true + # TODO: we do not have a route to access group level work items yet. + # That is to be done as part of view group level work item issue: + # see https://gitlab.com/gitlab-org/gitlab/-/work_items/393987 + return unless entity.project.present? - project_work_items_url(entity.project, entity.iid, **options) + project_work_items_url(entity.project, entity.iid, *args) end def merge_request_url(entity, *args) diff --git a/app/helpers/routing/pseudonymization_helper.rb b/app/helpers/routing/pseudonymization_helper.rb index 63e2b377fef..a304d14afb9 100644 --- a/app/helpers/routing/pseudonymization_helper.rb +++ b/app/helpers/routing/pseudonymization_helper.rb @@ -13,6 +13,11 @@ module Routing glm_source glm_content _gl + utm_medium + utm_source + utm_campaign + utm_content + utm_budget ].freeze def initialize(request_object, group, project) diff --git a/app/helpers/safe_format_helper.rb b/app/helpers/safe_format_helper.rb new file mode 100644 index 00000000000..c79e8b50a1a --- /dev/null +++ b/app/helpers/safe_format_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module SafeFormatHelper + # Returns a HTML-safe string where +format+ and +args+ are escaped via + # `html_escape` if they are not marked as HTML-safe. + # + # Argument +format+ must not be marked as HTML-safe via `.html_safe`. + # + # Example: + # safe_format('Some %{open}bold%{close} text.', open: '<strong>'.html_safe, close: '</strong>'.html_safe) + # # => 'Some <strong>bold</strong>' + # safe_format('See %{user_input}', user_input: '<b>bold</b>') + # # => 'See <b>bold</b> + # + def safe_format(format, **args) + raise ArgumentError, 'Argument `format` must not be marked as html_safe!' if format.html_safe? + + format( + html_escape(format), + args.transform_values { |value| html_escape(value) } + ).html_safe + end +end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index ca5436ff019..2187126272d 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -204,7 +204,9 @@ module SearchHelper if search_has_project? hash[:project] = { id: @project.id, name: @project.name } - hash[:project_metadata] = { issues_path: project_issues_path(@project), mr_path: project_merge_requests_path(@project) } + hash[:project_metadata] = { mr_path: project_merge_requests_path(@project) } + hash[:project_metadata][:issues_path] = project_issues_path(@project) if @project.feature_available?(:issues, current_user) + hash[:code_search] = search_scope.nil? hash[:ref] = @ref if @ref && can?(current_user, :read_code, @project) end @@ -244,7 +246,7 @@ module SearchHelper # Autocomplete results for settings pages, for admins def default_autocomplete_admin [ - { category: "Settings", label: _("Admin Section"), url: admin_root_path } + { category: "Jump to", label: _("Admin Area / Dashboard"), url: admin_root_path } ] end @@ -339,7 +341,7 @@ module SearchHelper # Autocomplete results for the current user's projects # rubocop: disable CodeReuse/ActiveRecord def projects_autocomplete(term, limit = 5) - current_user.authorized_projects.order_id_desc.search_by_title(term) + current_user.authorized_projects.order_id_desc.search(term, include_namespace: true) .sorted_by_stars_desc.non_archived.limit(limit).map do |p| { category: "Projects", diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb index 8251e1cba8a..9ef347fff16 100644 --- a/app/helpers/sessions_helper.rb +++ b/app/helpers/sessions_helper.rb @@ -48,4 +48,8 @@ module SessionsHelper # Moved to Gitlab::Utils::Email in 15.9 Gitlab::Utils::Email.obfuscated_email(email) end + + def remember_me_enabled? + Gitlab::CurrentSettings.remember_me_enabled? + end end diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index 27020738515..02a912d0227 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -23,40 +23,130 @@ module SidebarsHelper end end - def project_sidebar_context(project, user, current_ref, ref_type: nil) + def project_sidebar_context(project, user, current_ref, ref_type: nil, **args) context_data = project_sidebar_context_data(project, user, current_ref, ref_type: ref_type) - Sidebars::Projects::Context.new(**context_data) + Sidebars::Projects::Context.new(**context_data, **args) end - def group_sidebar_context(group, user) + def group_sidebar_context(group, user, **args) context_data = group_sidebar_context_data(group, user) - Sidebars::Groups::Context.new(**context_data) + Sidebars::Groups::Context.new(**context_data, **args) end - def super_sidebar_context(user, group:, project:) + def your_work_sidebar_context(user, **args) + context_data = your_work_context_data(user) + + Sidebars::Context.new(**context_data, **args) + end + + def super_sidebar_context(user, group:, project:, panel:, panel_type:) # rubocop:disable Metrics/AbcSize { + current_menu_items: panel.super_sidebar_menu_items, + current_context_header: panel.super_sidebar_context_header, name: user.name, username: user.username, avatar_url: user.avatar_url, - assigned_open_issues_count: user.assigned_open_issues_count, - todos_pending_count: user.todos_pending_count, + has_link_to_profile: current_user_menu?(:profile), + link_to_profile: user_url(user), + logo_url: current_appearance&.header_logo_path, + status: user_status_menu_data(user), + settings: { + has_settings: current_user_menu?(:settings), + profile_path: profile_path, + profile_preferences_path: profile_preferences_path + }, + user_counts: { + assigned_issues: user.assigned_open_issues_count, + assigned_merge_requests: user.assigned_open_merge_requests_count, + review_requested_merge_requests: user.review_requested_open_merge_requests_count, + todos: user.todos_pending_count, + last_update: time_in_milliseconds + }, + can_sign_out: current_user_menu?(:sign_out), + sign_out_link: destroy_user_session_path, issues_dashboard_path: issues_dashboard_path(assignee_username: user.username), - total_merge_requests_count: user_merge_requests_counts[:total], + todos_dashboard_path: dashboard_todos_path, create_new_menu_groups: create_new_menu_groups(group: group, project: project), merge_request_menu: create_merge_request_menu(user), + projects_path: dashboard_projects_path, + groups_path: dashboard_groups_path, support_path: support_url, display_whats_new: display_whats_new?, whats_new_most_recent_release_items_count: whats_new_most_recent_release_items_count, whats_new_version_digest: whats_new_version_digest, show_version_check: show_version_check?, gitlab_version: Gitlab.version_info, - gitlab_version_check: gitlab_version_check + gitlab_version_check: gitlab_version_check, + gitlab_com_but_not_canary: Gitlab.com_but_not_canary?, + gitlab_com_and_canary: Gitlab.com_and_canary?, + canary_toggle_com_url: Gitlab::Saas.canary_toggle_com_url, + current_context: super_sidebar_current_context(project: project, group: group), + context_switcher_links: context_switcher_links, + search: search_data, + pinned_items: user.pinned_nav_items[panel_type] || super_sidebar_default_pins(panel_type), + panel_type: panel_type, + update_pins_url: pins_url, + is_impersonating: impersonating?, + stop_impersonation_path: admin_impersonation_path, + shortcut_links: shortcut_links(user, project: project) } end + def super_sidebar_nav_panel( + nav: nil, project: nil, user: nil, group: nil, current_ref: nil, ref_type: nil, + viewed_user: nil) + context_adds = { route_is_active: method(:active_nav_link?), is_super_sidebar: true } + case nav + when 'project' + context = project_sidebar_context(project, user, current_ref, ref_type: ref_type, **context_adds) + Sidebars::Projects::SuperSidebarPanel.new(context) + when 'group' + context = group_sidebar_context(group, user, **context_adds) + Sidebars::Groups::SuperSidebarPanel.new(context) + when 'profile' + context = Sidebars::Context.new(current_user: user, container: user, **context_adds) + Sidebars::UserSettings::Panel.new(context) + when 'user_profile' + context = Sidebars::Context.new(current_user: user, container: viewed_user, **context_adds) + Sidebars::UserProfile::Panel.new(context) + when 'explore' + Sidebars::Explore::Panel.new(Sidebars::Context.new(current_user: user, container: nil, **context_adds)) + when 'search' + context = Sidebars::Context.new(current_user: user, container: nil, **context_adds) + Sidebars::Search::Panel.new(context) + when 'admin' + Sidebars::Admin::Panel.new(Sidebars::Context.new(current_user: user, container: nil, **context_adds)) + else + context = your_work_sidebar_context(user, **context_adds) + Sidebars::YourWork::Panel.new(context) + end + end + private + def search_data + { + search_path: search_path, + issues_path: issues_dashboard_path, + mr_path: merge_requests_dashboard_path, + autocomplete_path: search_autocomplete_path, + search_context: header_search_context + } + end + + def user_status_menu_data(user) + { + can_update: can?(user, :update_user_status, user), + busy: user.status&.busy?, + customized: user.status&.customized?, + availability: user.status&.availability.to_s, + emoji: user.status&.emoji, + message: user.status&.message_html&.html_safe, + clear_after: user_clear_status_at(user) + } + end + def create_new_menu_groups(group:, project:) new_dropdown_sections = new_dropdown_view_model(group: group, project: project)[:menu_sections] show_headers = new_dropdown_sections.length > 1 @@ -64,11 +154,19 @@ module SidebarsHelper { name: show_headers ? section[:title] : '', items: section[:menu_items].map do |item| - { - text: item[:title], - href: item[:href] - } - end + { + text: item[:title], + href: item[:href].presence, + component: item[:component].presence, + extraAttrs: { + 'data-track-label': item[:id], + 'data-track-action': 'click_link', + 'data-track-property': 'nav_create_menu', + 'data-qa-selector': 'create_menu_item', + 'data-qa-create-menu-item': item[:id] + } + } + end } end end @@ -81,12 +179,26 @@ module SidebarsHelper { text: _('Assigned'), href: merge_requests_dashboard_path(assignee_username: user.username), - count: user_merge_requests_counts[:assigned] + count: user.assigned_open_merge_requests_count, + userCount: 'assigned_merge_requests', + extraAttrs: { + 'data-track-action': 'click_link', + 'data-track-label': 'merge_requests_assigned', + 'data-track-property': 'nav_core_menu', + class: 'dashboard-shortcuts-merge_requests' + } }, { text: _('Review requests'), href: merge_requests_dashboard_path(reviewer_username: user.username), - count: user_merge_requests_counts[:review_requested] + count: user.review_requested_open_merge_requests_count, + userCount: 'review_requested_merge_requests', + extraAttrs: { + 'data-track-action': 'click_link', + 'data-track-label': 'merge_requests_to_review', + 'data-track-property': 'nav_core_menu', + class: 'dashboard-shortcuts-review_requests' + } } ] } @@ -160,6 +272,131 @@ module SidebarsHelper container: group } end + + def your_work_context_data(user) + { + current_user: user, + container: user, + show_security_dashboard: false + } + end + + def super_sidebar_current_context(project: nil, group: nil) + if project&.persisted? + return { + namespace: 'projects', + item: { + id: project.id, + name: project.name, + namespace: project.full_name, + webUrl: project_path(project), + avatarUrl: project.avatar_url + } + } + end + + if group&.persisted? + return { + namespace: 'groups', + item: { + id: group.id, + name: group.name, + namespace: group.full_name, + webUrl: group_path(group), + avatarUrl: group.avatar_url + } + } + end + + {} + end + + def context_switcher_links + links = [ + # We should probably not return "You work" when used is not logged-in + { title: s_('Navigation|Your work'), link: root_path, icon: 'work' }, + { title: s_('Navigation|Explore'), link: explore_root_path, icon: 'compass' } + ] + + # Usually, using current_user.admin? is discouraged because it does not + # check for admin mode, but since here we want to check admin? and admin mode + # separately, we'll have to ignore the cop rule. + # rubocop: disable Cop/UserAdmin + if current_user&.can_admin_all_resources? + links.append( + { title: s_('Navigation|Admin Area'), link: admin_root_path, icon: 'admin' } + ) + end + + if Gitlab::CurrentSettings.admin_mode + if header_link?(:admin_mode) + links.append( + { + title: s_('Navigation|Leave admin mode'), + link: destroy_admin_session_path, + icon: 'lock-open', + data_method: 'post' + } + ) + elsif current_user&.admin? + links.append( + { + title: s_('Navigation|Enter admin mode'), + link: new_admin_session_path, + icon: 'lock' + } + ) + end + end + # rubocop: enable Cop/UserAdmin + + links + end + + def impersonating? + !!session[:impersonator_id] + end + + def shortcut_links(user, project: nil) + shortcut_links = [ + { + title: _('Milestones'), + href: dashboard_milestones_path, + css_class: 'dashboard-shortcuts-milestones' + }, + { + title: _('Snippets'), + href: dashboard_snippets_path, + css_class: 'dashboard-shortcuts-snippets' + }, + { + title: _('Activity'), + href: activity_dashboard_path, + css_class: 'dashboard-shortcuts-activity' + } + ] + + if project&.persisted? && can?(user, :create_issue, project) + shortcut_links << { + title: _('Create a new issue'), + href: new_project_issue_path(project), + css_class: 'shortcuts-new-issue' + } + end + + shortcut_links + end + + def super_sidebar_default_pins(panel_type) + case panel_type + when 'project' + [:project_issue_list, :project_merge_request_list] + when 'group' + [:group_issue_list, :group_merge_request_list] + else + [] + end + end end SidebarsHelper.prepend_mod_with('SidebarsHelper') diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 8558c664977..2f9117a74be 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -45,30 +45,35 @@ module SnippetsHelper def embedded_raw_snippet_button(snippet, blob) return if blob.empty? || blob.binary? || blob.stored_externally? - link_to(external_snippet_icon('doc-code'), - gitlab_raw_snippet_blob_url(snippet, blob.path), - class: 'gl-button btn btn-default', - target: '_blank', - rel: 'noopener noreferrer', - title: 'Open raw') + link_to( + external_snippet_icon('doc-code'), + gitlab_raw_snippet_blob_url(snippet, blob.path), + class: 'gl-button btn btn-default', + target: '_blank', + rel: 'noopener noreferrer', + title: 'Open raw' + ) end def embedded_snippet_download_button(snippet, blob) - link_to(external_snippet_icon('download'), - gitlab_raw_snippet_blob_url(snippet, blob.path, nil, inline: false), - class: 'gl-button btn btn-default', - target: '_blank', - title: 'Download', - rel: 'noopener noreferrer') + link_to( + external_snippet_icon('download'), + gitlab_raw_snippet_blob_url(snippet, blob.path, nil, inline: false), + class: 'gl-button btn btn-default', + target: '_blank', + title: 'Download', + rel: 'noopener noreferrer' + ) end def embedded_copy_snippet_button(blob) return unless blob.rendered_as_text?(ignore_errors: false) - content_tag(:button, - class: 'gl-button btn btn-default copy-to-clipboard-btn', - title: 'Copy snippet contents', - onclick: "copyToClipboard('.blob-content[data-blob-id=\"#{blob.id}\"] > pre')" + content_tag( + :button, + class: 'gl-button btn btn-default copy-to-clipboard-btn', + title: 'Copy snippet contents', + onclick: "copyToClipboard('.blob-content[data-blob-id=\"#{blob.id}\"] > pre')" ) do external_snippet_icon('copy-to-clipboard') end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 4a9596a1347..9038d972f65 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -227,7 +227,7 @@ module SortingHelper options.concat([due_date_option]) if viewing_issues options.concat([popularity_option, label_priority_option]) - options.concat([merged_option]) if viewing_merge_requests + options.concat([merged_option]) if can_sort_by_merged_date?(viewing_merge_requests) options.concat([relative_position_option]) if viewing_issues options.concat([title_option]) @@ -237,6 +237,10 @@ module SortingHelper false end + def can_sort_by_merged_date?(viewing_merge_requests) + viewing_merge_requests && %w[all merged].include?(params[:state]) + end + def due_date_option { value: sort_value_due_date, text: sort_title_due_date, href: page_filter_path(sort: sort_value_due_date) } end diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index c38d69df8e4..c8dd7f59b43 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -151,8 +151,6 @@ module SubmoduleHelper if uri.scheme.in?(VALID_SUBMODULE_PROTOCOLS) uri.to_s - else - nil end rescue URI::InvalidURIError nil diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb index 3e5f63796b2..d7ca76f6a8a 100644 --- a/app/helpers/system_note_helper.rb +++ b/app/helpers/system_note_helper.rb @@ -2,13 +2,13 @@ module SystemNoteHelper ICON_NAMES_BY_ACTION = { - 'approved' => 'approval', + 'approved' => 'check', 'unapproved' => 'unapproval', 'cherry_pick' => 'cherry-pick-commit', 'commit' => 'commit', 'description' => 'pencil', - 'merge' => 'git-merge', - 'merged' => 'git-merge', + 'merged' => 'merge', + 'merge' => 'merge', 'opened' => 'issues', 'closed' => 'issue-close', 'time_tracking' => 'timer', @@ -42,8 +42,6 @@ module SystemNoteHelper 'severity' => 'information-o', 'cloned' => 'documents', 'issue_type' => 'pencil', - 'attention_requested' => 'user', - 'attention_request_removed' => 'user', 'contact' => 'users', 'timeline_event' => 'clock', 'relate_to_child' => 'link', @@ -53,7 +51,13 @@ module SystemNoteHelper }.freeze def system_note_icon_name(note) - ICON_NAMES_BY_ACTION[note.system_note_metadata&.action] + if note.system_note_metadata&.action == 'closed' && note.for_merge_request? + 'merge-request-close' + elsif note.system_note_metadata&.action == 'merge' && note.for_merge_request? + 'mr-system-note-empty' + else + ICON_NAMES_BY_ACTION[note.system_note_metadata&.action] + end end def icon_for_system_note(note) diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 4a9dd30a5a2..9b0810f3d17 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -232,13 +232,15 @@ module TodosHelper '' end + due_date = + if is_due_today + _("today") + else + l(todo.target.due_date, format: Date::DATE_FORMATS[:medium]) + end + content = content_tag(:span, class: css_class) do - format(s_("Todos|Due %{due_date}"), due_date: if is_due_today - _("today") - else - l(todo.target.due_date, - format: Date::DATE_FORMATS[:medium]) - end) + format(s_("Todos|Due %{due_date}"), due_date: due_date) end "#{content} ·".html_safe diff --git a/app/helpers/users/callouts_helper.rb b/app/helpers/users/callouts_helper.rb index 2b8368dd29f..af3ac495164 100644 --- a/app/helpers/users/callouts_helper.rb +++ b/app/helpers/users/callouts_helper.rb @@ -11,9 +11,11 @@ module Users UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout' SECURITY_NEWSLETTER_CALLOUT = 'security_newsletter_callout' MERGE_REQUEST_SETTINGS_MOVED_CALLOUT = 'merge_request_settings_moved_callout' + PAGES_MOVED_CALLOUT = 'pages_moved_callout' REGISTRATION_ENABLED_CALLOUT_ALLOWED_CONTROLLER_PATHS = [/^root/, /^dashboard\S*/, /^admin\S*/].freeze WEB_HOOK_DISABLED = 'web_hook_disabled' ULTIMATE_FEATURE_REMOVAL_BANNER = 'ultimate_feature_removal_banner' + BRANCH_RULES_INFO_CALLOUT = 'branch_rules_info_callout' def show_gke_cluster_integration_callout?(project) active_nav_link?(controller: sidebar_operations_paths) && @@ -59,42 +61,47 @@ module Users !user_dismissed?(SECURITY_NEWSLETTER_CALLOUT) end - def web_hook_disabled_dismissed?(project) - return false unless project - - last_failure = Gitlab::Redis::SharedState.with do |redis| - key = "web_hooks:last_failure:project-#{project.id}" - redis.get(key) - end + def web_hook_disabled_dismissed?(object) + return false unless object.is_a?(::WebHooks::HasWebHooks) - last_failure = DateTime.parse(last_failure) if last_failure - - user_dismissed?(WEB_HOOK_DISABLED, last_failure, project: project) + user_dismissed?(WEB_HOOK_DISABLED, object.last_webhook_failure, object: object) end def show_merge_request_settings_callout?(project) !user_dismissed?(MERGE_REQUEST_SETTINGS_MOVED_CALLOUT) && project.merge_requests_enabled? end + def show_pages_menu_callout? + !user_dismissed?(PAGES_MOVED_CALLOUT) + end + + def show_branch_rules_info? + !user_dismissed?(BRANCH_RULES_INFO_CALLOUT) + end + def ultimate_feature_removal_banner_dismissed?(project) return false unless project - user_dismissed?(ULTIMATE_FEATURE_REMOVAL_BANNER, project: project) + user_dismissed?(ULTIMATE_FEATURE_REMOVAL_BANNER, object: project) end private - def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil, project: nil) + def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil, object: nil) return false unless current_user query = { feature_name: feature_name, ignore_dismissal_earlier_than: ignore_dismissal_earlier_than } - if project - current_user.dismissed_callout_for_project?(project: project, **query) + if object + dismissed_callout?(object, query) else current_user.dismissed_callout?(**query) end end + + def dismissed_callout?(object, query) + current_user.dismissed_callout_for_project?(project: object, **query) + end end end diff --git a/app/helpers/users/group_callouts_helper.rb b/app/helpers/users/group_callouts_helper.rb index 0aa4eb89499..92cf41400e7 100644 --- a/app/helpers/users/group_callouts_helper.rb +++ b/app/helpers/users/group_callouts_helper.rb @@ -17,9 +17,11 @@ module Users def user_dismissed_for_group(feature_name, group, ignore_dismissal_earlier_than = nil) return false unless current_user - current_user.dismissed_callout_for_group?(feature_name: feature_name, - group: group, - ignore_dismissal_earlier_than: ignore_dismissal_earlier_than) + current_user.dismissed_callout_for_group?( + feature_name: feature_name, + group: group, + ignore_dismissal_earlier_than: ignore_dismissal_earlier_than + ) end def just_created? diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 62b9eb2b506..60230d58e30 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -8,10 +8,15 @@ module UsersHelper } end + def user_clear_status_at(user) + # The user.status can be nil when the user has no status, so we need to protect against that case. + # iso8601 is the official RFC supported format for frontend parsing of date: + # https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date + user.status&.clear_status_at&.to_s(:iso8601) + end + def user_link(user) - link_to(user.name, user_path(user), - title: user.email, - class: 'has-tooltip commit-committer-link') + link_to(user.name, user_path(user), title: user.email, class: 'has-tooltip commit-committer-link') end def user_email_help_text(user) @@ -53,12 +58,21 @@ module UsersHelper end # Used to preload when you are rendering many projects and checking access - # - # rubocop: disable CodeReuse/ActiveRecord: `projects` can be array which also responds to pluck def load_max_project_member_accesses(projects) - current_user&.max_member_access_for_project_ids(projects.pluck(:id)) + # There are two different request store paradigms for max member access and + # we need to preload both of them. One is keyed User the other is keyed by + # Project. See https://gitlab.com/gitlab-org/gitlab/-/issues/396822 + + # rubocop: disable CodeReuse/ActiveRecord: `projects` can be array which also responds to pluck + project_ids = projects.pluck(:id) + # rubocop: enable CodeReuse/ActiveRecord + + Preloaders::UserMaxAccessLevelInProjectsPreloader + .new(project_ids, current_user) + .execute + + current_user&.max_member_access_for_project_ids(project_ids) end - # rubocop: enable CodeReuse/ActiveRecord def max_project_member_access(project) current_user&.max_member_access_for_project(project.id) || Gitlab::Access::NO_ACCESS @@ -79,9 +93,9 @@ module UsersHelper return unless user.status content_tag :span, - class: 'user-status-emoji has-tooltip', - title: user.status.message_html, - data: { html: true, placement: 'top' } do + class: 'user-status-emoji has-tooltip', + title: user.status.message_html, + data: { html: true, placement: 'top' } do emoji_icon user.status.emoji end end @@ -168,6 +182,16 @@ module UsersHelper user.public_email.present? end + def user_profile_tabs_app_data(user) + { + followees: user.followees.count, + followers: user.followers.count, + user_calendar_path: user_calendar_path(user, :json), + utc_offset: local_timezone_instance(user.timezone).now.utc_offset, + user_id: user.id + } + end + private def admin_users_paths @@ -211,14 +235,6 @@ module UsersHelper tabs end - def trials_link_url - 'https://about.gitlab.com/free-trial/' - end - - def trials_allowed?(user) - false - end - def get_current_user_menu_items items = [] @@ -229,7 +245,6 @@ module UsersHelper items << :help items << :profile if can?(current_user, :read_user, current_user) items << :settings if can?(current_user, :update_user, current_user) - items << :start_trial if trials_allowed?(current_user) items end diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb index 1fec0a916b8..dc8ef4e44be 100644 --- a/app/helpers/version_check_helper.rb +++ b/app/helpers/version_check_helper.rb @@ -22,25 +22,12 @@ module VersionCheckHelper end def link_to_version + link = link_to(Gitlab::Source.ref, Gitlab::Source.release_url) + if Gitlab.pre_release? - commit_link = link_to(Gitlab.revision, source_host_url + namespace_project_commits_path(source_code_group, source_code_project, Gitlab.revision)) - [Gitlab::VERSION, content_tag(:small, commit_link)].join(' ').html_safe + [Gitlab::VERSION, content_tag(:small, link)].join(' ').html_safe else - link_to Gitlab::VERSION, source_host_url + namespace_project_tag_path(source_code_group, source_code_project, "v#{Gitlab::VERSION}") + link end end - - def source_host_url - Gitlab::Saas.com_url - end - - def source_code_group - 'gitlab-org' - end - - def source_code_project - 'gitlab-foss' - end end - -VersionCheckHelper.prepend_mod diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 5ed341ee5e5..68b15f7e042 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -22,7 +22,7 @@ module VisibilityLevelHelper when Project project_visibility_level_description(level) when Group - group_visibility_level_description(level) + group_visibility_level_description(level, form_model) end end @@ -44,9 +44,8 @@ module VisibilityLevelHelper Gitlab::CurrentSettings.restricted_visibility_levels || [] end - delegate :default_project_visibility, - :default_group_visibility, - to: :'Gitlab::CurrentSettings.current_application_settings' + delegate :default_project_visibility, :default_group_visibility, + to: :'Gitlab::CurrentSettings.current_application_settings' def disallowed_visibility_level?(form_model, level) return false unless form_model.respond_to?(:visibility_level_allowed?) @@ -126,22 +125,39 @@ module VisibilityLevelHelper def project_visibility_level_description(level) case level when Gitlab::VisibilityLevel::PRIVATE - _("Project access must be granted explicitly to each user. If this project is part of a group, access is granted to members of the group.") + s_("VisibilityLevel|Project access must be granted explicitly to each user. If this project is part of a group, access is granted to members of the group.") when Gitlab::VisibilityLevel::INTERNAL - _("The project can be accessed by any logged in user except external users.") + s_("VisibilityLevel|The project can be accessed by any logged in user except external users.") when Gitlab::VisibilityLevel::PUBLIC - _("The project can be accessed without any authentication.") + s_("VisibilityLevel|The project can be accessed without any authentication.") end end - def group_visibility_level_description(level) + def show_updated_public_description_for_setting(group) + group && !group.new_record? && Gitlab::CurrentSettings.current_application_settings.try(:should_check_namespace_plan?) + end + + def group_visibility_level_description(level, group = nil) case level when Gitlab::VisibilityLevel::PRIVATE - _("The group and its projects can only be viewed by members.") + s_("VisibilityLevel|The group and its projects can only be viewed by members.") when Gitlab::VisibilityLevel::INTERNAL - _("The group and any internal projects can be viewed by any logged in user except external users.") + s_("VisibilityLevel|The group and any internal projects can be viewed by any logged in user except external users.") when Gitlab::VisibilityLevel::PUBLIC - _("The group and any public projects can be viewed without any authentication.") + unless show_updated_public_description_for_setting(group) + return s_('VisibilityLevel|The group and any public projects can be viewed without any authentication.') + end + + Kernel.format( + s_( + 'VisibilityLevel|The group, any public projects, and any of their members, issues, and merge requests can be viewed without authentication. ' \ + 'Public groups and projects will be indexed by search engines. ' \ + 'Read more about %{free_user_limit_doc_link_start}free user limits%{link_end}, ' \ + 'or %{group_billings_link_start}upgrade to a paid tier%{link_end}.'), + free_user_limit_doc_link_start: "<a href='#{help_page_path('user/free_user_limit')}' target='_blank' rel='noopener noreferrer'>".html_safe, + group_billings_link_start: "<a href='#{group_billings_path(group)}' target='_blank' rel='noopener noreferrer'>".html_safe, + link_end: "</a>".html_safe + ).html_safe end end diff --git a/app/helpers/web_hooks/web_hooks_helper.rb b/app/helpers/web_hooks/web_hooks_helper.rb index 514db6ba8a2..ad792f761f8 100644 --- a/app/helpers/web_hooks/web_hooks_helper.rb +++ b/app/helpers/web_hooks/web_hooks_helper.rb @@ -4,19 +4,31 @@ module WebHooks module WebHooksHelper def show_project_hook_failed_callout?(project:) return false if project_hook_page? + + show_hook_failed_callout?(project) + end + + private + + def show_hook_failed_callout?(object) return false unless current_user - return false unless Ability.allowed?(current_user, :read_web_hooks, project) + + return false unless can_access_web_hooks?(object) # Assumes include of Users::CalloutsHelper - return false if web_hook_disabled_dismissed?(project) + return false if web_hook_disabled_dismissed?(object) - project.fetch_web_hook_failure + object.fetch_web_hook_failure end - private - def project_hook_page? current_controller?('projects/hooks') || current_controller?('projects/hook_logs') end + + def can_access_web_hooks?(object) + Ability.allowed?(current_user, :admin_project, object) + end end end + +WebHooks::WebHooksHelper.prepend_mod diff --git a/app/helpers/work_items_helper.rb b/app/helpers/work_items_helper.rb index efa9a2bd463..9036c7c8347 100644 --- a/app/helpers/work_items_helper.rb +++ b/app/helpers/work_items_helper.rb @@ -6,7 +6,9 @@ module WorkItemsHelper full_path: project.full_path, issues_list_path: project_issues_path(project), register_path: new_user_registration_path(redirect_to_referer: 'yes'), - sign_in_path: new_session_path(:user, redirect_to_referer: 'yes') + sign_in_path: new_session_path(:user, redirect_to_referer: 'yes'), + new_comment_template_path: profile_comment_templates_path, + report_abuse_path: add_category_abuse_reports_path } end end |