# frozen_string_literal: true class ProjectPresenter < Gitlab::View::Presenter::Delegated include ActionView::Helpers::NumberHelper include GitlabRoutingHelper include StorageHelper include TreeHelper include Ci::PipelineEditorHelper include IconsHelper include BlobHelper include ChecksCollaboration include Gitlab::Utils::StrongMemoize include Gitlab::Experiment::Dsl delegator_override_with GitlabRoutingHelper # TODO: Remove `GitlabRoutingHelper` inclusion as it's duplicate delegator_override_with Gitlab::Utils::StrongMemoize # This module inclusion is expected. See https://gitlab.com/gitlab-org/gitlab/-/issues/352884. presents ::Project, as: :project AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon, :itemprop, :data) MAX_TOPICS_TO_SHOW = 3 def statistic_icon(icon_name = 'plus-square-o') sprite_icon(icon_name, css_class: 'icon gl-mr-2 gl-text-gray-500') end def statistics_anchors(show_auto_devops_callout:) [ commits_anchor_data, branches_anchor_data, tags_anchor_data, storage_anchor_data, releases_anchor_data ].compact.select(&:is_link) end def statistics_buttons(show_auto_devops_callout:) [ readme_anchor_data, license_anchor_data, changelog_anchor_data, contribution_guide_anchor_data, autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout), kubernetes_cluster_anchor_data, gitlab_ci_anchor_data, integrations_anchor_data ].compact.reject(&:is_link).sort_by.with_index { |item, idx| [item.class_modifier ? 0 : 1, idx] } end def empty_repo_statistics_anchors [] end def empty_repo_statistics_buttons [ upload_anchor_data, new_file_anchor_data, readme_anchor_data, license_anchor_data, changelog_anchor_data, contribution_guide_anchor_data, gitlab_ci_anchor_data, integrations_anchor_data ].compact.reject { |item| item.is_link } end def default_view return anonymous_project_view unless current_user user_view = current_user.project_view if can?(current_user, :read_code, project) user_view elsif user_view == 'activity' 'activity' elsif project.wiki_repository_exists? && can?(current_user, :read_wiki, project) 'wiki' elsif can?(current_user, :read_issue, project) 'projects/issues/issues' else 'activity' end end def readme_path filename_path(repository.readme_path) end def changelog_path filename_path(repository.changelog&.name) end def license_path filename_path(repository.license_blob&.name) end def contribution_guide_path if project && contribution_guide = repository.contribution_guide project_blob_path( project, tree_join(project.default_branch, contribution_guide.name) ) end end def add_license_path add_special_file_path(file_name: 'LICENSE') end def add_license_ide_path ide_edit_path(project, default_branch_or_main, 'LICENSE') end def add_changelog_path add_special_file_path(file_name: 'CHANGELOG') end def add_changelog_ide_path ide_edit_path(project, default_branch_or_main, 'CHANGELOG') end def add_contribution_guide_path add_special_file_path(file_name: 'CONTRIBUTING.md', commit_message: 'Add CONTRIBUTING') end def add_contribution_guide_ide_path ide_edit_path(project, default_branch_or_main, 'CONTRIBUTING.md') end def add_readme_path add_special_file_path(file_name: 'README.md') end def add_readme_ide_path ide_edit_path(project, default_branch_or_main, 'README.md') end def license_short_name license = repository.license license&.nickname || license&.name || 'LICENSE' end def can_current_user_push_code? strong_memoize(:can_current_user_push_code) do if empty_repo? can?(current_user, :push_code, project) else can_current_user_push_to_branch?(default_branch) end end end def can_current_user_push_to_branch?(branch) return false unless current_user user_access(project).can_push_to_branch?(branch) end def can_current_user_push_to_default_branch? can_current_user_push_to_branch?(default_branch) end def storage_anchor_data can_show_quota = can?(current_user, :admin_project, project) && !empty_repo? AnchorData.new(true, statistic_icon('disk') + _('%{strong_start}%{human_size}%{strong_end} Project Storage').html_safe % { human_size: storage_counter(statistics.storage_size), strong_start: ''.html_safe, strong_end: ''.html_safe }, can_show_quota ? project_usage_quotas_path(project) : nil) end def releases_anchor_data return unless can?(current_user, :read_release, project) releases_count = project.releases.count return if releases_count < 1 AnchorData.new(true, statistic_icon('deployments') + n_('%{strong_start}%{release_count}%{strong_end} Release', '%{strong_start}%{release_count}%{strong_end} Releases', releases_count).html_safe % { release_count: number_with_delimiter(releases_count), strong_start: ''.html_safe, strong_end: ''.html_safe }, project_releases_path(project)) end def commits_anchor_data AnchorData.new(true, statistic_icon('commit') + n_('%{strong_start}%{commit_count}%{strong_end} Commit', '%{strong_start}%{commit_count}%{strong_end} Commits', statistics.commit_count).html_safe % { commit_count: number_with_delimiter(statistics.commit_count), strong_start: ''.html_safe, strong_end: ''.html_safe }, empty_repo? ? nil : project_commits_path(project, default_branch_or_main)) end def branches_anchor_data AnchorData.new(true, statistic_icon('branch') + n_('%{strong_start}%{branch_count}%{strong_end} Branch', '%{strong_start}%{branch_count}%{strong_end} Branches', repository.branch_count).html_safe % { branch_count: number_with_delimiter(repository.branch_count), strong_start: ''.html_safe, strong_end: ''.html_safe }, empty_repo? ? nil : project_branches_path(project)) end def tags_anchor_data AnchorData.new(true, statistic_icon('label') + n_('%{strong_start}%{tag_count}%{strong_end} Tag', '%{strong_start}%{tag_count}%{strong_end} Tags', repository.tag_count).html_safe % { tag_count: number_with_delimiter(repository.tag_count), strong_start: ''.html_safe, strong_end: ''.html_safe }, empty_repo? ? nil : project_tags_path(project)) end def upload_anchor_data strong_memoize(:upload_anchor_data) do next unless can_current_user_push_to_default_branch? AnchorData.new(false, statistic_icon('upload') + _('Upload file'), '#modal-upload-blob', 'js-upload-file-trigger', nil, nil, { 'target_branch' => default_branch_or_main, 'original_branch' => default_branch_or_main, 'can_push_code' => 'true', 'path' => project_create_blob_path(project, default_branch_or_main), 'project_path' => project.full_path } ) end end def new_file_anchor_data if can_current_user_push_to_default_branch? new_file_path = empty_repo? ? ide_edit_path(project, default_branch_or_main) : project_new_blob_path(project, default_branch_or_main) AnchorData.new(false, statistic_icon + _('New file'), new_file_path, 'btn-dashed') end end def readme_anchor_data if can_current_user_push_to_default_branch? && readme_path.nil? AnchorData.new(false, statistic_icon + _('Add README'), empty_repo? ? add_readme_ide_path : add_readme_path) elsif readme_path AnchorData.new(false, statistic_icon('doc-text') + _('README'), default_view != 'readme' ? readme_path : '#readme', 'btn-default', 'doc-text') end end def changelog_anchor_data if can_current_user_push_to_default_branch? && repository.changelog.blank? AnchorData.new(false, statistic_icon + _('Add CHANGELOG'), empty_repo? ? add_changelog_ide_path : add_changelog_path) elsif repository.changelog.present? AnchorData.new(false, statistic_icon('doc-text') + _('CHANGELOG'), changelog_path, 'btn-default') end end def license_anchor_data icon = statistic_icon('scale') if repository.license_blob.present? AnchorData.new(false, icon + content_tag(:span, license_short_name, class: 'project-stat-value'), license_path, 'btn-default', nil, 'license') elsif can_current_user_push_to_default_branch? AnchorData.new(false, content_tag(:span, statistic_icon + _('Add LICENSE'), class: 'add-license-link d-flex'), empty_repo? ? add_license_ide_path : add_license_path) end end def contribution_guide_anchor_data if can_current_user_push_to_default_branch? && repository.contribution_guide.blank? AnchorData.new(false, statistic_icon + _('Add CONTRIBUTING'), empty_repo? ? add_contribution_guide_ide_path : add_contribution_guide_path) elsif repository.contribution_guide.present? AnchorData.new(false, statistic_icon('doc-text') + _('CONTRIBUTING'), contribution_guide_path, 'btn-default') end end def autodevops_anchor_data(show_auto_devops_callout: false) return unless project.feature_available?(:builds, current_user) if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout if auto_devops_enabled? AnchorData.new(false, statistic_icon('settings') + _('Auto DevOps enabled'), project_settings_ci_cd_path(project, anchor: 'autodevops-settings'), 'btn-default') else AnchorData.new(false, statistic_icon + _('Enable Auto DevOps'), project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) end elsif auto_devops_enabled? AnchorData.new(false, _('Auto DevOps enabled'), nil) end end def kubernetes_cluster_anchor_data if can_instantiate_cluster? if clusters.empty? AnchorData.new(false, statistic_icon + _('Add Kubernetes cluster'), project_clusters_path(project)) else cluster_link = clusters.count == 1 ? project_cluster_path(project, clusters.first) : project_clusters_path(project) AnchorData.new(false, _('Kubernetes'), cluster_link, 'btn-default') end end end def gitlab_ci_anchor_data return unless can_view_pipeline_editor?(project) if cicd_missing? AnchorData.new(false, statistic_icon + _('Set up CI/CD'), project_ci_pipeline_editor_path(project)) elsif repository.gitlab_ci_yml.present? AnchorData.new(false, statistic_icon('doc-text') + _('CI/CD configuration'), project_ci_pipeline_editor_path(project), 'btn-default') end end def topics_to_show project_topic_list.take(MAX_TOPICS_TO_SHOW) # rubocop: disable CodeReuse/ActiveRecord end def topics_not_shown project_topic_list - topics_to_show end def count_of_extra_topics_not_shown if project_topic_list.count > MAX_TOPICS_TO_SHOW project_topic_list.count - MAX_TOPICS_TO_SHOW else 0 end end def has_extra_topics? count_of_extra_topics_not_shown > 0 end def can_setup_review_app? strong_memoize(:can_setup_review_app) do (can_instantiate_cluster? && all_clusters_empty?) || cicd_missing? end end def all_clusters_empty? strong_memoize(:all_clusters_empty) do project.all_clusters.empty? end end private def integrations_anchor_data return unless can?(current_user, :admin_project, project) label = statistic_icon('settings') + _('Configure Integrations') AnchorData.new(false, label, project_settings_integrations_path(project), nil, nil, nil) end def cicd_missing? current_user && can_current_user_push_code? && repository.gitlab_ci_yml.blank? && !auto_devops_enabled? end def can_instantiate_cluster? current_user && can?(current_user, :create_cluster, project) end def filename_path(filepath) return if filepath.blank? project_blob_path(project, tree_join(default_branch, filepath)) end def anonymous_project_view if !project.empty_repo? && can?(current_user, :read_code, project) 'files' elsif project.wiki_repository_exists? && can?(current_user, :read_wiki, project) 'wiki' elsif can?(current_user, :read_issue, project) 'projects/issues/issues' else 'activity' end end def add_special_file_path(file_name:, commit_message: nil, branch_name: nil, additional_params: {}) commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name } project_new_blob_path( project, default_branch_or_main, file_name: file_name, commit_message: commit_message, branch_name: branch_name, **additional_params ) end def project_topic_list strong_memoize(:project_topic_list) do project.topics.map { |topic| { name: topic.name, title: topic.title_or_name } } end end # Avoid including ActionView::Helpers::UrlHelper def content_tag(*args) ActionController::Base.helpers.content_tag(*args) end end ProjectPresenter.prepend_mod_with('ProjectPresenter')