From 859a6fb938bb9ee2a317c46dfa4fcc1af49608f0 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 18 Feb 2021 10:34:06 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-9-stable-ee --- app/helpers/analytics/cycle_analytics_helper.rb | 11 + app/helpers/analytics/unique_visits_helper.rb | 1 - app/helpers/application_helper.rb | 2 +- app/helpers/application_settings_helper.rb | 24 +- app/helpers/auth_helper.rb | 2 +- app/helpers/blob_helper.rb | 40 +-- app/helpers/boards_helper.rb | 20 +- app/helpers/ci/jobs_helper.rb | 1 - app/helpers/commits_helper.rb | 54 ++-- app/helpers/container_registry_helper.rb | 2 +- app/helpers/diff_helper.rb | 28 +- app/helpers/enable_search_settings_helper.rb | 9 + app/helpers/events_helper.rb | 4 +- app/helpers/external_link_helper.rb | 2 +- app/helpers/groups/group_members_helper.rb | 14 +- app/helpers/groups_helper.rb | 23 ++ app/helpers/in_product_marketing_helper.rb | 350 +++++++++++++++++++++ app/helpers/invite_members_helper.rb | 10 +- .../issuables_description_templates_helper.rb | 47 +++ app/helpers/issuables_helper.rb | 73 +---- app/helpers/jira_connect_helper.rb | 10 +- app/helpers/labels_helper.rb | 42 +-- app/helpers/learn_gitlab_helper.rb | 60 ++++ app/helpers/merge_requests_helper.rb | 6 - app/helpers/nav_helper.rb | 2 +- app/helpers/notes_helper.rb | 6 +- app/helpers/notifications_helper.rb | 9 + app/helpers/notify_helper.rb | 26 ++ app/helpers/operations_helper.rb | 2 +- app/helpers/projects/alert_management_helper.rb | 7 +- app/helpers/projects/project_members_helper.rb | 26 ++ app/helpers/projects_helper.rb | 37 ++- app/helpers/search_helper.rb | 21 ++ app/helpers/services_helper.rb | 7 + app/helpers/sorting_helper.rb | 21 +- app/helpers/sorting_titles_values_helper.rb | 8 - app/helpers/stat_anchors_helper.rb | 4 +- app/helpers/tree_helper.rb | 45 --- app/helpers/user_callouts_helper.rb | 5 + app/helpers/users_helper.rb | 23 +- 40 files changed, 799 insertions(+), 285 deletions(-) create mode 100644 app/helpers/analytics/cycle_analytics_helper.rb create mode 100644 app/helpers/enable_search_settings_helper.rb create mode 100644 app/helpers/in_product_marketing_helper.rb create mode 100644 app/helpers/issuables_description_templates_helper.rb create mode 100644 app/helpers/learn_gitlab_helper.rb (limited to 'app/helpers') diff --git a/app/helpers/analytics/cycle_analytics_helper.rb b/app/helpers/analytics/cycle_analytics_helper.rb new file mode 100644 index 00000000000..c43ac545bf8 --- /dev/null +++ b/app/helpers/analytics/cycle_analytics_helper.rb @@ -0,0 +1,11 @@ +# 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 + end +end diff --git a/app/helpers/analytics/unique_visits_helper.rb b/app/helpers/analytics/unique_visits_helper.rb index 4c709b2ed23..337a5dc9536 100644 --- a/app/helpers/analytics/unique_visits_helper.rb +++ b/app/helpers/analytics/unique_visits_helper.rb @@ -14,7 +14,6 @@ module Analytics end def track_visit(target_id) - return unless Feature.enabled?(:track_unique_visits, default_enabled: true) return unless visitor_id Gitlab::Analytics::UniqueVisits.new.track_visit(visitor_id, target_id) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 2a1652cf2ba..8268ab1ad56 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -280,7 +280,7 @@ module ApplicationHelper def page_class class_names = [] - class_names << 'issue-boards-page' if current_controller?(:boards) + class_names << 'issue-boards-page gl-overflow-hidden' if current_controller?(:boards) class_names << 'environment-logs-page' if current_controller?(:logs) class_names << 'with-performance-bar' if performance_bar_enabled? class_names << system_message_class diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index ed30adfabf0..30ae535b06f 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -26,6 +26,16 @@ module ApplicationSettingsHelper end end + def kroki_available_formats + ApplicationSetting.kroki_formats_attributes.map do |key, value| + { + name: "kroki_formats_#{key}", + label: value[:label], + value: @application_setting.kroki_formats[key] || false + } + end + end + def storage_weights ApplicationSetting.repository_storages_weighted_attributes.map do |attribute| storage = attribute.to_s.delete_prefix('repository_storages_weighted_') @@ -181,7 +191,7 @@ module ApplicationSettingsHelper :asset_proxy_enabled, :asset_proxy_secret_key, :asset_proxy_url, - :asset_proxy_whitelist, + :asset_proxy_allowlist, :static_objects_external_storage_auth_token, :static_objects_external_storage_url, :authorized_keys_enabled, @@ -259,6 +269,7 @@ module ApplicationSettingsHelper :personal_access_token_prefix, :kroki_enabled, :kroki_url, + :kroki_formats, :plantuml_enabled, :plantuml_url, :polling_interval_multiplier, @@ -328,6 +339,8 @@ module ApplicationSettingsHelper :email_restrictions_enabled, :email_restrictions, :issues_create_limit, + :notes_create_limit, + :notes_create_limit_allowlist_raw, :raw_blob_request_limit, :project_import_limit, :project_export_limit, @@ -337,7 +350,10 @@ module ApplicationSettingsHelper :group_download_export_limit, :wiki_page_max_content_bytes, :container_registry_delete_tags_service_timeout, - :rate_limiting_response_text + :rate_limiting_response_text, + :container_registry_expiration_policies_worker_capacity, + :container_registry_cleanup_tags_service_max_list_size, + :keep_latest_artifact ] end @@ -353,9 +369,11 @@ module ApplicationSettingsHelper ] end + # ok to remove in REST API v5 def deprecated_attributes [ - :admin_notification_email # ok to remove in REST API v5 + :admin_notification_email, + :asset_proxy_whitelist ] end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 0b79d4c36a1..24c1d224c89 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module AuthHelper - PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq salesforce atlassian_oauth2).freeze + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq salesforce atlassian_oauth2 openid_connect).freeze LDAP_PROVIDER = /\Aldap/.freeze def ldap_enabled? diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index bca53dfb88a..28a947a6ca1 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -64,7 +64,7 @@ module BlobHelper def edit_blob_button(project = @project, ref = @ref, path = @path, options = {}) return unless blob = readable_blob(options, path, project, ref) - common_classes = "btn btn-primary js-edit-blob gl-mr-3 #{options[:extra_class]}" + common_classes = "btn gl-button btn-confirm js-edit-blob gl-ml-3 #{options[:extra_class]}" data = { track_event: 'click_edit', track_label: 'Edit' } if Feature.enabled?(:web_ide_primary_edit, project.group) @@ -84,7 +84,7 @@ module BlobHelper def ide_edit_button(project = @project, ref = @ref, path = @path, blob:) return unless blob - common_classes = 'btn btn-primary ide-edit-button gl-mr-3' + common_classes = 'btn gl-button btn-confirm ide-edit-button gl-ml-3' data = { track_event: 'click_edit_ide', track_label: 'Web IDE' } unless Feature.enabled?(:web_ide_primary_edit, project.group) @@ -105,7 +105,7 @@ module BlobHelper return unless current_user return unless blob - common_classes = "btn btn-#{btn_class}" + common_classes = "btn gl-button btn-default btn-#{btn_class}" base_button = button_tag(label, class: "#{common_classes} disabled", disabled: true) if !on_top_of_branch?(project, ref) @@ -194,40 +194,28 @@ module BlobHelper @ref_project ||= @target_project || @project end - def template_dropdown_names(items) - grouped = items.group_by(&:category) - categories = grouped.keys - - categories.each_with_object({}) do |category, hash| - hash[category] = grouped[category].map do |item| - { name: item.name, id: item.key } - end - end - end - private :template_dropdown_names - def licenses_for_select(project) - @licenses_for_select ||= template_dropdown_names(TemplateFinder.build(:licenses, project).execute) + @licenses_for_select ||= TemplateFinder.all_template_names(project, :licenses) end def gitignore_names(project) - @gitignore_names ||= template_dropdown_names(TemplateFinder.build(:gitignores, project).execute) + @gitignore_names ||= TemplateFinder.all_template_names(project, :gitignores) end def gitlab_ci_ymls(project) - @gitlab_ci_ymls ||= template_dropdown_names(TemplateFinder.build(:gitlab_ci_ymls, project).execute) + @gitlab_ci_ymls ||= TemplateFinder.all_template_names(project, :gitlab_ci_ymls) end def gitlab_ci_syntax_ymls(project) - @gitlab_ci_syntax_ymls ||= template_dropdown_names(TemplateFinder.build(:gitlab_ci_syntax_ymls, project).execute) + @gitlab_ci_syntax_ymls ||= TemplateFinder.all_template_names(project, :gitlab_ci_syntax_ymls) end def metrics_dashboard_ymls(project) - @metrics_dashboard_ymls ||= template_dropdown_names(TemplateFinder.build(:metrics_dashboard_ymls, project).execute) + @metrics_dashboard_ymls ||= TemplateFinder.all_template_names(project, :metrics_dashboard_ymls) end def dockerfile_names(project) - @dockerfile_names ||= template_dropdown_names(TemplateFinder.build(:dockerfiles, project).execute) + @dockerfile_names ||= TemplateFinder.all_template_names(project, :dockerfiles) end def blob_editor_paths(project) @@ -241,13 +229,13 @@ module BlobHelper end def copy_file_path_button(file_path) - clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent', title: _('Copy file path')) + clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'gl-button btn btn-default-tertiary btn-icon btn-sm', title: _('Copy file path')) end def copy_blob_source_button(blob) return unless blob.rendered_as_text?(ignore_errors: false) - clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}'] > pre", class: "btn btn-sm js-copy-blob-source-btn", title: _("Copy file contents")) + clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}'] > pre", class: "btn gl-button btn-default btn-icon js-copy-blob-source-btn", title: _("Copy file contents")) end def open_raw_blob_button(blob) @@ -257,7 +245,7 @@ module BlobHelper title = _('Open raw') link_to sprite_icon('doc-code'), external_storage_url_or_path(blob_raw_path), - class: 'btn btn-sm has-tooltip', + class: 'btn gl-button btn-default btn-icon has-tooltip', target: '_blank', rel: 'noopener noreferrer', aria: { label: title }, @@ -272,7 +260,7 @@ module BlobHelper link_to sprite_icon('download'), external_storage_url_or_path(blob_raw_path(inline: false)), download: @path, - class: 'btn btn-sm has-tooltip', + class: 'btn gl-button btn-default btn-icon has-tooltip', target: '_blank', rel: 'noopener noreferrer', aria: { label: title }, @@ -373,7 +361,7 @@ module BlobHelper end def edit_link_tag(link_text, edit_path, common_classes, data) - link_to link_text, edit_path, class: "#{common_classes} btn-sm", data: data + link_to link_text, edit_path, class: "#{common_classes}", data: data end def edit_button_tag(blob, common_classes, text, edit_path, project, ref, data) diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index c827fb4dd95..73e7476b55d 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -21,7 +21,8 @@ module BoardsHelper group_id: @group&.id, labels_filter_base_path: build_issue_link_base, labels_fetch_path: labels_fetch_path, - labels_manage_path: labels_manage_path + labels_manage_path: labels_manage_path, + board_type: board.to_type } end @@ -99,23 +100,6 @@ module BoardsHelper } end - def board_sidebar_user_data - dropdown_options = assignees_dropdown_options('issue') - - { - toggle: 'dropdown', - field_name: 'issue[assignee_ids][]', - first_user: current_user&.username, - current_user: 'true', - project_id: @project&.id, - group_id: @group&.id, - null_user: 'true', - multi_select: 'true', - 'dropdown-header': dropdown_options[:data][:'dropdown-header'], - 'max-select': dropdown_options[:data][:'max-select'] - } - end - def boards_link_text if current_board_parent.multiple_issue_boards_available? s_("IssueBoards|Boards") diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb index d47f6195c61..ec17eccf693 100644 --- a/app/helpers/ci/jobs_helper.rb +++ b/app/helpers/ci/jobs_helper.rb @@ -8,7 +8,6 @@ module Ci "project_path" => @project.full_path, "artifact_help_url" => help_page_path('user/gitlab_com/index.html', anchor: 'gitlab-cicd'), "deployment_help_url" => help_page_path('user/project/clusters/index.html', anchor: 'troubleshooting'), - "runner_help_url" => help_page_path('ci/runners/README.html', anchor: 'set-maximum-job-timeout-for-a-runner'), "runner_settings_url" => project_runners_path(@build.project, anchor: 'js-runners-settings'), "variables_settings_url" => project_variables_path(@build.project, anchor: 'js-cicd-variables-settings'), "page_path" => project_job_path(@project, @build), diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index e6e2b5b128b..f5c75d62097 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -105,31 +105,35 @@ module CommitsHelper tooltip = _("Browse Directory") end - link_to url, class: "btn btn-default has-tooltip", title: tooltip, data: { container: "body" } do + link_to url, class: "btn gl-button btn-default has-tooltip", title: tooltip, data: { container: "body" } do sprite_icon('folder-open') end end - def revert_commit_link(commit, continue_to_path, btn_class: nil, pajamas: false) + def revert_commit_link return unless current_user - action = 'revert' - - if pajamas && can_collaborate_with_project?(@project) - tag(:div, data: { display_text: action.capitalize }, class: "js-revert-commit-trigger") - else - commit_action_link(action, commit, continue_to_path, btn_class: btn_class, has_tooltip: false) - end + tag(:div, data: { display_text: 'Revert' }, class: "js-revert-commit-trigger") end - def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) - commit_action_link('cherry-pick', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip) + def cherry_pick_commit_link + return unless current_user + + tag(:div, data: { display_text: 'Cherry-pick' }, class: "js-cherry-pick-commit-trigger") end def commit_signature_badge_classes(additional_classes) %w(btn gpg-status-box) + Array(additional_classes) end + def conditionally_paginate_diff_files(diffs, paginate:, per: Projects::CommitController::COMMIT_DIFFS_PER_PAGE) + if paginate && Feature.enabled?(:paginate_commit_view, @project, type: :development) + Kaminari.paginate_array(diffs.diff_files.to_a).page(params[:page]).per(per) + else + diffs.diff_files + end + end + protected # Private: Returns a link to a person. If the person has a matching user and @@ -143,7 +147,7 @@ module CommitsHelper def commit_person_link(commit, options = {}) user = commit.public_send(options[:source]) # rubocop:disable GitlabSecurity/PublicSend - source_name = clean(commit.public_send(:"#{options[:source]}_name")) # rubocop:disable GitlabSecurity/PublicSend + source_name = clean(commit.public_send(:"#{options[:source]}_name")) # rubocop:disable GitlabSecurity/PublicSend source_email = clean(commit.public_send(:"#{options[:source]}_email")) # rubocop:disable GitlabSecurity/PublicSend person_name = user.try(:name) || source_name @@ -166,33 +170,11 @@ module CommitsHelper end end - def commit_action_link(action, commit, continue_to_path, btn_class: nil, has_tooltip: true) - return unless current_user - - tooltip = "#{action.capitalize} this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip - btn_class = "btn btn-#{btn_class}" unless btn_class.nil? - - if can_collaborate_with_project?(@project) - link_to action.capitalize, "#modal-#{action}-commit", 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" - elsif can?(current_user, :fork_project, @project) - continue_params = { - to: continue_to_path, - notice: "#{edit_in_new_fork_notice} Try to #{action} this commit again.", - notice_now: edit_in_new_fork_notice_now - } - fork_path = project_forks_path(@project, - namespace_key: current_user.namespace.id, - continue: continue_params) - - link_to action.capitalize, fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) - end - end - def view_file_button(commit_sha, diff_new_path, project, replaced: false) path = project_blob_path(project, tree_join(commit_sha, diff_new_path)) title = replaced ? _('View replaced file @ ') : _('View file @ ') - link_to(path, class: 'btn') do + link_to(path, class: 'btn gl-button btn-default') do raw(title) + content_tag(:span, truncate_sha(commit_sha), class: 'commit-sha') end end @@ -203,7 +185,7 @@ module CommitsHelper external_url = environment.external_url_for(diff_new_path, commit_sha) return unless external_url - link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do + link_to(external_url, class: 'btn gl-button btn-default btn-file-option has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do sprite_icon('external-link') end end diff --git a/app/helpers/container_registry_helper.rb b/app/helpers/container_registry_helper.rb index 0efc8c50d58..1b77b639ce1 100644 --- a/app/helpers/container_registry_helper.rb +++ b/app/helpers/container_registry_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module ContainerRegistryHelper - def limit_delete_tags_service? + def container_registry_expiration_policies_throttling? Feature.enabled?(:container_registry_expiration_policies_throttling) && ContainerRegistry::Client.supports_tag_delete? end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 69a2efebb1f..233a8260036 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -133,7 +133,7 @@ module DiffHelper ].join('').html_safe tooltip = _('Compare submodule commit revisions') - link = content_tag(:span, link_to(link_text, compare_url, class: 'btn has-tooltip', title: tooltip), class: 'submodule-compare') + link = content_tag(:span, link_to(link_text, compare_url, class: 'btn gl-button has-tooltip', title: tooltip), class: 'submodule-compare') end link @@ -203,6 +203,26 @@ module DiffHelper set_secure_cookie(:diff_view, params.delete(:view), type: CookiesHelper::COOKIE_TYPE_PERMANENT) if params[:view].present? end + def collapsed_diff_url(diff_file) + url_for( + safe_params.merge( + action: :diff_for_path, + old_path: diff_file.old_path, + new_path: diff_file.new_path, + file_identifier: diff_file.file_identifier + ) + ) + end + + # As the fork suggestion button is identical every time, we cache it for a full page load + def render_fork_suggestion + return unless current_user + + strong_memoize(:fork_suggestion) do + render partial: "projects/fork_suggestion" + end + end + private def diff_btn(title, name, selected) @@ -212,7 +232,7 @@ module DiffHelper # Always use HTML to handle case where JSON diff rendered this button params_copy.delete(:format) - link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn'), data: { view_type: name } do + link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn gl-button active' : 'btn gl-button'), data: { view_type: name } do title end end @@ -241,7 +261,7 @@ module DiffHelper end def toggle_whitespace_link(url, options) - options[:class] = [*options[:class], 'btn btn-default'].join(' ') + options[:class] = [*options[:class], 'btn gl-button btn-default'].join(' ') link_to "#{hide_whitespace? ? 'Show' : 'Hide'} whitespace changes", url, class: options[:class] end @@ -254,7 +274,7 @@ module DiffHelper end def code_navigation_path(diffs) - Gitlab::CodeNavigationPath.new(merge_request.project, diffs.diff_refs&.head_sha) + Gitlab::CodeNavigationPath.new(merge_request.project, merge_request.diff_head_sha) end def conflicts diff --git a/app/helpers/enable_search_settings_helper.rb b/app/helpers/enable_search_settings_helper.rb new file mode 100644 index 00000000000..aa92a1b0b1e --- /dev/null +++ b/app/helpers/enable_search_settings_helper.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module EnableSearchSettingsHelper + def enable_search_settings(locals: {}) + content_for :before_content do + render "shared/search_settings", locals + end + end +end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index e6603237676..52b8ac915f1 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -178,8 +178,8 @@ module EventsHelper def event_note_target_url(event) if event.commit_note? project_commit_url(event.project, event.note_target, anchor: dom_id(event.target)) - elsif event.project_snippet_note? - project_snippet_url(event.project, event.note_target, anchor: dom_id(event.target)) + elsif event.snippet_note? + gitlab_snippet_url(event.note_target, anchor: dom_id(event.target)) elsif event.issue_note? project_issue_url(event.project, id: event.note_target, anchor: dom_id(event.target)) elsif event.merge_request_note? diff --git a/app/helpers/external_link_helper.rb b/app/helpers/external_link_helper.rb index bf47087543f..058302d1ed8 100644 --- a/app/helpers/external_link_helper.rb +++ b/app/helpers/external_link_helper.rb @@ -3,7 +3,7 @@ module ExternalLinkHelper def external_link(body, url, options = {}) link_to url, { target: '_blank', rel: 'noopener noreferrer' }.merge(options) do - "#{body} #{sprite_icon('external-link')}".html_safe + "#{body}#{sprite_icon('external-link', css_class: 'gl-ml-1')}".html_safe end end end diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb index a4159ed6b19..3e7d6febabf 100644 --- a/app/helpers/groups/group_members_helper.rb +++ b/app/helpers/groups/group_members_helper.rb @@ -13,12 +13,12 @@ module Groups::GroupMembersHelper render 'shared/members/invite_member', submit_url: group_group_members_path(group), access_levels: group.access_level_roles, default_access_level: default_access_level end - def linked_groups_data_json(group_links) - GroupGroupLinkSerializer.new.represent(group_links, { current_user: current_user }).to_json + def group_group_links_data_json(group_links) + GroupLink::GroupGroupLinkSerializer.new.represent(group_links, { current_user: current_user }).to_json end def members_data_json(group, members) - MemberSerializer.new.represent(members, { current_user: current_user, group: group }).to_json + MemberSerializer.new.represent(members, { current_user: current_user, group: group, source: group }).to_json end # Overridden in `ee/app/helpers/ee/groups/group_members_helper.rb` @@ -26,16 +26,16 @@ module Groups::GroupMembersHelper { members: members_data_json(group, members), member_path: group_group_member_path(group, ':id'), - group_id: group.id, + source_id: group.id, can_manage_members: can?(current_user, :admin_group_member, group).to_s } end - def linked_groups_list_data_attributes(group) + def group_group_links_list_data_attributes(group) { - members: linked_groups_data_json(group.shared_with_group_links), + members: group_group_links_data_json(group.shared_with_group_links), member_path: group_group_link_path(group, ':id'), - group_id: group.id + source_id: group.id } end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 133d9d21a14..eeeffb7b3ae 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -62,6 +62,14 @@ module GroupsHelper can?(current_user, :set_emails_disabled, group) && !group.parent&.emails_disabled? end + def group_open_issues_count(group) + if Feature.enabled?(:cached_sidebar_open_issues_count, group) + cached_open_group_issues_count(group) + else + number_with_delimiter(group_issues_count(state: 'opened')) + end + end + def group_issues_count(state:) IssuesFinder .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true) @@ -69,6 +77,21 @@ module GroupsHelper .count end + def cached_open_group_issues_count(group) + count_service = Groups::OpenIssuesCountService + issues_count = count_service.new(group, current_user).count + + if issues_count > count_service::CACHED_COUNT_THRESHOLD + ActiveSupport::NumberHelper + .number_to_human( + issues_count, + units: { thousand: 'k', million: 'm' }, precision: 1, significant: false, format: '%n%u' + ) + else + number_with_delimiter(issues_count) + end + end + def group_merge_requests_count(state:) MergeRequestsFinder .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true) diff --git a/app/helpers/in_product_marketing_helper.rb b/app/helpers/in_product_marketing_helper.rb new file mode 100644 index 00000000000..a0e533d3fb8 --- /dev/null +++ b/app/helpers/in_product_marketing_helper.rb @@ -0,0 +1,350 @@ +# frozen_string_literal: true + +module InProductMarketingHelper + def subject_line(track, series) + { + create: [ + s_('InProductMarketing|Create a project in GitLab in 5 minutes'), + s_('InProductMarketing|Import your project and code from GitHub, Bitbucket and others'), + s_('InProductMarketing|Understand repository mirroring') + ], + verify: [ + s_('InProductMarketing|Feel the need for speed?'), + s_('InProductMarketing|3 ways to dive into GitLab CI/CD'), + s_('InProductMarketing|Explore the power of GitLab CI/CD') + ], + trial: [ + s_('InProductMarketing|Go farther with GitLab'), + s_('InProductMarketing|Automated security scans directly within GitLab'), + s_('InProductMarketing|Take your source code management to the next level') + ], + team: [ + s_('InProductMarketing|Working in GitLab = more efficient'), + s_("InProductMarketing|Multiple owners, confusing workstreams? We've got you covered"), + s_('InProductMarketing|Your teams can be more efficient') + ] + }[track][series] + end + + def in_product_marketing_logo(track, series) + inline_image_link('mailers/in_product_marketing', "#{track}-#{series}.png", { width: '150', style: 'width: 150px;' }) + end + + def about_link(folder, image, width) + link_to inline_image_link(folder, image, { width: width, style: "width: #{width}px;", alt: s_('InProductMarketing|go to about.gitlab.com') }), 'https://about.gitlab.com/' + end + + def in_product_marketing_tagline(track, series) + { + create: [ + s_('InProductMarketing|Get started today'), + s_('InProductMarketing|Get our import guides'), + s_('InProductMarketing|Need an alternative to importing?') + ], + verify: [ + s_('InProductMarketing|Use GitLab CI/CD'), + s_('InProductMarketing|Test, create, deploy'), + s_('InProductMarketing|Are your runners ready?') + ], + trial: [ + s_('InProductMarketing|Start a free trial of GitLab Gold – no CC required'), + s_('InProductMarketing|Improve app security with a 30-day trial'), + s_('InProductMarketing|Start with a GitLab Gold free trial') + ], + team: [ + s_('InProductMarketing|Invite your colleagues to join in less than one minute'), + s_('InProductMarketing|Get your team set up on GitLab'), + nil + ] + }[track][series] + end + + def in_product_marketing_title(track, series) + { + create: [ + s_('InProductMarketing|Take your first steps with GitLab'), + s_('InProductMarketing|Start by importing your projects'), + s_('InProductMarketing|How (and why) mirroring makes sense') + ], + verify: [ + s_('InProductMarketing|Rapid development, simplified'), + s_('InProductMarketing|Get started with GitLab CI/CD'), + s_('InProductMarketing|Launch GitLab CI/CD in 20 minutes or less') + ], + trial: [ + s_('InProductMarketing|Give us one minute...'), + s_("InProductMarketing|Security that's integrated into your development lifecycle"), + s_('InProductMarketing|Improve code quality and streamline reviews') + ], + team: [ + s_('InProductMarketing|Team work makes the dream work'), + s_('InProductMarketing|*GitLab*, noun: a synonym for efficient teams'), + s_('InProductMarketing|Find out how your teams are really doing') + ] + }[track][series] + end + + def in_product_marketing_subtitle(track, series) + { + create: [ + s_('InProductMarketing|Dig in and create a project and a repo'), + s_("InProductMarketing|Here's what you need to know"), + s_('InProductMarketing|Try it out') + ], + verify: [ + s_('InProductMarketing|How to build and test faster'), + s_('InProductMarketing|Explore the options'), + s_('InProductMarketing|Follow our steps') + ], + trial: [ + s_('InProductMarketing|...and you can get a free trial of GitLab Gold'), + s_('InProductMarketing|Try GitLab Gold for free'), + s_('InProductMarketing|Better code in less time') + ], + team: [ + s_('InProductMarketing|Actually, GitLab makes the team work (better)'), + s_('InProductMarketing|Our tool brings all the things together'), + s_("InProductMarketing|It's all in the stats") + ] + }[track][series] + end + + def in_product_marketing_body_line1(track, series, format: nil) + { + create: [ + s_("InProductMarketing|To understand and get the most out of GitLab, start at the beginning and %{project_link}. In GitLab, repositories are part of a project, so after you've created your project you can go ahead and %{repo_link}.") % { project_link: project_link(format), repo_link: repo_link(format) }, + s_("InProductMarketing|Making the switch? It's easier than you think to import your projects into GitLab. Move %{github_link}, or import something %{bitbucket_link}.") % { github_link: github_link(format), bitbucket_link: bitbucket_link(format) }, + s_("InProductMarketing|Sometimes you're not ready to make a full transition to a new tool. If you're not ready to fully commit, %{mirroring_link} gives you a safe way to try out GitLab in parallel with your current tool.") % { mirroring_link: mirroring_link(format) } + ], + verify: [ + s_("InProductMarketing|Tired of wrestling with disparate tool chains, information silos and inefficient processes? GitLab's CI/CD is built on a DevOps platform with source code management, planning, monitoring and more ready to go. Find out %{ci_link}.") % { ci_link: ci_link(format) }, + s_("InProductMarketing|GitLab's CI/CD makes software development easier. Don't believe us? Here are three ways you can take it for a fast (and satisfying) test drive:"), + s_("InProductMarketing|Get going with CI/CD quickly using our %{quick_start_link}. Start with an available runner and then create a CI .yml file – it's really that easy.") % { quick_start_link: quick_start_link(format) } + ], + trial: [ + [ + s_("InProductMarketing|GitLab's premium tiers are designed to make you, your team and your application more efficient and more secure with features including but not limited to:"), + list([ + s_('InProductMarketing|%{strong_start}Company wide portfolio management%{strong_end} — including multi-level epics, scoped labels').html_safe % strong_options(format), + s_('InProductMarketing|%{strong_start}Multiple approval roles%{strong_end} — including code owners and required merge approvals').html_safe % strong_options(format), + s_('InProductMarketing|%{strong_start}Advanced application security%{strong_end} — including SAST, DAST scanning, FUZZ testing, dependency scanning, license compliance, secrete detection').html_safe % strong_options(format), + s_('InProductMarketing|%{strong_start}Executive level insights%{strong_end} — including reporting on productivity, tasks by type, days to completion, value stream').html_safe % strong_options(format) + ], format) + ].join("\n"), + s_('InProductMarketing|GitLab provides static application security testing (SAST), dynamic application security testing (DAST), container scanning, and dependency scanning to help you deliver secure applications along with license compliance.'), + s_('InProductMarketing|By enabling code owners and required merge approvals the right person will review the right MR. This is a win-win: cleaner code and a more efficient review process.') + ], + team: [ + [ + s_('InProductMarketing|Did you know teams that use GitLab are far more efficient?'), + list([ + s_('InProductMarketing|Goldman Sachs went from 1 build every two weeks to thousands of builds a day'), + s_('InProductMarketing|Ticketmaster decreased their CI build time by 15X') + ], format) + ].join("\n"), + s_("InProductMarketing|We know a thing or two about efficiency and we don't want to keep that to ourselves. Sign up for a free trial of GitLab Gold and your teams will be on it from day one."), + [ + s_('InProductMarketing|Stop wondering and use GitLab to answer questions like:'), + list([ + s_('InProductMarketing|How long does it take us to close issues/MRs by types like feature requests, bugs, tech debt, security?'), + s_('InProductMarketing|How many days does it take our team to complete various tasks?'), + s_('InProductMarketing|What does our value stream timeline look like from product to development to review and production?') + ], format) + ].join("\n") + ] + }[track][series] + end + + def in_product_marketing_body_line2(track, series, format: nil) + { + create: [ + s_("InProductMarketing|That's all it takes to get going with GitLab, but if you're new to working with Git, check out our %{basics_link} for helpful tips and tricks for getting started.") % { basics_link: basics_link(format) }, + s_("InProductMarketing|Have a different instance you'd like to import? Here's our %{import_link}.") % { import_link: import_link(format) }, + s_("InProductMarketing|It's also possible to simply %{external_repo_link} in order to take advantage of GitLab's CI/CD.") % { external_repo_link: external_repo_link(format) } + ], + verify: [ + nil, + list([ + s_('InProductMarketing|Start by %{performance_link}').html_safe % { performance_link: performance_link(format) }, + s_('InProductMarketing|Move on to easily creating a Pages website %{ci_template_link}').html_safe % { ci_template_link: ci_template_link(format) }, + s_('InProductMarketing|And finally %{deploy_link} a Python application.').html_safe % { deploy_link: deploy_link(format) } + ], format), + nil + ], + trial: [ + s_('InProductMarketing|Start a GitLab Gold trial today in less than one minute, no credit card required.'), + s_('InProductMarketing|Get started today with a 30-day GitLab Gold trial, no credit card required.'), + s_('InProductMarketing|Code owners and required merge approvals are part of the paid tiers of GitLab. You can start a free 30-day trial of GitLab Gold and enable these features in less than 5 minutes with no credit card required.') + ], + team: [ + s_('InProductMarketing|Invite your colleagues and start shipping code faster.'), + s_("InProductMarketing|Streamline code review, know at a glance who's unavailable, communicate in comments or in email and integrate with Slack so everyone's on the same page."), + s_('InProductMarketing|When your team is on GitLab these answers are a click away.') + ] + }[track][series] + end + + def cta_link(track, series, group, format: nil) + case format + when :html + link_to in_product_marketing_cta_text(track, series), group_email_campaigns_url(group, track: track, series: series), target: '_blank', rel: 'noopener noreferrer' + else + [in_product_marketing_cta_text(track, series), group_email_campaigns_url(group, track: track, series: series)].join(' >> ') + end + end + + def in_product_marketing_progress(track, series) + s_('InProductMarketing|This is email %{series} of 3 in the %{track} series.') % { series: series + 1, track: track.to_s.humanize } + end + + def footer_links(format: nil) + links = [ + [s_('InProductMarketing|Blog'), 'https://about.gitlab.com/blog'], + [s_('InProductMarketing|Twitter'), 'https://twitter.com/gitlab'], + [s_('InProductMarketing|Facebook'), 'https://www.facebook.com/gitlab'], + [s_('InProductMarketing|YouTube'), 'https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg'] + ] + case format + when :html + links.map do |text, link| + link_to(text, link) + end + else + '| ' + links.map do |text, link| + [text, link].join(' ') + end.join("\n| ") + end + end + + def address(format: nil) + s_('InProductMarketing|%{strong_start}GitLab Inc.%{strong_end} 268 Bush Street, #350, San Francisco, CA 94104, USA').html_safe % strong_options(format) + end + + def unsubscribe(format: nil) + parts = [ + s_('InProductMarketing|If you no longer wish to receive marketing emails from us,'), + s_('InProductMarketing|you may %{unsubscribe_link} at any time.') % { unsubscribe_link: unsubscribe_link(format) } + ] + case format + when :html + parts.join(' ') + else + parts.join("\n" + ' ' * 16) + end + end + + private + + def in_product_marketing_cta_text(track, series) + { + create: [ + s_('InProductMarketing|Create your first project!'), + s_('InProductMarketing|Master the art of importing!'), + s_('InProductMarketing|Understand your project options') + ], + verify: [ + s_('InProductMarketing|Get to know GitLab CI/CD'), + s_('InProductMarketing|Try it yourself'), + s_('InProductMarketing|Explore GitLab CI/CD') + ], + trial: [ + s_('InProductMarketing|Start a trial'), + s_('InProductMarketing|Beef up your security'), + s_('InProductMarketing|Go for the gold!') + ], + team: [ + s_('InProductMarketing|Invite your colleagues today'), + s_('InProductMarketing|Invite your team in less than 60 seconds'), + s_('InProductMarketing|Invite your team now') + ] + }[track][series] + end + + def project_link(format) + link(s_('InProductMarketing|create a project'), help_page_url('gitlab-basics/create-project'), format) + end + + def repo_link(format) + link(s_('InProductMarketing|set up a repo'), help_page_url('user/project/repository/index', anchor: 'create-a-repository'), format) + end + + def github_link(format) + link(s_('InProductMarketing|GitHub Enterprise projects to GitLab'), help_page_url('integration/github'), format) + end + + def bitbucket_link(format) + link(s_('InProductMarketing|from Bitbucket'), help_page_url('user/project/import/bitbucket_server'), format) + end + + def mirroring_link(format) + link(s_('InProductMarketing|repository mirroring'), help_page_url('user/project/repository/repository_mirroring'), format) + end + + def ci_link(format) + link(s_('InProductMarketing|how easy it is to get started'), help_page_url('ci/README'), format) + end + + def performance_link(format) + link(s_('InProductMarketing|testing browser performance'), help_page_url('user/project/merge_requests/browser_performance_testing'), format) + end + + def ci_template_link(format) + link(s_('InProductMarketing|using a CI/CD template'), help_page_url('user/project/pages/getting_started/pages_ci_cd_template'), format) + end + + def deploy_link(format) + link(s_('InProductMarketing|test and deploy'), help_page_url('ci/examples/test-and-deploy-python-application-to-heroku'), format) + end + + def quick_start_link(format) + link(s_('InProductMarketing|quick start guide'), help_page_url('ci/quick_start/README'), format) + end + + def basics_link(format) + link(s_('InProductMarketing|Git basics'), help_page_url('gitlab-basics/README'), format) + end + + def import_link(format) + link(s_('InProductMarketing|comprehensive guide'), help_page_url('user/project/import/index'), format) + end + + def external_repo_link(format) + link(s_('InProductMarketing|connect an external repository'), new_project_url(anchor: 'cicd_for_external_repo'), format) + end + + def unsubscribe_link(format) + link(s_('InProductMarketing|unsubscribe'), '%tag_unsubscribe_url%', format) + end + + def link(text, link, format) + case format + when :html + link_to text, link + else + "#{text} (#{link})" + end + end + + def list(array, format) + case format + when :html + tag.ul { array.map { |item| concat tag.li item} } + else + '- ' + array.join("\n- ") + end + end + + def strong_options(format) + case format + when :html + { strong_start: ''.html_safe, strong_end: ''.html_safe } + else + { strong_start: '', strong_end: '' } + end + end + + def inline_image_link(folder, image, **options) + attachments.inline[image] = File.read(Rails.root.join("app/assets/images", folder, image)) + image_tag attachments[image].url, **options + end +end diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb index a643fea6d5a..889365e39de 100644 --- a/app/helpers/invite_members_helper.rb +++ b/app/helpers/invite_members_helper.rb @@ -3,10 +3,14 @@ module InviteMembersHelper include Gitlab::Utils::StrongMemoize - def invite_members_allowed?(group) + def can_invite_members_for_group?(group) Feature.enabled?(:invite_members_group_modal, group) && can?(current_user, :admin_group_member, group) end + def can_invite_members_for_project?(project) + Feature.enabled?(:invite_members_group_modal, project.group) && can_import_members? + end + def directly_invite_members? strong_memoize(:directly_invite_members) do experiment_enabled?(:invite_members_version_a) && can_import_members? @@ -27,8 +31,8 @@ module InviteMembersHelper link_to invite_members_url(form_model), data: { 'track-event': 'click_link', - 'track-label': tracking_label(current_user), - 'track-property': experiment_tracking_category_and_group(:invite_members_new_dropdown, subject: current_user) + 'track-label': tracking_label, + 'track-property': experiment_tracking_category_and_group(:invite_members_new_dropdown) } do invite_member_link_content end diff --git a/app/helpers/issuables_description_templates_helper.rb b/app/helpers/issuables_description_templates_helper.rb new file mode 100644 index 00000000000..110b3954900 --- /dev/null +++ b/app/helpers/issuables_description_templates_helper.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module IssuablesDescriptionTemplatesHelper + include Gitlab::Utils::StrongMemoize + include GitlabRoutingHelper + + def template_dropdown_tag(issuable, &block) + title = selected_template(issuable) || "Choose a template" + options = { + toggle_class: 'js-issuable-selector', + title: title, + filter: true, + placeholder: 'Filter', + footer_content: true, + data: { + data: issuable_templates(ref_project, issuable.to_ability_name), + field_name: 'issuable_template', + selected: selected_template(issuable), + project_id: ref_project.id + } + } + + dropdown_tag(title, options: options) do + capture(&block) + end + end + + def issuable_templates(project, issuable_type) + @template_types ||= {} + @template_types[project.id] ||= {} + @template_types[project.id][issuable_type] ||= TemplateFinder.all_template_names_array(project, issuable_type.pluralize) + end + + def issuable_templates_names(issuable) + issuable_templates(ref_project, issuable.to_ability_name).map { |template| template[:name] } + end + + def selected_template(issuable) + params[:issuable_template] if issuable_templates(ref_project, issuable.to_ability_name).any? { |template| template[:name] == params[:issuable_template] } + end + + def template_names_path(parent, issuable) + return '' unless parent.is_a?(Project) + + project_template_names_path(parent, template_type: issuable.to_ability_name) + end +end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index da142cbed0e..41e9f61cf9f 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -2,6 +2,7 @@ module IssuablesHelper include GitlabRoutingHelper + include IssuablesDescriptionTemplatesHelper def sidebar_gutter_toggle_icon content_tag(:span, class: 'js-sidebar-toggle-container', data: { is_expanded: !sidebar_gutter_collapsed? }) do @@ -75,28 +76,6 @@ module IssuablesHelper .to_json end - def template_dropdown_tag(issuable, &block) - title = selected_template(issuable) || "Choose a template" - options = { - toggle_class: 'js-issuable-selector', - title: title, - filter: true, - placeholder: 'Filter', - footer_content: true, - data: { - data: issuable_templates(issuable), - field_name: 'issuable_template', - selected: selected_template(issuable), - project_path: ref_project.path, - namespace_path: ref_project.namespace.full_path - } - } - - dropdown_tag(title, options: options) do - capture(&block) - end - end - def users_dropdown_label(selected_users) case selected_users.length when 0 @@ -215,24 +194,12 @@ module IssuablesHelper state_title = titles[state] || state.to_s.humanize html = content_tag(:span, state_title) - if display_count - count = issuables_count_for_state(issuable_type, state) - tag = - if count == -1 - tooltip = _("Couldn't calculate number of %{issuables}.") % { issuables: issuable_type.to_s.humanize(capitalize: false) } - - content_tag( - :span, - '?', - class: 'badge badge-pill has-tooltip', - aria: { label: tooltip }, - title: tooltip - ) - else - content_tag(:span, number_with_delimiter(count), class: 'badge badge-pill') - end - - html << " " << tag + return html.html_safe unless display_count + + count = issuables_count_for_state(issuable_type, state) + + if count != -1 + html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge badge-pill') end html.html_safe @@ -294,6 +261,7 @@ module IssuablesHelper { projectPath: ref_project.path, + projectId: ref_project.id, projectNamespace: ref_project.namespace.full_path } end @@ -346,6 +314,7 @@ module IssuablesHelper def assignee_sidebar_data(assignee, merge_request: nil) { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }.tap do |data| data[:can_merge] = merge_request.can_be_merged_by?(assignee) if merge_request + data[:availability] = assignee.status.availability if assignee.association(:status).loaded? && assignee.status&.availability end end @@ -369,24 +338,6 @@ module IssuablesHelper cookies[:collapsed_gutter] == 'true' end - def issuable_templates(issuable) - @issuable_templates ||= - case issuable - when Issue - ref_project.repository.issue_template_names - when MergeRequest - ref_project.repository.merge_request_template_names - end - end - - def issuable_templates_names(issuable) - issuable_templates(issuable).map { |template| template[:name] } - end - - def selected_template(issuable) - params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] } - end - def issuable_todo_button_data(issuable, is_collapsed) { todo_text: _('Add a to do'), @@ -424,12 +375,6 @@ module IssuablesHelper end end - def template_names_path(parent, issuable) - return '' unless parent.is_a?(Project) - - project_template_names_path(parent, template_type: issuable.class.name.underscore) - end - def issuable_sidebar_options(issuable) { endpoint: "#{issuable[:issuable_json_path]}?serializer=sidebar_extras", diff --git a/app/helpers/jira_connect_helper.rb b/app/helpers/jira_connect_helper.rb index f1527b9b85a..080883fd594 100644 --- a/app/helpers/jira_connect_helper.rb +++ b/app/helpers/jira_connect_helper.rb @@ -5,9 +5,15 @@ module JiraConnectHelper Feature.enabled?(:new_jira_connect_ui, type: :development, default_enabled: :yaml) end - def jira_connect_app_data + def jira_connect_app_data(subscriptions) + return {} unless new_jira_connect_ui? + + skip_groups = subscriptions.map(&:namespace_id) + { - groups_path: api_v4_groups_path(params: { min_access_level: Gitlab::Access::MAINTAINER }) + groups_path: api_v4_groups_path(params: { min_access_level: Gitlab::Access::MAINTAINER, skip_groups: skip_groups }), + subscriptions_path: jira_connect_subscriptions_path, + users_path: current_user ? nil : jira_connect_users_path } end end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 312d535a92c..cfc4075100b 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -80,27 +80,27 @@ module LabelsHelper def suggested_colors { - '#0033CC' => s_('SuggestedColors|UA blue'), - '#428BCA' => s_('SuggestedColors|Moderate blue'), - '#44AD8E' => s_('SuggestedColors|Lime green'), - '#A8D695' => s_('SuggestedColors|Feijoa'), - '#5CB85C' => s_('SuggestedColors|Slightly desaturated green'), - '#69D100' => s_('SuggestedColors|Bright green'), - '#004E00' => s_('SuggestedColors|Very dark lime green'), - '#34495E' => s_('SuggestedColors|Very dark desaturated blue'), - '#7F8C8D' => s_('SuggestedColors|Dark grayish cyan'), - '#A295D6' => s_('SuggestedColors|Slightly desaturated blue'), - '#5843AD' => s_('SuggestedColors|Dark moderate blue'), - '#8E44AD' => s_('SuggestedColors|Dark moderate violet'), - '#FFECDB' => s_('SuggestedColors|Very pale orange'), - '#AD4363' => s_('SuggestedColors|Dark moderate pink'), - '#D10069' => s_('SuggestedColors|Strong pink'), - '#CC0033' => s_('SuggestedColors|Strong red'), - '#FF0000' => s_('SuggestedColors|Pure red'), - '#D9534F' => s_('SuggestedColors|Soft red'), - '#D1D100' => s_('SuggestedColors|Strong yellow'), - '#F0AD4E' => s_('SuggestedColors|Soft orange'), - '#AD8D43' => s_('SuggestedColors|Dark moderate orange') + '#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|Lavendar'), + '#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') } end diff --git a/app/helpers/learn_gitlab_helper.rb b/app/helpers/learn_gitlab_helper.rb new file mode 100644 index 00000000000..e72a9c83fc9 --- /dev/null +++ b/app/helpers/learn_gitlab_helper.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module LearnGitlabHelper + def learn_gitlab_experiment_enabled?(project) + return false unless current_user + return false unless experiment_enabled_for_user? + + learn_gitlab_onboarding_available?(project) + end + + def onboarding_actions_data(project) + attributes = onboarding_progress(project).attributes.symbolize_keys + + action_urls.map do |action, url| + [ + action, + url: url, + completed: attributes[OnboardingProgress.column_name(action)].present? + ] + end.to_h + end + + private + + ACTION_ISSUE_IDS = { + git_write: 2, + pipeline_created: 4, + merge_request_created: 6, + user_added: 7, + trial_started: 13, + required_mr_approvals_enabled: 15, + code_owners_enabled: 16 + }.freeze + + ACTION_DOC_URLS = { + security_scan_enabled: 'https://docs.gitlab.com/ee/user/application_security/security_dashboard/#gitlab-security-dashboard-security-center-and-vulnerability-reports' + }.freeze + + def action_urls + ACTION_ISSUE_IDS.transform_values { |id| project_issue_url(learn_gitlab_project, id) }.merge(ACTION_DOC_URLS) + end + + def learn_gitlab_project + @learn_gitlab_project ||= LearnGitlab.new(current_user).project + end + + def onboarding_progress(project) + OnboardingProgress.find_by(namespace: project.namespace) # rubocop: disable CodeReuse/ActiveRecord + end + + def experiment_enabled_for_user? + Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_a, subject: current_user) || + Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_b, subject: current_user) + end + + def learn_gitlab_onboarding_available?(project) + OnboardingProgress.onboarding?(project.namespace) && + LearnGitlab.new(current_user).available? + end +end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 6ab5f499b9a..ff1305f8cc5 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -174,15 +174,9 @@ module MergeRequestsHelper end end - def merge_request_reviewers_enabled? - Feature.enabled?(:merge_request_reviewers, default_enabled: :yaml) - end - private def review_requested_merge_requests_count - return 0 unless merge_request_reviewers_enabled? - current_user.review_requested_open_merge_requests_count end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 3c757a4ef26..c170e58b4ce 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -64,7 +64,7 @@ module NavHelper end def admin_analytics_nav_links - %w(dev_ops_report cohorts) + %w(dev_ops_report) end def group_issues_sub_menu_items diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 871d19c6a8c..62580124c0f 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -175,7 +175,9 @@ module NotesHelper end end - def notes_data(issuable) + def notes_data(issuable, start_at_zero = false) + initial_last_fetched_at = start_at_zero ? 0 : Time.current.to_i * ::Gitlab::UpdatedNotesPaginator::MICROSECOND + data = { discussionsPath: discussions_path(issuable), registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), @@ -186,7 +188,7 @@ module NotesHelper reopenPath: reopen_issuable_path(issuable), notesPath: notes_url, prerenderedNotesCount: issuable.capped_notes_count(MAX_PRERENDERED_NOTES), - lastFetchedAt: Time.now.to_i * ::Gitlab::UpdatedNotesPaginator::MICROSECOND + lastFetchedAt: initial_last_fetched_at } if issuable.is_a?(MergeRequest) diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 2b68d953431..729585be84a 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -125,4 +125,13 @@ module NotificationsHelper def can_read_project?(project) can?(current_user, :read_project, project) end + + def notification_dropdown_items(notification_setting) + NotificationSetting.levels.each_key.map do |level| + next if level == "custom" + next if level == "global" && notification_setting.source.nil? + + level + end.compact + end end diff --git a/app/helpers/notify_helper.rb b/app/helpers/notify_helper.rb index db7527d9d58..03da679cfdd 100644 --- a/app/helpers/notify_helper.rb +++ b/app/helpers/notify_helper.rb @@ -8,4 +8,30 @@ module NotifyHelper def issue_reference_link(entity, *args, full: false) link_to(entity.to_reference(full: full), issue_url(entity, *args)) end + + def invited_role_description(role_name) + case role_name + when "Guest" + s_("InviteEmail|As a guest, you can view projects, leave comments, and create issues.") + when "Reporter" + s_("InviteEmail|As a reporter, you can view projects and reports, and leave comments on issues.") + when "Developer" + s_("InviteEmail|As a developer, you have full access to projects, so you can take an idea from concept to production.") + when "Maintainer" + s_("InviteEmail|As a maintainer, you have full access to projects. You can push commits to master and deploy to production.") + when "Owner" + s_("InviteEmail|As an owner, you have full access to projects and can manage access to the group, including inviting new members.") + when "Minimal Access" + s_("InviteEmail|As a user with minimal access, you can view the high-level group from the UI and API.") + end + end + + def invited_to_description(source) + case source + when "project" + s_('InviteEmail|Projects can be used to host your code, track issues, collaborate on code, and continuously build, test, and deploy your app with built-in GitLab CI/CD.') + when "group" + s_('InviteEmail|Groups assemble related projects together and grant members access to several projects at once.') + end + end end diff --git a/app/helpers/operations_helper.rb b/app/helpers/operations_helper.rb index 6d721776f0d..51f4304911b 100644 --- a/app/helpers/operations_helper.rb +++ b/app/helpers/operations_helper.rb @@ -17,7 +17,7 @@ module OperationsHelper 'prometheus_authorization_key' => @project.alerting_setting&.token, 'prometheus_api_url' => prometheus_service.api_url, 'prometheus_url' => notify_project_prometheus_alerts_url(@project, format: :json), - 'alerts_setup_url' => help_page_path('operations/incident_management/alert_integrations.md', anchor: 'generic-http-endpoint'), + 'alerts_setup_url' => help_page_path('operations/incident_management/integrations.md', anchor: 'configuration'), 'alerts_usage_url' => project_alert_management_index_path(@project), 'disabled' => disabled.to_s, 'project_path' => @project.full_path, diff --git a/app/helpers/projects/alert_management_helper.rb b/app/helpers/projects/alert_management_helper.rb index 58f1abb2818..b705258f133 100644 --- a/app/helpers/projects/alert_management_helper.rb +++ b/app/helpers/projects/alert_management_helper.rb @@ -5,8 +5,8 @@ module Projects::AlertManagementHelper { 'project-path' => project.full_path, 'enable-alert-management-path' => project_settings_operations_path(project, anchor: 'js-alert-management-settings'), - 'alerts-help-url' => help_page_url('operations/incident_management/index.md'), - 'populating-alerts-help-url' => help_page_url('operations/incident_management/index.md', anchor: 'enable-alert-management'), + 'alerts-help-url' => help_page_url('operations/incident_management/alerts.md'), + 'populating-alerts-help-url' => help_page_url('operations/incident_management/integrations.md', anchor: 'configuration'), 'empty-alert-svg-path' => image_path('illustrations/alert-management-empty-state.svg'), 'user-can-enable-alert-management' => can?(current_user, :admin_operations, project).to_s, 'alert-management-enabled' => alert_management_enabled?(project).to_s, @@ -20,7 +20,8 @@ module Projects::AlertManagementHelper 'alert-id' => alert_id, 'project-path' => project.full_path, 'project-id' => project.id, - 'project-issues-path' => project_issues_path(project) + 'project-issues-path' => project_issues_path(project), + 'page' => 'OPERATIONS' } end diff --git a/app/helpers/projects/project_members_helper.rb b/app/helpers/projects/project_members_helper.rb index 168526d2abb..99c1b742da4 100644 --- a/app/helpers/projects/project_members_helper.rb +++ b/app/helpers/projects/project_members_helper.rb @@ -26,4 +26,30 @@ module Projects::ProjectMembersHelper project.group.has_owner?(current_user) end + + def project_group_links_data_json(group_links) + GroupLink::ProjectGroupLinkSerializer.new.represent(group_links, { current_user: current_user }).to_json + end + + def project_members_data_json(project, members) + MemberSerializer.new.represent(members, { current_user: current_user, group: project.group, source: project }).to_json + end + + def project_members_list_data_attributes(project, members) + { + members: project_members_data_json(project, members), + member_path: project_project_member_path(project, ':id'), + source_id: project.id, + can_manage_members: can_manage_project_members?(project) + } + end + + def project_group_links_list_data_attributes(project, group_links) + { + members: project_group_links_data_json(group_links), + member_path: project_group_link_path(project, ':id'), + source_id: project.id, + can_manage_members: can_manage_project_members?(project) + } + end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index b21d3ca51db..f5cd89d96b4 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -139,6 +139,10 @@ module ProjectsHelper project_nav_tabs.include? name end + def any_project_nav_tab?(tabs) + tabs.any? { |tab| project_nav_tab?(tab) } + end + def project_for_deploy_key(deploy_key) if deploy_key.has_access_to?(@project) @project @@ -267,10 +271,6 @@ module ProjectsHelper "xcode://clone?repo=#{CGI.escape(default_url_to_repo(project))}" end - def link_to_filter_repo - link_to 'git filter-repo', 'https://github.com/newren/git-filter-repo', target: '_blank', rel: 'noopener noreferrer' - end - def explore_projects_tab? current_page?(explore_projects_path) || current_page?(trending_explore_projects_path) || @@ -378,6 +378,20 @@ module ProjectsHelper private + def can_read_security_configuration?(project, current_user) + ::Feature.enabled?(:secure_security_and_compliance_configuration_page_on_ce, @subject, default_enabled: :yaml) && + can?(current_user, :read_security_configuration, project) + end + + def get_project_security_nav_tabs(project, current_user) + if can_read_security_configuration?(project, current_user) + [:security_and_compliance, :security_configuration] + else + [] + end + end + + # rubocop:disable Metrics/CyclomaticComplexity def get_project_nav_tabs(project, current_user) nav_tabs = [:home] @@ -386,6 +400,8 @@ module ProjectsHelper nav_tabs << :releases if can?(current_user, :read_release, project) end + nav_tabs += get_project_security_nav_tabs(project, current_user) + if project.repo_exists? && can?(current_user, :read_merge_request, project) nav_tabs << :merge_requests end @@ -417,8 +433,11 @@ module ProjectsHelper nav_tabs += package_nav_tabs(project, current_user) + nav_tabs << :learn_gitlab if learn_gitlab_experiment_enabled?(project) + nav_tabs end + # rubocop:enable Metrics/CyclomaticComplexity def package_nav_tabs(project, current_user) [].tap do |tabs| @@ -699,6 +718,12 @@ module ProjectsHelper "#{request.path}?#{options.to_param}" end + def sidebar_security_configuration_paths + %w[ + projects/security/configuration#show + ] + end + def sidebar_projects_paths %w[ projects#show @@ -763,6 +788,10 @@ module ProjectsHelper ] end + def sidebar_security_paths + %w[projects/security/configuration#show] + end + def user_can_see_auto_devops_implicitly_enabled_banner?(project, user) Ability.allowed?(user, :admin_project, project) && project.has_auto_devops_implicitly_enabled? && diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index bdc86043ddc..a7acc0cd7db 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -129,6 +129,27 @@ module SearchHelper @search_service ||= ::SearchService.new(current_user, params.merge(confidential: Gitlab::Utils.to_boolean(params[:confidential]))) end + def search_sort_options + [ + { + title: _('Created date'), + sortable: true, + sortParam: { + asc: 'created_asc', + desc: 'created_desc' + } + }, + { + title: _('Last updated'), + sortable: true, + sortParam: { + asc: 'updated_asc', + desc: 'updated_desc' + } + } + ] + end + private # Autocomplete results for various settings pages diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index 5f361e6653d..14d20e7c622 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -128,6 +128,13 @@ module ServicesHelper !Gitlab.com? end + def jira_issue_breadcrumb_link(issue_reference) + link_to '', { class: 'gl-display-flex gl-align-items-center gl-white-space-nowrap' } do + icon = image_tag image_path('illustrations/logos/jira.svg'), width: 15, height: 15, class: 'gl-mr-2' + [icon, issue_reference].join.html_safe + end + end + extend self private diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 38758957dba..35c8b140bfe 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -30,8 +30,7 @@ module SortingHelper sort_value_contacted_date => sort_title_contacted_date, sort_value_relative_position => sort_title_relative_position, sort_value_size => sort_title_size, - sort_value_expire_date => sort_title_expire_date, - sort_value_relevant => sort_title_relevant + sort_value_expire_date => sort_title_expire_date } end @@ -85,13 +84,6 @@ module SortingHelper } end - def search_reverse_sort_options_hash - { - sort_value_recently_created => sort_value_oldest_created, - sort_value_oldest_created => sort_value_recently_created - } - end - def groups_sort_options_hash { sort_value_name => sort_title_name, @@ -229,10 +221,6 @@ module SortingHelper sort_options_hash[sort_value] end - def search_sort_option_title(sort_value) - sort_options_hash[sort_value] - end - def sort_direction_icon(sort_value) case sort_value when sort_value_milestone, sort_value_due_date, /_asc\z/ @@ -271,13 +259,6 @@ module SortingHelper sort_direction_button(url, reverse_sort, sort_value) end - def search_sort_direction_button(sort_value) - reverse_sort = search_reverse_sort_options_hash[sort_value] - url = page_filter_path(sort: reverse_sort) - - sort_direction_button(url, reverse_sort, sort_value) - end - def packages_sort_options_hash { sort_value_recently_created => sort_title_created_date, diff --git a/app/helpers/sorting_titles_values_helper.rb b/app/helpers/sorting_titles_values_helper.rb index 27f3638dc73..651a6437479 100644 --- a/app/helpers/sorting_titles_values_helper.rb +++ b/app/helpers/sorting_titles_values_helper.rb @@ -166,10 +166,6 @@ module SortingTitlesValuesHelper s_('SortOptions|Expired date') end - def sort_title_relevant - s_('SortOptions|Relevant') - end - # Values. def sort_value_access_level_asc 'access_level_asc' @@ -330,10 +326,6 @@ module SortingTitlesValuesHelper def sort_value_expire_date 'expired_asc' end - - def sort_value_relevant - 'relevant' - end end SortingHelper.include_if_ee('::EE::SortingTitlesValuesHelper') diff --git a/app/helpers/stat_anchors_helper.rb b/app/helpers/stat_anchors_helper.rb index 76e58b45912..1e8e6371284 100644 --- a/app/helpers/stat_anchors_helper.rb +++ b/app/helpers/stat_anchors_helper.rb @@ -11,14 +11,14 @@ module StatAnchorsHelper private def button_attribute(anchor) - "btn-#{anchor.class_modifier || 'missing'}" + "btn-#{anchor.class_modifier || 'dashed'}" end def extra_classes(anchor) if anchor.is_link 'stat-link' else - "btn #{button_attribute(anchor)}" + "gl-button btn #{button_attribute(anchor)}" end end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index f24aa5d3bcb..b050f533d77 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -4,30 +4,6 @@ module TreeHelper include BlobHelper include WebIdeButtonHelper - FILE_LIMIT = 1_000 - - # Sorts a repository's tree so that folders are before files and renders - # their corresponding partials - # - # tree - A `Tree` object for the current tree - # rubocop: disable CodeReuse/ActiveRecord - def render_tree(tree) - # Sort submodules and folders together by name ahead of files - folders, files, submodules = tree.trees, tree.blobs, tree.submodules - tree = [] - items = (folders + submodules).sort_by(&:name) + files - - if items.size > FILE_LIMIT - tree << render(partial: 'projects/tree/truncated_notice_tree_row', - locals: { limit: FILE_LIMIT, total: items.size }) - items = items.take(FILE_LIMIT) - end - - tree << render(partial: 'projects/tree/tree_row', collection: items) if items.present? - tree.join.html_safe - end - # rubocop: enable CodeReuse/ActiveRecord - # Return an image icon depending on the file type and mode # # type - String type of the tree item; either 'folder' or 'file' @@ -37,20 +13,6 @@ module TreeHelper sprite_icon(file_type_icon_class(type, mode, name)) end - # Using Rails `*_path` methods can be slow, especially when generating - # many paths, as with a repository tree that has thousands of items. - def fast_project_blob_path(project, blob_path) - ActionDispatch::Journey::Router::Utils.escape_path( - File.join(relative_url_root, project.path_with_namespace, '-', 'blob', blob_path) - ) - end - - def fast_project_tree_path(project, tree_path) - ActionDispatch::Journey::Router::Utils.escape_path( - File.join(relative_url_root, project.path_with_namespace, '-', 'tree', tree_path) - ) - end - # Simple shortcut to File.join def tree_join(*args) File.join(*args) @@ -167,13 +129,6 @@ module TreeHelper Gitlab.config.gitlab.relative_url_root.presence || '/' end - # project and path are used on the EE version - def tree_content_data(logs_path, project, path) - { - "logs-path" => logs_path - } - end - def breadcrumb_data_attributes attrs = { can_collaborate: can_collaborate_with_project?(@project).to_s, diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb index a06a31ddf32..f55a6c3c9e5 100644 --- a/app/helpers/user_callouts_helper.rb +++ b/app/helpers/user_callouts_helper.rb @@ -11,6 +11,7 @@ module UserCalloutsHelper CUSTOMIZE_HOMEPAGE = 'customize_homepage' FEATURE_FLAGS_NEW_VERSION = 'feature_flags_new_version' REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout' + UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout' def show_admin_integrations_moved? !user_dismissed?(ADMIN_INTEGRATIONS_MOVED) @@ -56,6 +57,10 @@ module UserCalloutsHelper !user_dismissed?(FEATURE_FLAGS_NEW_VERSION) end + def show_unfinished_tag_cleanup_callout? + !user_dismissed?(UNFINISHED_TAG_CLEANUP_CALLOUT) + end + def show_registration_enabled_user_callout? !Gitlab.com? && current_user&.admin? && diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index a5d4d6872df..1979426f844 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -242,7 +242,7 @@ module UsersHelper tabs = [] if can?(current_user, :read_user_profile, @user) - tabs += [:overview, :activity, :groups, :contributed, :projects, :starred, :snippets] + tabs += [:overview, :activity, :groups, :contributed, :projects, :starred, :snippets, :followers, :following] end tabs @@ -299,6 +299,27 @@ module UsersHelper html_escape(s_('Profile|%{job_title} at %{organization}')) % { job_title: job_title, organization: organization } end + + def user_table_headers + [ + { + section_class_name: 'section-40', + header_text: _('Name') + }, + { + section_class_name: 'section-10', + header_text: _('Projects') + }, + { + section_class_name: 'section-15', + header_text: _('Created on') + }, + { + section_class_name: 'section-15', + header_text: _('Last activity') + } + ] + end end UsersHelper.prepend_if_ee('EE::UsersHelper') -- cgit v1.2.3