Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/pages/projects/project.js57
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js5
-rw-r--r--app/assets/javascripts/read_more.js41
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/mobile.scss8
-rw-r--r--app/assets/stylesheets/framework/read_more.scss13
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/projects.scss235
-rw-r--r--app/helpers/button_helper.rb9
-rw-r--r--app/helpers/icons_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/helpers/visibility_level_helper.rb2
-rw-r--r--app/presenters/project_presenter.rb158
-rw-r--r--app/views/projects/_home_panel.html.haml83
-rw-r--r--app/views/projects/_stat_anchor_list.html.haml2
-rw-r--r--app/views/projects/buttons/_fork.html.haml30
-rw-r--r--app/views/projects/buttons/_star.html.haml30
-rw-r--r--app/views/projects/empty.html.haml18
-rw-r--r--app/views/projects/show.html.haml9
-rw-r--r--app/views/shared/_clone_panel.html.haml6
-rw-r--r--app/views/shared/_mobile_clone_panel.html.haml13
-rw-r--r--changelogs/unreleased/44704-improve-project-overview-ui.yml5
-rw-r--r--locale/gitlab.pot57
-rw-r--r--qa/qa/page/project/show.rb4
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb12
-rw-r--r--spec/features/projects_spec.rb56
-rw-r--r--spec/javascripts/fixtures/projects.rb10
-rw-r--r--spec/javascripts/read_more_spec.js23
-rw-r--r--spec/presenters/project_presenter_spec.rb153
-rw-r--r--spec/views/projects/_home_panel.html.haml_spec.rb6
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