diff options
32 files changed, 705 insertions, 354 deletions
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index a853624e944..fdcbcc236c1 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -13,40 +13,52 @@ export default class Project { constructor() { const $cloneOptions = $('ul.clone-options-dropdown'); const $projectCloneField = $('#project_clone'); - const $cloneBtnText = $('a.clone-dropdown-btn span'); + const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label'); - const selectedCloneOption = $cloneBtnText.text().trim(); + const selectedCloneOption = $cloneBtnLabel.text().trim(); if (selectedCloneOption.length > 0) { $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active'); } - $('a', $cloneOptions).on('click', (e) => { + $('a', $cloneOptions).on('click', e => { + e.preventDefault(); const $this = $(e.currentTarget); const url = $this.attr('href'); - const activeText = $this.find('.dropdown-menu-inner-title').text(); + const cloneType = $this.data('cloneType'); - e.preventDefault(); + $('.is-active', $cloneOptions).removeClass('is-active'); + $(`a[data-clone-type="${cloneType}"]`).each(function() { + const $el = $(this); + const activeText = $el.find('.dropdown-menu-inner-title').text(); + const $container = $el.closest('.project-clone-holder'); + const $label = $container.find('.js-clone-dropdown-label'); - $('.is-active', $cloneOptions).not($this).removeClass('is-active'); - $this.toggleClass('is-active'); - $projectCloneField.val(url); - $cloneBtnText.text(activeText); + $el.toggleClass('is-active'); + $label.text(activeText); + }); - return $('.clone').text(url); + $projectCloneField.val(url); + $('.js-git-empty .js-clone').text(url); }); // Ref switcher Project.initRefSwitcher(); $('.project-refs-select').on('change', function() { - return $(this).parents('form').submit(); + return $(this) + .parents('form') + .submit(); }); $('.hide-no-ssh-message').on('click', function(e) { Cookies.set('hide_no_ssh_message', 'false'); - $(this).parents('.no-ssh-key-message').remove(); + $(this) + .parents('.no-ssh-key-message') + .remove(); return e.preventDefault(); }); $('.hide-no-password-message').on('click', function(e) { Cookies.set('hide_no_password_message', 'false'); - $(this).parents('.no-password-message').remove(); + $(this) + .parents('.no-password-message') + .remove(); return e.preventDefault(); }); Project.projectSelectDropdown(); @@ -58,7 +70,7 @@ export default class Project { } static changeProject(url) { - return window.location = url; + return (window.location = url); } static initRefSwitcher() { @@ -73,14 +85,15 @@ export default class Project { selected = $dropdown.data('selected'); return $dropdown.glDropdown({ data(term, callback) { - axios.get($dropdown.data('refsUrl'), { - params: { - ref: $dropdown.data('ref'), - search: term, - }, - }) - .then(({ data }) => callback(data)) - .catch(() => flash(__('An error occurred while getting projects'))); + axios + .get($dropdown.data('refsUrl'), { + params: { + ref: $dropdown.data('ref'), + search: term, + }, + }) + .then(({ data }) => callback(data)) + .catch(() => flash(__('An error occurred while getting projects'))); }, selectable: true, filterable: true, diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index b76f2f76449..0507f67843f 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -8,15 +8,18 @@ import BlobViewer from '~/blob/viewer/index'; import Activities from '~/activities'; import { ajaxGet } from '~/lib/utils/common_utils'; import GpgBadges from '~/gpg_badges'; +import initReadMore from '~/read_more'; import Star from '../../../star'; import notificationsDropdown from '../../../notifications_dropdown'; document.addEventListener('DOMContentLoaded', () => { + initReadMore(); new Star(); // eslint-disable-line no-new notificationsDropdown(); new ShortcutsNavigation(); // eslint-disable-line no-new new NotificationsForm(); // eslint-disable-line no-new - new UserCallout({ // eslint-disable-line no-new + // eslint-disable-next-line no-new + new UserCallout({ setCalloutPerProject: false, className: 'js-autodevops-banner', }); diff --git a/app/assets/javascripts/read_more.js b/app/assets/javascripts/read_more.js new file mode 100644 index 00000000000..d2d1ac8c76a --- /dev/null +++ b/app/assets/javascripts/read_more.js @@ -0,0 +1,41 @@ +/** + * ReadMore + * + * Adds "read more" functionality to elements. + * + * Specifically, it looks for a trigger, by default ".js-read-more-trigger", and adds the class + * "is-expanded" to the previous element in order to provide a click to expand functionality. + * + * This is useful for long text elements that you would like to truncate, especially for mobile. + * + * Example Markup + * <div class="read-more-container"> + * <p>Some text that should be long enough to have to truncate within a specified container.</p> + * <p>This text will not appear in the container, as only the first line can be truncated.</p> + * <p>This should also not appear, if everything is working correctly!</p> + * </div> + * <button class="js-read-more-trigger">Read more</button> + * + */ +export default function initReadMore(triggerSelector = '.js-read-more-trigger') { + const triggerEls = document.querySelectorAll(triggerSelector); + + if (!triggerEls) return; + + triggerEls.forEach(triggerEl => { + const targetEl = triggerEl.previousElementSibling; + + if (!targetEl) { + return; + } + + triggerEl.addEventListener( + 'click', + e => { + targetEl.classList.add('is-expanded'); + e.target.remove(); + }, + { once: true }, + ); + }); +} diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index b1a20c06910..39ffabb3ea6 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -64,3 +64,4 @@ @import 'framework/ci_variable_list'; @import 'framework/feature_highlight'; @import 'framework/terms'; +@import 'framework/read_more'; diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 033e5e57177..6d20c46b99d 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -44,12 +44,8 @@ .project-repo-buttons { display: block; - .count-buttons .btn { - margin: 0 10px; - } - - .count-buttons .count-with-arrow { - display: none; + .count-buttons .count-badge { + margin-top: $gl-padding-8; } } } diff --git a/app/assets/stylesheets/framework/read_more.scss b/app/assets/stylesheets/framework/read_more.scss new file mode 100644 index 00000000000..b84b6e0b256 --- /dev/null +++ b/app/assets/stylesheets/framework/read_more.scss @@ -0,0 +1,13 @@ +.read-more-container { + @include media-breakpoint-down(md) { + &:not(.is-expanded) { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + > * { + display: inline; + } + } + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index d76f5cbd9ff..13ad52b62b6 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -271,6 +271,7 @@ $performance-bar-height: 35px; $flash-height: 52px; $context-header-height: 60px; $breadcrumb-min-height: 48px; +$project-title-row-height: 24px; /* * Common component specific colors diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index a95e78931b1..9b7051924e6 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -115,7 +115,7 @@ .project-feature-controls { display: flex; align-items: center; - margin: 8px 0; + margin: $gl-padding-8 0; max-width: 432px; .toggle-wrapper { @@ -144,12 +144,8 @@ .group-home-panel { padding-top: 24px; padding-bottom: 24px; + border-bottom: 1px solid $border-color; - @include media-breakpoint-up(sm) { - border-bottom: 1px solid $border-color; - } - - .project-avatar, .group-avatar { float: none; margin: 0 auto; @@ -175,7 +171,6 @@ } } - .project-home-desc, .group-home-desc { margin-left: auto; margin-right: auto; @@ -199,6 +194,62 @@ } } +.project-home-panel { + padding-top: $gl-padding-8; + padding-bottom: $gl-padding-24; + + .project-title-row { + margin-right: $gl-padding-8; + } + + .project-avatar { + width: $project-title-row-height; + height: $project-title-row-height; + flex-shrink: 0; + flex-basis: $project-title-row-height; + margin: 0 $gl-padding-8 0 0; + } + + .project-title { + font-size: 20px; + line-height: $project-title-row-height; + font-weight: bold; + } + + .project-metadata { + font-weight: normal; + font-size: 14px; + line-height: $gl-btn-line-height; + color: $gl-text-color-secondary; + + .icon { + margin-right: $gl-padding-4; + font-size: 16px; + } + + .project-visibility, + .project-license, + .project-tag-list { + margin-right: $gl-padding-8; + } + + .project-license { + .btn { + line-height: 0; + border-width: 0; + } + } + + .project-tag-list, + .project-license { + .icon { + position: relative; + top: 2px; + } + } + } +} + .nav > .project-repo-buttons { margin-top: 0; } @@ -206,8 +257,6 @@ .project-repo-buttons, .group-buttons { .btn { - padding: 3px 10px; - &:last-child { margin-left: 0; } @@ -222,11 +271,15 @@ .fa-caret-down { margin-left: 3px; + + &.dropdown-btn-icon { + margin-left: 0; + } } } .project-action-button { - margin: 15px 5px 0; + margin: $gl-padding $gl-padding-8 0 0; vertical-align: top; } @@ -243,82 +296,45 @@ .count-buttons { display: inline-block; vertical-align: top; - margin-top: 15px; - } + margin-top: $gl-padding; - .project-clone-holder { - display: inline-block; - margin: 15px 5px 0 0; + .count-badge { + height: $input-height; - input { - height: 28px; + .icon { + top: -1px; + } } - } - .count-with-arrow { - display: inline-block; - position: relative; - margin-left: 4px; + .count-badge-count, + .count-badge-button { + border: 1px solid $border-color; + line-height: 1; + } - .arrow { - &::before { - content: ''; - display: inline-block; - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - top: 50%; - left: 0; - margin-top: -6px; - border-width: 7px 5px 7px 0; - border-right-color: $count-arrow-border; - pointer-events: none; - } + .count, + .count-badge-button { + color: $gl-text-color; + } - &::after { - content: ''; - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - top: 50%; - left: 1px; - margin-top: -9px; - border-width: 10px 7px 10px 0; - border-right-color: $white-light; - pointer-events: none; - } + .count-badge-count { + padding: 0 12px; + border-right: 0; + border-radius: $border-radius-base 0 0 $border-radius-base; + background: $gray-light; } - .count { - @include btn-white; - display: inline-block; - background: $white-light; - border-radius: 2px; - border-width: 1px; - border-style: solid; - font-size: 13px; - font-weight: $gl-font-weight-bold; - line-height: 13px; - letter-spacing: 0.4px; - padding: 6px 14px; - text-align: center; - vertical-align: middle; - touch-action: manipulation; - background-image: none; - white-space: nowrap; - margin: 0 10px 0 4px; + .count-badge-button { + border-radius: 0 $border-radius-base $border-radius-base 0; + } + } - a { - color: inherit; - } + .project-clone-holder { + display: inline-block; + margin: $gl-padding $gl-padding-8 0 0; - &:hover { - background: $white-light; - } + input { + height: $input-height; } } @@ -333,6 +349,14 @@ min-width: 320px; } } + + .mobile-git-clone { + margin-top: $gl-padding-8; + + .dropdown-menu-inner-content { + @extend .monospace; + } + } } .split-one { @@ -511,7 +535,6 @@ .controls { margin-left: auto; } - } .choose-template { @@ -574,7 +597,7 @@ flex-wrap: wrap; .btn { - padding: 8px; + padding: $gl-padding-8; margin-right: 10px; } @@ -651,7 +674,7 @@ left: -10px; top: 50%; z-index: 10; - padding: 8px 0; + padding: $gl-padding-8 0; text-align: center; background-color: $white-light; color: $gl-text-color-tertiary; @@ -665,7 +688,7 @@ left: 50%; top: 0; transform: translateX(-50%); - padding: 0 8px; + padding: 0 $gl-padding-8; } } @@ -699,17 +722,51 @@ .project-stats { font-size: 0; text-align: center; - max-width: 100%; border-bottom: 1px solid $border-color; - .nav { - margin-top: $gl-padding-8; - margin-bottom: $gl-padding-8; + .scrolling-tabs-container { + .scrolling-tabs { + margin-top: $gl-padding-8; + margin-bottom: $gl-padding-8; + flex-wrap: wrap; + border-bottom: 0; + } + .fade-left, + .fade-right { + top: 0; + height: 100%; + + .fa { + top: 50%; + margin-top: -$gl-padding-8; + } + } + + .nav { + flex-basis: 100%; + + + .nav { + margin: $gl-padding-8 0; + } + } + + @include media-breakpoint-down(md) { + flex-direction: column; + + .nav { + flex-wrap: nowrap; + } + + .nav:first-child { + margin-right: $gl-padding-8; + } + } + } + + .nav { > li { display: inline-block; - margin-top: $gl-padding-4; - margin-bottom: $gl-padding-4; &:not(:last-child) { margin-right: $gl-padding; @@ -732,13 +789,17 @@ font-size: $gl-font-size; line-height: $gl-btn-line-height; color: $gl-text-color-secondary; + white-space: nowrap; } .stat-link { + border-bottom: 0; + &:hover, &:focus { color: $gl-text-color; text-decoration: underline; + border-bottom: 0; } } @@ -868,7 +929,7 @@ pre.light-well { } .git-clone-holder { - width: 380px; + width: 320px; .btn-clipboard { border: 1px solid $border-color; diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 26e3850a540..2b3fe57767c 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -61,7 +61,7 @@ module ButtonHelper dropdown_description = http_dropdown_description(protocol) append_url = project.http_url_to_repo if append_link - dropdown_item_with_description(protocol, dropdown_description, href: append_url) + dropdown_item_with_description(protocol, dropdown_description, href: append_url, data: { clone_type: 'http' }) end def http_dropdown_description(protocol) @@ -80,16 +80,17 @@ module ButtonHelper append_url = project.ssh_url_to_repo if append_link - dropdown_item_with_description('SSH', dropdown_description, href: append_url) + dropdown_item_with_description('SSH', dropdown_description, href: append_url, data: { clone_type: 'ssh' }) end - def dropdown_item_with_description(title, description, href: nil) + def dropdown_item_with_description(title, description, href: nil, data: nil) button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title') button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description content_tag (href ? :a : :span), (href ? button_content : title), class: "#{title.downcase}-selector", - href: (href if href) + href: (href if href), + data: (data if data) end end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index a8a10c98d69..a5612372aa6 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -86,7 +86,7 @@ module IconsHelper end end - def visibility_level_icon(level, fw: true) + def visibility_level_icon(level, fw: true, options: {}) name = case level when Gitlab::VisibilityLevel::PRIVATE @@ -99,7 +99,7 @@ module IconsHelper name << " fw" if fw - icon(name) + icon(name, options) end def file_type_icon_class(type, mode, name) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 18b3badda8d..e0080b6660a 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -351,6 +351,10 @@ module ProjectsHelper end end + def default_clone_label + _("Copy %{protocol} clone URL") % { protocol: default_clone_protocol.upcase } + end + def default_clone_protocol if allowed_protocols_present? enabled_protocol diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index cf2fe5a2019..7b64869c9ea 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -138,7 +138,7 @@ module VisibilityLevelHelper end def project_visibility_icon_description(level) - "#{visibility_level_label(level)} - #{project_visibility_level_description(level)}" + "#{project_visibility_level_description(level)}" end def visibility_level_label(level) diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index 4c2f33213d6..6a54054badc 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -11,16 +11,18 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated presents :project + AnchorData = Struct.new(:enabled, :label, :link, :class_modifier) + MAX_TAGS_TO_SHOW = 3 + def statistics_anchors(show_auto_devops_callout:) [ + readme_anchor_data, + changelog_anchor_data, + contribution_guide_anchor_data, files_anchor_data, commits_anchor_data, branches_anchor_data, tags_anchor_data, - readme_anchor_data, - changelog_anchor_data, - license_anchor_data, - contribution_guide_anchor_data, gitlab_ci_anchor_data, autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout), kubernetes_cluster_anchor_data @@ -31,7 +33,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated [ readme_anchor_data, changelog_anchor_data, - license_anchor_data, contribution_guide_anchor_data, autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout), kubernetes_cluster_anchor_data, @@ -42,6 +43,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated def empty_repo_statistics_anchors [ + files_anchor_data, + commits_anchor_data, + branches_anchor_data, + tags_anchor_data, autodevops_anchor_data, kubernetes_cluster_anchor_data ].compact.select { |item| item.enabled } @@ -51,7 +56,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated [ new_file_anchor_data, readme_anchor_data, - license_anchor_data, autodevops_anchor_data, kubernetes_cluster_anchor_data ].compact.reject { |item| item.enabled } @@ -182,95 +186,101 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def files_anchor_data - OpenStruct.new(enabled: true, - label: _('Files (%{human_size})') % { human_size: storage_counter(statistics.total_repository_size) }, - link: project_tree_path(project)) + AnchorData.new(true, + _('Files (%{human_size})') % { human_size: storage_counter(statistics.total_repository_size) }, + empty_repo? ? nil : project_tree_path(project)) end def commits_anchor_data - OpenStruct.new(enabled: true, - label: n_('Commit (%{commit_count})', 'Commits (%{commit_count})', statistics.commit_count) % { commit_count: number_with_delimiter(statistics.commit_count) }, - link: project_commits_path(project, repository.root_ref)) + AnchorData.new(true, + n_('Commit (%{commit_count})', 'Commits (%{commit_count})', statistics.commit_count) % { commit_count: number_with_delimiter(statistics.commit_count) }, + empty_repo? ? nil : project_commits_path(project, repository.root_ref)) end def branches_anchor_data - OpenStruct.new(enabled: true, - label: n_('Branch (%{branch_count})', 'Branches (%{branch_count})', repository.branch_count) % { branch_count: number_with_delimiter(repository.branch_count) }, - link: project_branches_path(project)) + AnchorData.new(true, + n_('Branch (%{branch_count})', 'Branches (%{branch_count})', repository.branch_count) % { branch_count: number_with_delimiter(repository.branch_count) }, + empty_repo? ? nil : project_branches_path(project)) end def tags_anchor_data - OpenStruct.new(enabled: true, - label: n_('Tag (%{tag_count})', 'Tags (%{tag_count})', repository.tag_count) % { tag_count: number_with_delimiter(repository.tag_count) }, - link: project_tags_path(project)) + AnchorData.new(true, + n_('Tag (%{tag_count})', 'Tags (%{tag_count})', repository.tag_count) % { tag_count: number_with_delimiter(repository.tag_count) }, + empty_repo? ? nil : project_tags_path(project)) end def new_file_anchor_data if current_user && can_current_user_push_to_default_branch? - OpenStruct.new(enabled: false, - label: _('New file'), - link: project_new_blob_path(project, default_branch || 'master'), - class_modifier: 'new') + AnchorData.new(false, + _('New file'), + project_new_blob_path(project, default_branch || 'master'), + 'new') end end def readme_anchor_data if current_user && can_current_user_push_to_default_branch? && repository.readme.nil? - OpenStruct.new(enabled: false, - label: _('Add Readme'), - link: add_readme_path) + AnchorData.new(false, + _('Add Readme'), + add_readme_path) elsif repository.readme - OpenStruct.new(enabled: true, - label: _('Readme'), - link: default_view != 'readme' ? readme_path : '#readme') + AnchorData.new(true, + _('Readme'), + default_view != 'readme' ? readme_path : '#readme') end end def changelog_anchor_data if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank? - OpenStruct.new(enabled: false, - label: _('Add Changelog'), - link: add_changelog_path) + AnchorData.new(false, + _('Add Changelog'), + add_changelog_path) elsif repository.changelog.present? - OpenStruct.new(enabled: true, - label: _('Changelog'), - link: changelog_path) + AnchorData.new(true, + _('Changelog'), + changelog_path) end end def license_anchor_data - if current_user && can_current_user_push_to_default_branch? && repository.license_blob.blank? - OpenStruct.new(enabled: false, - label: _('Add License'), - link: add_license_path) - elsif repository.license_blob.present? - OpenStruct.new(enabled: true, - label: license_short_name, - link: license_path) + if repository.license_blob.present? + AnchorData.new(true, + license_short_name, + license_path) + else + if current_user && can_current_user_push_to_default_branch? + AnchorData.new(false, + _('Add license'), + add_license_path) + else + AnchorData.new(false, + _('No license. All rights reserved'), + nil) + end end end def contribution_guide_anchor_data if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank? - OpenStruct.new(enabled: false, - label: _('Add Contribution guide'), - link: add_contribution_guide_path) + AnchorData.new(false, + _('Add Contribution guide'), + add_contribution_guide_path) elsif repository.contribution_guide.present? - OpenStruct.new(enabled: true, - label: _('Contribution guide'), - link: contribution_guide_path) + AnchorData.new(true, + _('Contribution guide'), + contribution_guide_path) end end def autodevops_anchor_data(show_auto_devops_callout: false) if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout - OpenStruct.new(enabled: auto_devops_enabled?, - label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'), - link: project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) + AnchorData.new(auto_devops_enabled?, + auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'), + project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) elsif auto_devops_enabled? - OpenStruct.new(enabled: true, - label: _('Auto DevOps enabled'), - link: nil) + AnchorData.new(true, + _('Auto DevOps enabled'), + nil) end end @@ -282,32 +292,48 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated cluster_link = new_project_cluster_path(project) end - OpenStruct.new(enabled: !clusters.empty?, - label: clusters.empty? ? _('Add Kubernetes cluster') : _('Kubernetes configured'), - link: cluster_link) + AnchorData.new(!clusters.empty?, + clusters.empty? ? _('Add Kubernetes cluster') : _('Kubernetes configured'), + cluster_link) end end def gitlab_ci_anchor_data if current_user && can_current_user_push_code? && repository.gitlab_ci_yml.blank? && !auto_devops_enabled? - OpenStruct.new(enabled: false, - label: _('Set up CI/CD'), - link: add_ci_yml_path) + AnchorData.new(false, + _('Set up CI/CD'), + add_ci_yml_path) elsif repository.gitlab_ci_yml.present? - OpenStruct.new(enabled: true, - label: _('CI/CD configuration'), - link: ci_configuration_path) + AnchorData.new(true, + _('CI/CD configuration'), + ci_configuration_path) end end def koding_anchor_data if current_user && can_current_user_push_code? && koding_enabled? && repository.koding_yml.blank? - OpenStruct.new(enabled: false, - label: _('Set up Koding'), - link: add_koding_stack_path) + AnchorData.new(false, + _('Set up Koding'), + add_koding_stack_path) end end + def tags_to_show + project.tag_list.take(MAX_TAGS_TO_SHOW) + end + + def count_of_extra_tags_not_shown + if project.tag_list.count > MAX_TAGS_TO_SHOW + project.tag_list.count - MAX_TAGS_TO_SHOW + else + 0 + end + end + + def has_extra_tags? + count_of_extra_tags_not_shown > 0 + end + private def filename_path(filename) diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 1b6c4193c4d..ced6a2a0399 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,16 +1,35 @@ - empty_repo = @project.empty_repo? -.project-home-panel.text-center{ class: ("empty-project" if empty_repo) } +- license = @project.license_anchor_data +.project-home-panel{ class: ("empty-project" if empty_repo) } .limit-container-width{ class: container_class } - .avatar-container.s70.project-avatar - = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile', width: 70, height: 70) - %h1.project-title.qa-project-name - = @project.name - %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } - = visibility_level_icon(@project.visibility_level, fw: false) + .project-header.d-flex.flex-row.flex-wrap.align-items-center.append-bottom-8 + .project-title-row.d-flex.align-items-center + .avatar-container.project-avatar.float-none + = project_icon(@project, alt: @project.name, class: 'avatar avatar-tile') + %h1.project-title.d-flex.align-items-baseline.qa-project-name + = @project.name + .project-metadata.d-flex.flex-row.flex-wrap.align-items-baseline + .project-visibility.d-inline-flex.align-items-baseline.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } + = visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'}) + = visibility_level_label(@project.visibility_level) + - if license.present? + .project-license.d-inline-flex.align-items-baseline + = link_to_if license.link, sprite_icon('scale', size: 16, css_class: 'icon') + license.label, license.link, class: license.enabled ? 'btn btn-link btn-secondary-hover-link' : 'btn btn-link' + - if @project.tag_list.present? + .project-tag-list.d-inline-flex.align-items-baseline.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_tags? ? @project.tag_list.join(', ') : nil } + = sprite_icon('tag', size: 16, css_class: 'icon') + = @project.tags_to_show + - if @project.has_extra_tags? + = _("+ %{count} more") % { count: @project.count_of_extra_tags_not_shown } .project-home-desc - if @project.description.present? - = markdown_field(@project, :description) + .project-description + .project-description-markdown.read-more-container + = markdown_field(@project, :description) + %button.btn.btn-blank.btn-link.text-secondary.js-read-more-trigger.text-secondary.d-lg-none{ type: "button" } + = _("Read more") + - if can?(current_user, :read_project, @project) .text-secondary.prepend-top-8 = s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id } @@ -25,34 +44,42 @@ - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)') = deleted_message % { project_name: fork_source_name(@project) } - .project-badges.prepend-top-default.append-bottom-default - - @project.badges.each do |badge| - %a.append-right-8{ href: badge.rendered_link_url(@project), - target: '_blank', - rel: 'noopener noreferrer' }> - %img.project-badge{ src: badge.rendered_image_url(@project), - 'aria-hidden': true, - alt: '' }> - - .project-repo-buttons - .count-buttons + - if @project.badges.present? + .project-badges.prepend-top-default.append-bottom-default + - @project.badges.each do |badge| + %a.append-right-8{ href: badge.rendered_link_url(@project), + target: '_blank', + rel: 'noopener noreferrer' }> + %img.project-badge{ src: badge.rendered_image_url(@project), + 'aria-hidden': true, + alt: 'Project badge' }> + + .project-repo-buttons.d-inline-flex.flex-wrap + .count-buttons.d-inline-flex = render 'projects/buttons/star' = render 'projects/buttons/fork' - %span.d-none.d-sm-inline - - if can?(current_user, :download_code, @project) - .project-clone-holder - = render "shared/clone_panel" + - if can?(current_user, :download_code, @project) + .project-clone-holder.d-inline-flex.d-sm-none + = render "shared/mobile_clone_panel" - - if show_xcode_link?(@project) - .project-action-button.project-xcode.inline - = render "projects/buttons/xcode_link" + .project-clone-holder.d-none.d-sm-inline-flex + = render "shared/clone_panel" - - if current_user - - if can?(current_user, :download_code, @project) + - if show_xcode_link?(@project) + .project-action-button.project-xcode.inline + = render "projects/buttons/xcode_link" + + - if current_user + - if can?(current_user, :download_code, @project) + .d-none.d-sm-inline-flex = render 'projects/buttons/download', project: @project, ref: @ref + .d-none.d-sm-inline-flex = render 'projects/buttons/dropdown' + .d-none.d-sm-inline-flex = render 'projects/buttons/koding' + .d-none.d-sm-inline-flex = render 'shared/notifications/button', notification_setting: @notification_setting + .d-none.d-sm-inline-flex = render 'shared/members/access_request_buttons', source: @project diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml index 15ec58289e3..4cf49f3cf62 100644 --- a/app/views/projects/_stat_anchor_list.html.haml +++ b/app/views/projects/_stat_anchor_list.html.haml @@ -1,7 +1,7 @@ - anchors = local_assigns.fetch(:anchors, []) - return unless anchors.any? -%ul.nav.justify-content-center +%ul.nav - anchors.each do |anchor| %li.nav-item = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'nav-link stat-link' : "nav-link btn btn-#{anchor.class_modifier || 'missing'}" do diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index f880556a9f7..8da27ca7cb3 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -1,17 +1,17 @@ - unless @project.empty_repo? - if current_user && can?(current_user, :fork_project, @project) - - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: _('Go to your fork'), class: 'btn has-tooltip' do - = custom_icon('icon_fork') - %span= s_('GoToYourFork|Fork') - - else - - can_create_fork = current_user.can?(:create_fork) - = link_to new_project_fork_path(@project), - class: "btn btn-default #{'has-tooltip disabled' unless can_create_fork}", - title: (_('You have reached your project limit') unless can_create_fork) do - = custom_icon('icon_fork') - %span= s_('CreateNewFork|Fork') - .count-with-arrow - %span.arrow - = link_to project_forks_path(@project), title: n_('Fork', 'Forks', @project.forks_count), class: 'count' do - = @project.forks_count + .count-badge.d-inline-flex.align-item-stretch.append-right-8 + %span.fork-count.count-badge-count.d-flex.align-items-center + = link_to project_forks_path(@project), title: n_(s_('ProjectOverview|Fork'), s_('ProjectOverview|Forks'), @project.forks_count), class: 'count' do + = @project.forks_count + - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: s_('ProjectOverview|Go to your fork'), class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn' do + = sprite_icon('fork', { css_class: 'icon' }) + %span= s_('ProjectOverview|Fork') + - else + - can_create_fork = current_user.can?(:create_fork) + = link_to new_project_fork_path(@project), + class: "btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn #{'has-tooltip disabled' unless can_create_fork}", + title: (s_('ProjectOverview|You have reached your project limit') unless can_create_fork) do + = sprite_icon('fork', { css_class: 'icon' }) + %span= s_('ProjectOverview|Fork') diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index a2dc2730ecc..0d04ecb3a58 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -1,21 +1,19 @@ - if current_user - %button.btn.btn-default.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } }> - - if current_user.starred?(@project) - = sprite_icon('star') - %span.starred= _('Unstar') - - else - = sprite_icon('star-o') - %span= s_('StarProject|Star') - .count-with-arrow - %span.arrow - %span.count.star-count + .count-badge.d-inline-flex.align-item-stretch.append-right-8 + %span.star-count.count-badge-count.d-flex.align-items-center = @project.star_count + %button.count-badge-button.btn.btn-default.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } } + - if current_user.starred?(@project) + = sprite_icon('star', { css_class: 'icon' }) + %span.starred= s_('ProjectOverview|Unstar') + - else + = sprite_icon('star-o', { css_class: 'icon' }) + %span= s_('ProjectOverview|Star') - else - = link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: _('You must sign in to star a project') do - = sprite_icon('star') - #{ s_('StarProject|Star') } - .count-with-arrow - %span.arrow - %span.count + .count-badge.d-inline-flex.align-item-stretch.append-right-8 + %span.star-count.count-badge-count.d-flex.align-items-center = @project.star_count + = link_to new_user_session_path, class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do + = sprite_icon('star-o', { css_class: 'icon' }) + %span= s_('ProjectOverview|Star') diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index d47dc3d8143..d104608b2fe 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -32,9 +32,13 @@ = _('Otherwise it is recommended you start with one of the options below.') .prepend-top-20 -%nav.project-stats{ class: container_class } - = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors - = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons +%nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] } + .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + .nav-links.scrolling-tabs + = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors + = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons - if can?(current_user, :push_code, @project) %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } @@ -42,7 +46,7 @@ .empty_wrapper %h3#repo-command-line-instructions.page-title-empty Command line instructions - .git-empty + .git-empty.js-git-empty %fieldset %h5 Git global setup %pre.bg-light @@ -54,7 +58,7 @@ %h5 Create a new repository %pre.bg-light :preserve - git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')} cd #{h @project.path} touch README.md git add README.md @@ -69,7 +73,7 @@ :preserve cd existing_folder git init - git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')} git add . git commit -m "Initial commit" - if @project.can_current_user_push_to_default_branch? @@ -82,7 +86,7 @@ :preserve cd existing_repo git remote rename origin old-origin - git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')} - if @project.can_current_user_push_to_default_branch? %span>< git push -u origin --all diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index df8a5742450..aba289c790f 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -19,8 +19,13 @@ - if can?(current_user, :download_code, @project) %nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] } - = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) - = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) + .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + .nav-links.scrolling-tabs + = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) + = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) + = repository_languages_bar(@project.repository_languages) %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 3655c2a1d42..a2df0347fd6 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -1,14 +1,14 @@ - project = project || @project -.git-clone-holder.input-group +.git-clone-holder.js-git-clone-holder.input-group .input-group-prepend - if allowed_protocols_present? .input-group-text.clone-dropdown-btn.btn - %span + %span.js-clone-dropdown-label = enabled_project_button(project, enabled_protocol) - else %a#clone-dropdown.input-group-text.btn.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } } - %span + %span.js-clone-dropdown-label = default_clone_protocol.upcase = icon('caret-down') %ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown diff --git a/app/views/shared/_mobile_clone_panel.html.haml b/app/views/shared/_mobile_clone_panel.html.haml new file mode 100644 index 00000000000..998985cabe1 --- /dev/null +++ b/app/views/shared/_mobile_clone_panel.html.haml @@ -0,0 +1,13 @@ +- project = project || @project +- ssh_copy_label = _("Copy SSH clone URL") +- http_copy_label = _("Copy HTTPS clone URL") + +.btn-group.mobile-git-clone.js-mobile-git-clone + = clipboard_button(button_text: default_clone_label, target: '#project_clone', hide_button_icon: true, class: "input-group-text clone-dropdown-btn js-clone-dropdown-label btn btn-default") + %button.btn.btn-default.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { toggle: "dropdown" } } + = icon("caret-down", class: "dropdown-btn-icon") + %ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown{ data: { dropdown: true } } + %li + = dropdown_item_with_description(ssh_copy_label, project.ssh_url_to_repo, href: project.ssh_url_to_repo, data: { clone_type: 'ssh' }) + %li + = dropdown_item_with_description(http_copy_label, project.http_url_to_repo, href: project.http_url_to_repo, data: { clone_type: 'http' }) diff --git a/changelogs/unreleased/44704-improve-project-overview-ui.yml b/changelogs/unreleased/44704-improve-project-overview-ui.yml new file mode 100644 index 00000000000..6fb8359f2dc --- /dev/null +++ b/changelogs/unreleased/44704-improve-project-overview-ui.yml @@ -0,0 +1,5 @@ +--- +title: Update design of project overview page +merge_request: 20536 +author: +type: changed diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7d0bd01142c..2b7fe643d95 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -150,6 +150,9 @@ msgstr "" msgid "%{unstaged} unstaged and %{staged} staged changes" msgstr "" +msgid "+ %{count} more" +msgstr "" + msgid "+ %{moreCount} more" msgstr "" @@ -319,10 +322,10 @@ msgstr "" msgid "Add Kubernetes cluster" msgstr "" -msgid "Add License" +msgid "Add Readme" msgstr "" -msgid "Add Readme" +msgid "Add license" msgstr "" msgid "Add new application" @@ -1897,6 +1900,15 @@ msgstr "" msgid "ConvDev Index" msgstr "" +msgid "Copy %{protocol} clone URL" +msgstr "" + +msgid "Copy HTTPS clone URL" +msgstr "" + +msgid "Copy SSH clone URL" +msgstr "" + msgid "Copy URL to clipboard" msgstr "" @@ -1990,9 +2002,6 @@ msgstr "" msgid "Create project label" msgstr "" -msgid "CreateNewFork|Fork" -msgstr "" - msgid "CreateTag|Tag" msgstr "" @@ -2730,11 +2739,6 @@ msgstr "" msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)" msgstr "" -msgid "Fork" -msgid_plural "Forks" -msgstr[0] "" -msgstr[1] "" - msgid "ForkedFromProjectPath|Forked from" msgstr "" @@ -2858,12 +2862,6 @@ msgstr "" msgid "Go to %{link_to_google_takeout}." msgstr "" -msgid "Go to your fork" -msgstr "" - -msgid "GoToYourFork|Fork" -msgstr "" - msgid "Google Code import" msgstr "" @@ -3895,6 +3893,9 @@ msgstr "" msgid "No labels with such name or description" msgstr "" +msgid "No license. All rights reserved" +msgstr "" + msgid "No merge requests found" msgstr "" @@ -4554,6 +4555,27 @@ msgstr "" msgid "ProjectLifecycle|Stage" msgstr "" +msgid "ProjectOverview|Fork" +msgstr "" + +msgid "ProjectOverview|Forks" +msgstr "" + +msgid "ProjectOverview|Go to your fork" +msgstr "" + +msgid "ProjectOverview|Star" +msgstr "" + +msgid "ProjectOverview|Unstar" +msgstr "" + +msgid "ProjectOverview|You have reached your project limit" +msgstr "" + +msgid "ProjectOverview|You must sign in to star a project" +msgstr "" + msgid "ProjectPage|Project ID: %{project_id}" msgstr "" @@ -6516,9 +6538,6 @@ msgstr "" msgid "You must have maintainer access to force delete a lock" msgstr "" -msgid "You must sign in to star a project" -msgstr "" - msgid "You need permission." msgstr "" diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 07b4d0b745d..267e7bbc249 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -23,7 +23,7 @@ module QA end view 'app/views/projects/buttons/_fork.html.haml' do - element :fork_label, "%span= s_('GoToYourFork|Fork')" + element :fork_label, "%span= s_('ProjectOverview|Fork')" element :fork_link, "link_to new_project_fork_path(@project)" end @@ -32,7 +32,7 @@ module QA end view 'app/presenters/project_presenter.rb' do - element :new_file_button, "label: _('New file')," + element :new_file_button, "_('New file')," end def project_name diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index ac6c8c337fa..6762460971f 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -36,7 +36,7 @@ describe 'Projects > Files > Project owner creates a license file', :js do end it 'project maintainer creates a license file from the "Add license" link' do - click_link 'Add License' + click_link 'Add license' expect(page).to have_content('New file') expect(current_path).to eq( diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index 801291c1f77..0b8474fb87a 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -10,7 +10,7 @@ describe 'Projects > Files > Project owner sees a link to create a license file it 'project maintainer creates a license file from a template' do visit project_path(project) - click_on 'Add License' + click_on 'Add license' expect(page).to have_content('New file') expect(current_path).to eq( diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb index 0405e21a0d7..b8326edd4fd 100644 --- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb +++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'Projects > Show > User sees setup shortcut buttons' do - # For "New file", "Add License" functionality, + # For "New file", "Add license" functionality, # see spec/features/projects/files/project_owner_creates_license_file_spec.rb # see spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -58,9 +58,9 @@ describe 'Projects > Show > User sees setup shortcut buttons' do end end - it '"Add License" button linked to new file populated for a license' do - page.within('.project-stats') do - expect(page).to have_link('Add License', href: presenter.add_license_path) + it '"Add license" button linked to new file populated for a license' do + page.within('.project-metadata') do + expect(page).to have_link('Add license', href: presenter.add_license_path) end end @@ -201,13 +201,13 @@ describe 'Projects > Show > User sees setup shortcut buttons' do end end - it 'no "Add License" button if the project already has a license' do + it 'no "Add license" button if the project already has a license' do visit project_path(project) expect(project.repository.license_blob).not_to be_nil page.within('.project-stats') do - expect(page).not_to have_link('Add License') + expect(page).not_to have_link('Add license') end end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 56ed0c936a6..22e3a99072f 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe 'Project' do include ProjectForksHelper + include MobileHelpers describe 'creating from template' do let(:user) { create(:user) } @@ -54,25 +55,72 @@ describe 'Project' do it 'parses Markdown' do project.update_attribute(:description, 'This is **my** project') visit path - expect(page).to have_css('.project-home-desc > p > strong') + expect(page).to have_css('.project-description > .project-description-markdown > p > strong') end it 'passes through html-pipeline' do project.update_attribute(:description, 'This project is the :poop:') visit path - expect(page).to have_css('.project-home-desc > p > gl-emoji') + expect(page).to have_css('.project-description > .project-description-markdown > p > gl-emoji') end it 'sanitizes unwanted tags' do project.update_attribute(:description, "```\ncode\n```") visit path - expect(page).not_to have_css('.project-home-desc code') + expect(page).not_to have_css('.project-description code') end it 'permits `rel` attribute on links' do project.update_attribute(:description, 'https://google.com/') visit path - expect(page).to have_css('.project-home-desc a[rel]') + expect(page).to have_css('.project-description a[rel]') + end + + context 'read more', :js do + let(:read_more_selector) { '.read-more-container' } + let(:read_more_trigger_selector) { '.project-home-desc .js-read-more-trigger' } + + it 'does not display "read more" link on desktop breakpoint' do + project.update_attribute(:description, 'This is **my** project') + visit path + + expect(find(read_more_trigger_selector, visible: false)).not_to be_visible + end + + it 'displays "read more" link on mobile breakpoint' do + project.update_attribute(:description, 'This is **my** project') + visit path + resize_screen_xs + + find(read_more_trigger_selector).click + + expect(page).to have_css('.project-description .is-expanded') + end + end + end + + describe 'copy clone URL to clipboard', :js do + let(:project) { create(:project, :repository) } + let(:path) { project_path(project) } + + before do + sign_in(create(:admin)) + visit path + end + + context 'desktop component' do + it 'shows on md and larger breakpoints' do + expect(find('.git-clone-holder')).to be_visible + expect(find('.mobile-git-clone', visible: false)).not_to be_visible + end + end + + context 'mobile component' do + it 'shows mobile component on sm and smaller breakpoints' do + resize_screen_xs + expect(find('.mobile-git-clone')).to be_visible + expect(find('.git-clone-holder', visible: false)).not_to be_visible + end end end diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb index 57c78182abc..d98f7f55b20 100644 --- a/spec/javascripts/fixtures/projects.rb +++ b/spec/javascripts/fixtures/projects.rb @@ -6,6 +6,7 @@ describe 'Projects (JavaScript fixtures)', type: :controller do let(:admin) { create(:admin) } let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project, namespace: namespace, path: 'builds-project') } + let(:project_with_repo) { create(:project, :repository, description: 'Code and stuff') } let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2') } let!(:variable1) { create(:ci_variable, project: project_variable_populated) } let!(:variable2) { create(:ci_variable, project: project_variable_populated) } @@ -35,6 +36,15 @@ describe 'Projects (JavaScript fixtures)', type: :controller do store_frontend_fixture(response, example.description) end + it 'projects/overview.html.raw' do |example| + get :show, + namespace_id: project_with_repo.namespace.to_param, + id: project_with_repo + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + it 'projects/edit.html.raw' do |example| get :edit, namespace_id: project.namespace.to_param, diff --git a/spec/javascripts/read_more_spec.js b/spec/javascripts/read_more_spec.js new file mode 100644 index 00000000000..b1af0f80a50 --- /dev/null +++ b/spec/javascripts/read_more_spec.js @@ -0,0 +1,23 @@ +import initReadMore from '~/read_more'; + +describe('Read more click-to-expand functionality', () => { + const fixtureName = 'projects/overview.html.raw'; + + preloadFixtures(fixtureName); + + beforeEach(() => { + loadFixtures(fixtureName); + }); + + describe('expands target element', () => { + it('adds "is-expanded" class to target element', () => { + const target = document.querySelector('.read-more-container'); + const trigger = document.querySelector('.js-read-more-trigger'); + initReadMore(); + + trigger.click(); + + expect(target.classList.contains('is-expanded')).toEqual(true); + }); + }); +}); diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb index 01085dbcb49..d9fb27e101e 100644 --- a/spec/presenters/project_presenter_spec.rb +++ b/spec/presenters/project_presenter_spec.rb @@ -159,39 +159,76 @@ describe ProjectPresenter do end end + context 'statistics anchors (empty repo)' do + let(:project) { create(:project, :empty_repo) } + let(:presenter) { described_class.new(project, current_user: user) } + + describe '#files_anchor_data' do + it 'returns files data' do + expect(presenter.files_anchor_data).to have_attributes(enabled: true, + label: 'Files (0 Bytes)', + link: nil) + end + end + + describe '#commits_anchor_data' do + it 'returns commits data' do + expect(presenter.commits_anchor_data).to have_attributes(enabled: true, + label: 'Commits (0)', + link: nil) + end + end + + describe '#branches_anchor_data' do + it 'returns branches data' do + expect(presenter.branches_anchor_data).to have_attributes(enabled: true, + label: "Branches (0)", + link: nil) + end + end + + describe '#tags_anchor_data' do + it 'returns tags data' do + expect(presenter.tags_anchor_data).to have_attributes(enabled: true, + label: "Tags (0)", + link: nil) + end + end + end + context 'statistics anchors' do let(:project) { create(:project, :repository) } let(:presenter) { described_class.new(project, current_user: user) } describe '#files_anchor_data' do it 'returns files data' do - expect(presenter.files_anchor_data).to eq(OpenStruct.new(enabled: true, - label: 'Files (0 Bytes)', - link: presenter.project_tree_path(project))) + expect(presenter.files_anchor_data).to have_attributes(enabled: true, + label: 'Files (0 Bytes)', + link: presenter.project_tree_path(project)) end end describe '#commits_anchor_data' do it 'returns commits data' do - expect(presenter.commits_anchor_data).to eq(OpenStruct.new(enabled: true, - label: 'Commits (0)', - link: presenter.project_commits_path(project, project.repository.root_ref))) + expect(presenter.commits_anchor_data).to have_attributes(enabled: true, + label: 'Commits (0)', + link: presenter.project_commits_path(project, project.repository.root_ref)) end end describe '#branches_anchor_data' do it 'returns branches data' do - expect(presenter.branches_anchor_data).to eq(OpenStruct.new(enabled: true, - label: "Branches (#{project.repository.branches.size})", - link: presenter.project_branches_path(project))) + expect(presenter.branches_anchor_data).to have_attributes(enabled: true, + label: "Branches (#{project.repository.branches.size})", + link: presenter.project_branches_path(project)) end end describe '#tags_anchor_data' do it 'returns tags data' do - expect(presenter.tags_anchor_data).to eq(OpenStruct.new(enabled: true, - label: "Tags (#{project.repository.tags.size})", - link: presenter.project_tags_path(project))) + expect(presenter.tags_anchor_data).to have_attributes(enabled: true, + label: "Tags (#{project.repository.tags.size})", + link: presenter.project_tags_path(project)) end end @@ -199,10 +236,10 @@ describe ProjectPresenter do it 'returns new file data if user can push' do project.add_developer(user) - expect(presenter.new_file_anchor_data).to eq(OpenStruct.new(enabled: false, - label: "New file", - link: presenter.project_new_blob_path(project, 'master'), - class_modifier: 'new')) + expect(presenter.new_file_anchor_data).to have_attributes(enabled: false, + label: "New file", + link: presenter.project_new_blob_path(project, 'master'), + class_modifier: 'new') end it 'returns nil if user cannot push' do @@ -227,9 +264,9 @@ describe ProjectPresenter do project.add_developer(user) allow(project.repository).to receive(:readme).and_return(nil) - expect(presenter.readme_anchor_data).to eq(OpenStruct.new(enabled: false, - label: 'Add Readme', - link: presenter.add_readme_path)) + expect(presenter.readme_anchor_data).to have_attributes(enabled: false, + label: 'Add Readme', + link: presenter.add_readme_path) end end @@ -237,9 +274,9 @@ describe ProjectPresenter do it 'returns anchor data' do allow(project.repository).to receive(:readme).and_return(double(name: 'readme')) - expect(presenter.readme_anchor_data).to eq(OpenStruct.new(enabled: true, - label: 'Readme', - link: presenter.readme_path)) + expect(presenter.readme_anchor_data).to have_attributes(enabled: true, + label: 'Readme', + link: presenter.readme_path) end end end @@ -250,9 +287,9 @@ describe ProjectPresenter do project.add_developer(user) allow(project.repository).to receive(:changelog).and_return(nil) - expect(presenter.changelog_anchor_data).to eq(OpenStruct.new(enabled: false, - label: 'Add Changelog', - link: presenter.add_changelog_path)) + expect(presenter.changelog_anchor_data).to have_attributes(enabled: false, + label: 'Add Changelog', + link: presenter.add_changelog_path) end end @@ -260,9 +297,9 @@ describe ProjectPresenter do it 'returns anchor data' do allow(project.repository).to receive(:changelog).and_return(double(name: 'foo')) - expect(presenter.changelog_anchor_data).to eq(OpenStruct.new(enabled: true, - label: 'Changelog', - link: presenter.changelog_path)) + expect(presenter.changelog_anchor_data).to have_attributes(enabled: true, + label: 'Changelog', + link: presenter.changelog_path) end end end @@ -273,9 +310,9 @@ describe ProjectPresenter do project.add_developer(user) allow(project.repository).to receive(:license_blob).and_return(nil) - expect(presenter.license_anchor_data).to eq(OpenStruct.new(enabled: false, - label: 'Add License', - link: presenter.add_license_path)) + expect(presenter.license_anchor_data).to have_attributes(enabled: false, + label: 'Add license', + link: presenter.add_license_path) end end @@ -283,9 +320,9 @@ describe ProjectPresenter do it 'returns anchor data' do allow(project.repository).to receive(:license_blob).and_return(double(name: 'foo')) - expect(presenter.license_anchor_data).to eq(OpenStruct.new(enabled: true, - label: presenter.license_short_name, - link: presenter.license_path)) + expect(presenter.license_anchor_data).to have_attributes(enabled: true, + label: presenter.license_short_name, + link: presenter.license_path) end end end @@ -296,9 +333,9 @@ describe ProjectPresenter do project.add_developer(user) allow(project.repository).to receive(:contribution_guide).and_return(nil) - expect(presenter.contribution_guide_anchor_data).to eq(OpenStruct.new(enabled: false, - label: 'Add Contribution guide', - link: presenter.add_contribution_guide_path)) + expect(presenter.contribution_guide_anchor_data).to have_attributes(enabled: false, + label: 'Add Contribution guide', + link: presenter.add_contribution_guide_path) end end @@ -306,9 +343,9 @@ describe ProjectPresenter do it 'returns anchor data' do allow(project.repository).to receive(:contribution_guide).and_return(double(name: 'foo')) - expect(presenter.contribution_guide_anchor_data).to eq(OpenStruct.new(enabled: true, - label: 'Contribution guide', - link: presenter.contribution_guide_path)) + expect(presenter.contribution_guide_anchor_data).to have_attributes(enabled: true, + label: 'Contribution guide', + link: presenter.contribution_guide_path) end end end @@ -318,9 +355,9 @@ describe ProjectPresenter do it 'returns anchor data' do allow(project).to receive(:auto_devops_enabled?).and_return(true) - expect(presenter.autodevops_anchor_data).to eq(OpenStruct.new(enabled: true, - label: 'Auto DevOps enabled', - link: nil)) + expect(presenter.autodevops_anchor_data).to have_attributes(enabled: true, + label: 'Auto DevOps enabled', + link: nil) end end @@ -330,9 +367,9 @@ describe ProjectPresenter do allow(project).to receive(:auto_devops_enabled?).and_return(false) allow(project.repository).to receive(:gitlab_ci_yml).and_return(nil) - expect(presenter.autodevops_anchor_data).to eq(OpenStruct.new(enabled: false, - label: 'Enable Auto DevOps', - link: presenter.project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))) + expect(presenter.autodevops_anchor_data).to have_attributes(enabled: false, + label: 'Enable Auto DevOps', + link: presenter.project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) end end end @@ -343,9 +380,9 @@ describe ProjectPresenter do project.add_maintainer(user) cluster = create(:cluster, projects: [project]) - expect(presenter.kubernetes_cluster_anchor_data).to eq(OpenStruct.new(enabled: true, - label: 'Kubernetes configured', - link: presenter.project_cluster_path(project, cluster))) + expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(enabled: true, + label: 'Kubernetes configured', + link: presenter.project_cluster_path(project, cluster)) end it 'returns link to clusters page if more than one exists' do @@ -353,17 +390,17 @@ describe ProjectPresenter do create(:cluster, :production_environment, projects: [project]) create(:cluster, projects: [project]) - expect(presenter.kubernetes_cluster_anchor_data).to eq(OpenStruct.new(enabled: true, - label: 'Kubernetes configured', - link: presenter.project_clusters_path(project))) + expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(enabled: true, + label: 'Kubernetes configured', + link: presenter.project_clusters_path(project)) end it 'returns link to create a cluster if no cluster exists' do project.add_maintainer(user) - expect(presenter.kubernetes_cluster_anchor_data).to eq(OpenStruct.new(enabled: false, - label: 'Add Kubernetes cluster', - link: presenter.new_project_cluster_path(project))) + expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(enabled: false, + label: 'Add Kubernetes cluster', + link: presenter.new_project_cluster_path(project)) end end @@ -380,9 +417,9 @@ describe ProjectPresenter do allow(project.repository).to receive(:koding_yml).and_return(nil) allow(Gitlab::CurrentSettings).to receive(:koding_enabled?).and_return(true) - expect(presenter.koding_anchor_data).to eq(OpenStruct.new(enabled: false, - label: 'Set up Koding', - link: presenter.add_koding_stack_path)) + expect(presenter.koding_anchor_data).to have_attributes(enabled: false, + label: 'Set up Koding', + link: presenter.add_koding_stack_path) end it 'returns nil if user cannot push' do diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb index b56940a9613..fc1fe5739c3 100644 --- a/spec/views/projects/_home_panel.html.haml_spec.rb +++ b/spec/views/projects/_home_panel.html.haml_spec.rb @@ -9,6 +9,7 @@ describe 'projects/_home_panel' do allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:can?).with(user, :read_project, project).and_return(false) + allow(project).to receive(:license_anchor_data).and_return(false) end context 'when user is signed in' do @@ -63,6 +64,7 @@ describe 'projects/_home_panel' do allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:can?).with(user, :read_project, project).and_return(false) + allow(project).to receive(:license_anchor_data).and_return(false) end context 'has no badges' do @@ -71,8 +73,7 @@ describe 'projects/_home_panel' do it 'should not render any badge' do render - expect(rendered).to have_selector('.project-badges') - expect(rendered).not_to have_selector('.project-badges > a') + expect(rendered).not_to have_selector('.project-badges') end end @@ -118,6 +119,7 @@ describe 'projects/_home_panel' do assign(:project, project) allow(view).to receive(:current_user).and_return(user) + allow(project).to receive(:license_anchor_data).and_return(false) end context 'user can read project' do |