summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js2
-rw-r--r--app/assets/javascripts/gl_dropdown.js14
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss1
-rw-r--r--app/assets/stylesheets/framework/header.scss13
-rw-r--r--app/assets/stylesheets/framework/variables.scss3
-rw-r--r--app/assets/stylesheets/new_nav.scss270
-rw-r--r--app/assets/stylesheets/new_sidebar.scss10
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/helpers/nav_helper.rb10
-rw-r--r--app/views/layouts/header/_default.html.haml8
-rw-r--r--app/views/layouts/header/_new.html.haml39
-rw-r--r--app/views/layouts/header/_new_dropdown.haml6
-rw-r--r--app/views/layouts/nav/_new_dashboard.html.haml41
-rw-r--r--app/views/layouts/nav/_new_explore.html.haml17
-rw-r--r--app/views/projects/issues/show.html.haml3
-rw-r--r--app/views/shared/_logo.svg2
-rw-r--r--app/views/shared/icons/_caret_down.svg1
-rw-r--r--app/views/shared/icons/_mr_bold.svg3
-rw-r--r--app/views/shared/icons/_plus_square.svg1
-rw-r--r--app/views/shared/icons/_todo_done.svg1
-rw-r--r--app/views/shared/issuable/_close_reopen_button.html.haml3
-rw-r--r--app/views/shared/issuable/_close_reopen_report_toggle.html.haml22
-rw-r--r--changelogs/unreleased/36860-migrate-issues-author.yml5
-rw-r--r--db/migrate/20170825104051_migrate_issues_to_ghost_user.rb36
-rw-r--r--db/migrate/20170901071411_add_foreign_key_to_issue_author.rb14
-rw-r--r--db/schema.rb3
-rw-r--r--doc/api/README.md11
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/confidential_issues_system_notes.pngbin2330 -> 4214 bytes
-rw-r--r--doc/user/search/img/issue_search_by_term.pngbin65826 -> 127492 bytes
-rw-r--r--features/support/gitaly.rb3
-rw-r--r--spec/features/issues/issue_detail_spec.rb14
-rw-r--r--spec/javascripts/gl_dropdown_spec.js323
-rw-r--r--spec/migrations/migrate_issues_to_ghost_user_spec.rb51
33 files changed, 603 insertions, 329 deletions
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 1c5ca1d3cf9..23040cd9eb8 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -61,7 +61,7 @@ class DropdownHint extends gl.FilteredSearchDropdown {
.map(tokenKey => ({
icon: `fa-${tokenKey.icon}`,
hint: tokenKey.key,
- tag: `<${tokenKey.tag}>`,
+ tag: `:${tokenKey.tag}`,
type: tokenKey.type,
}));
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index d65bbc0d808..6f7671aa6fe 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -637,11 +637,15 @@ GitLabDropdown = (function() {
value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName;
- if (value) { value = value.toString().replace(/'/g, '\\\''); }
-
- field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
- if (field.length) {
- selected = true;
+ if (value) {
+ value = value.toString().replace(/'/g, '\\\'');
+ field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`);
+ if (field.length) {
+ selected = true;
+ }
+ } else {
+ field = this.dropdown.parent().find(`input[name='${fieldName}']`);
+ selected = !field.length;
}
}
// Set URL
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index e65a78b8dc3..6b21def33a6 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -766,6 +766,7 @@
box-shadow: none;
padding: 8px 16px;
text-align: left;
+ white-space: normal;
width: 100%;
// make sure the text color is not overriden
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 35bd97980e2..b00a2d053e2 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -105,12 +105,11 @@ header {
top: -3px;
font-size: 10px;
}
+ }
+ .user-counter {
svg {
- position: relative;
- top: 2px;
- height: 17px;
- // hack to get SVG to line up with FA icons
+ height: 16px;
width: 23px;
fill: currentColor;
}
@@ -325,12 +324,12 @@ header {
li {
.badge {
position: inherit;
- top: -8px;
font-weight: $gl-font-weight-normal;
- margin-left: -11px;
+ margin-left: -6px;
font-size: 11px;
color: $white-light;
- padding: 1px 5px 2px;
+ padding: 0 5px;
+ line-height: 12px;
border-radius: 7px;
box-shadow: 0 1px 0 rgba($gl-header-color, .2);
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 01fffa717e9..88b08998dfd 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -177,13 +177,14 @@ $row-hover: $blue-25;
$row-hover-border: $blue-100;
$progress-color: #c0392b;
$header-height: 50px;
+$new-navbar-height: 40px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$limited-layout-width-sm: 790px;
$container-text-max-width: 540px;
$gl-avatar-size: 40px;
$error-exclamation-point: $red-500;
-$border-radius-default: 3px;
+$border-radius-default: 4px;
$settings-icon-size: 18px;
$provider-btn-not-active-color: $blue-500;
$link-underline-blue: $blue-500;
diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss
index b711bd12c73..4deb7431284 100644
--- a/app/assets/stylesheets/new_nav.scss
+++ b/app/assets/stylesheets/new_nav.scss
@@ -2,15 +2,21 @@
@import 'framework/tw_bootstrap_variables';
@import "bootstrap/variables";
+.content-wrapper.page-with-new-nav {
+ margin-top: $new-navbar-height;
+}
+
header.navbar-gitlab-new {
color: $white-light;
background: linear-gradient(to right, $indigo-900, $indigo-800);
border-bottom: 0;
+ min-height: $new-navbar-height;
.header-content {
display: -webkit-flex;
display: flex;
padding-left: 0;
+ min-height: $new-navbar-height;
.title-container {
display: -webkit-flex;
@@ -38,20 +44,13 @@ header.navbar-gitlab-new {
display: -webkit-flex;
display: flex;
align-items: center;
- padding-right: $gl-padding;
- padding-left: $gl-padding;
- margin-left: -$gl-padding;
-
- @media (min-width: $screen-sm-min) {
- padding-right: $gl-padding;
- padding-left: $gl-padding;
- }
+ padding: 2px 8px;
+ margin: 5px 2px 5px -8px;
+ border-radius: $border-radius-default;
svg {
- margin-top: -3px;
-
@media (min-width: $screen-sm-min) {
- margin-right: 10px;
+ margin-right: 8px;
}
}
@@ -60,7 +59,7 @@ header.navbar-gitlab-new {
svg {
width: 55px;
- height: 15px;
+ height: 14px;
margin: 0;
fill: $white-light;
}
@@ -68,9 +67,7 @@ header.navbar-gitlab-new {
&:hover,
&:focus {
- .logo-text svg {
- fill: $tanuki-yellow;
- }
+ background-color: rgba($indigo-200, .2);
}
}
}
@@ -90,6 +87,20 @@ header.navbar-gitlab-new {
right: 0;
}
}
+
+ &.menu-expanded {
+ @media (max-width: $screen-xs-max) {
+ .title-container,
+ .header-logo, {
+ display: none;
+ }
+ }
+ }
+ }
+
+ .dropdown-bold-header {
+ color: $gl-text-color-secondary;
+ font-size: 12px;
}
.navbar-collapse {
@@ -98,14 +109,10 @@ header.navbar-gitlab-new {
box-shadow: 0;
@media (max-width: $screen-xs-max) {
- margin-left: -$gl-padding;
+ margin-left: -8px;
margin-right: -10px;
}
- .dropdown-bold-header {
- color: initial;
- }
-
.nav {
> li:not(.hidden-xs) a {
@media (max-width: $screen-xs-max) {
@@ -119,7 +126,7 @@ header.navbar-gitlab-new {
.container-fluid {
.navbar-toggle {
min-width: 45px;
- padding: 6px $gl-padding;
+ padding: 4px $gl-padding;
margin-right: -7px;
font-size: 14px;
text-align: center;
@@ -156,31 +163,90 @@ header.navbar-gitlab-new {
}
> a {
- background: none;
will-change: color;
+ margin: 4px 2px;
+ padding: 6px 8px;
+ color: $indigo-200;
+ height: 32px;
+
+ @media (max-width: $screen-xs-max) {
+ padding: 0;
+ }
+
+ svg {
+ fill: $indigo-200;
+ }
&.header-user-dropdown-toggle {
+ margin-left: 2px;
+
.header-user-avatar {
border-color: $indigo-200;
+ margin-right: 0;
}
}
+ }
- &:hover,
- &:focus {
- color: $white-light;
- opacity: 1;
+ .header-new-dropdown-toggle {
+ margin-right: 0;
+ }
- > svg {
- fill: $white-light;
- }
+ > a:hover,
+ > a:focus {
+ text-decoration: none;
+ outline: 0;
+ opacity: 1;
+ color: $white-light;
+
+ @media (min-width: $screen-sm-min) {
+ background-color: rgba($indigo-200, .2);
+ }
+
+ svg {
+ fill: currentColor;
+ }
- &.header-user-dropdown-toggle {
- .header-user-avatar {
- border-color: $white-light;
- }
+ &.header-user-dropdown-toggle {
+ .header-user-avatar {
+ border-color: $white-light;
}
}
}
+
+ .impersonated-user,
+ .impersonated-user:hover {
+ margin-right: 1px;
+ background-color: $white-light;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+
+ svg {
+ fill: $indigo-900;
+ }
+ }
+
+ .impersonation-btn,
+ .impersonation-btn:hover {
+ background-color: $white-light;
+ margin-left: 0;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+
+ i {
+ color: $orange-500;
+ font-size: 20px;
+ }
+ }
+
+ &.active > a,
+ &.dropdown.open > a {
+ color: $indigo-900;
+ background-color: $white-light;
+
+ svg {
+ fill: currentColor;
+ }
+ }
}
}
}
@@ -188,45 +254,76 @@ header.navbar-gitlab-new {
.navbar-sub-nav {
display: -webkit-flex;
display: flex;
- margin-bottom: 0;
+ margin: 0 0 0 6px;
color: $indigo-200;
- > li {
- > a:hover,
- > a:focus {
- box-shadow: inset 0 -3px 0 rgba($indigo-200, .4);
- text-decoration: none;
- outline: 0;
- color: $white-light;
- }
+ .dropdown-chevron {
+ position: relative;
+ top: -1px;
+ font-size: 10px;
+ }
+}
- &.active > a {
- box-shadow: inset 0 -3px 0 $indigo-500;
- color: $white-light;
- font-weight: $gl-font-weight-bold;
- }
+.navbar-gitlab-new {
+ .navbar-sub-nav,
+ .navbar-nav {
+ > li {
+ > a:hover,
+ > a:focus {
+ text-decoration: none;
+ outline: 0;
+ color: $white-light;
+ background-color: rgba($indigo-200, .2);
- > a {
- display: block;
- padding: 16px 10px;
- font-size: 13px;
- color: currentColor;
- box-shadow: inset 0 0 0 transparent;
- will-change: box-shadow;
- transition: box-shadow 0.15s;
+ svg {
+ fill: currentColor;
+ }
+ }
- @media (min-width: $screen-sm-min) {
- padding: 15px $gl-padding;
- font-size: 14px;
+ &.active > a,
+ &.dropdown.open > a {
+ color: $indigo-900;
+ background-color: $white-light;
+
+ svg {
+ fill: currentColor;
+ }
+ }
+
+ > a {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 6px 8px;
+ margin: 4px 2px;
+ font-size: 12px;
+ color: currentColor;
+ border-radius: $border-radius-default;
+ height: 32px;
+ font-weight: $gl-font-weight-bold;
+
+ svg {
+ fill: currentColor;
+ }
+ }
+
+ &.line-separator {
+ border-left: 1px solid rgba($indigo-200, .2);
+ margin: 8px;
}
}
}
+}
- .dropdown-chevron {
- position: relative;
- top: -1px;
- font-size: 10px;
- }
+.admin-icon i {
+ font-size: 18px;
+}
+
+.caret-down {
+ height: 11px;
+ width: 11px;
+ margin-left: 4px;
+ fill: currentColor;
}
.header-user .dropdown-menu-nav,
@@ -235,10 +332,14 @@ header.navbar-gitlab-new {
}
.search {
+ margin: 4px 8px 0;
+
form {
+ height: 32px;
border: 0;
+ border-radius: $border-radius-default;
background-color: rgba($indigo-200, .2);
- transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s, background-color ease-in-out 0.15s;
+ transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover {
background-color: rgba($indigo-200, .3);
@@ -247,31 +348,50 @@ header.navbar-gitlab-new {
}
&.search-active form {
- background-color: rgba($indigo-200, .3);
+ background-color: $white-light;
box-shadow: none;
+
+ .search-input {
+ color: $gl-text-color;
+ transition: color ease-in-out 0.15s;
+ }
+
+ .search-input::placeholder {
+ color: $gl-text-color-tertiary;
+ }
+
+ .search-input-wrap {
+ .search-icon,
+ .clear-icon {
+ color: $gl-text-color-tertiary;
+ transition: color ease-in-out 0.15s;
+ }
+ }
}
.search-input {
color: $white-light;
background: none;
+ transition: color ease-in-out 0.15s;
}
.search-input::placeholder {
color: rgba($indigo-200, .8);
+ transition: color ease-in-out 0.15s;
}
.location-badge {
font-size: 12px;
color: $indigo-100;
background-color: rgba($indigo-200, .1);
- transition: color 0.15s;
will-change: color;
margin: -4px 4px -4px -4px;
line-height: 25px;
padding: 4px 8px;
border-radius: 2px 0 0 2px;
border-right: 1px solid $indigo-800;
- height: 34px;
+ height: 32px;
+ transition: border-color ease-in-out 0.15s;
}
.search-input-wrap {
@@ -283,8 +403,9 @@ header.navbar-gitlab-new {
&.search-active {
.location-badge {
- color: $white-light;
- background-color: rgba($indigo-200, .2);
+ color: $gl-text-color;
+ background-color: $nav-badge-bg;
+ border-color: $border-color;
}
.search-input-wrap {
@@ -458,3 +579,14 @@ header.navbar-gitlab-new {
}
}
}
+
+.btn-sign-in {
+ margin-top: 3px;
+ background-color: $indigo-100;
+ color: $indigo-900;
+ font-weight: $gl-font-weight-bold;
+
+ &:hover {
+ background-color: $white-light;
+ }
+}
diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss
index f624b130e19..90b0a543c5c 100644
--- a/app/assets/stylesheets/new_sidebar.scss
+++ b/app/assets/stylesheets/new_sidebar.scss
@@ -26,7 +26,7 @@ $new-sidebar-collapsed-width: 50px;
// Override position: absolute
.right-sidebar {
position: fixed;
- height: calc(100% - #{$header-height});
+ height: calc(100% - #{$new-navbar-height});
}
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
@@ -93,7 +93,7 @@ $new-sidebar-collapsed-width: 50px;
z-index: 400;
width: $new-sidebar-width;
transition: left $sidebar-transition-duration;
- top: $header-height;
+ top: $new-navbar-height;
bottom: 0;
left: 0;
background-color: $gray-normal;
@@ -189,7 +189,7 @@ $new-sidebar-collapsed-width: 50px;
}
.with-performance-bar .nav-sidebar {
- top: $header-height + $performance-bar-height;
+ top: $new-navbar-height + $performance-bar-height;
}
.sidebar-sub-level-items {
@@ -453,7 +453,7 @@ $new-sidebar-collapsed-width: 50px;
// Make issue boards full-height now that sub-nav is gone
.boards-list {
- height: calc(100vh - #{$header-height});
+ height: calc(100vh - #{$new-navbar-height});
@media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS
@@ -464,7 +464,7 @@ $new-sidebar-collapsed-width: 50px;
}
.with-performance-bar .boards-list {
- height: calc(100vh - #{$header-height} - #{$performance-bar-height});
+ height: calc(100vh - #{$new-navbar-height} - #{$performance-bar-height});
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 8d73246223d..615020ca856 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -190,6 +190,8 @@ input[type="checkbox"]:hover {
}
.search-holder {
+ @include new-style-dropdown;
+
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
display: flex;
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index b63b3b70903..73b3386fe9c 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -38,7 +38,7 @@ module NavHelper
end
def layout_nav_class
- return [] if show_new_nav?
+ return 'page-with-new-nav' if show_new_nav?
class_names = []
class_names << 'page-with-layout-nav' if defined?(nav) && nav
@@ -50,4 +50,12 @@ module NavHelper
def nav_control_class
"nav-control" if current_user
end
+
+ def user_dropdown_class
+ class_names = []
+ class_names << 'header-user-dropdown-toggle'
+ class_names << 'impersonated-user' if session[:impersonator_id]
+
+ class_names
+ end
end
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 1d875f81041..0d6760e7b8f 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -42,21 +42,21 @@
= link_to sherlock_transactions_path, title: 'Sherlock Transactions',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('tachometer fw')
- %li
+ %li.user-counter
= link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('issues')
- issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
- %li
+ %li.user-counter
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold')
- merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count)
- %li
+ %li.user-counter
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('check-circle fw')
+ = custom_icon('todo_done')
%span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count)
%li.header-user.dropdown
diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml
index c84d7053cd6..61b71c091be 100644
--- a/app/views/layouts/header/_new.html.haml
+++ b/app/views/layouts/header/_new.html.haml
@@ -16,47 +16,35 @@
.navbar-collapse.collapse
%ul.nav.navbar-nav
+ - if current_user
+ = render 'layouts/header/new_dropdown'
%li.hidden-sm.hidden-xs
= render 'layouts/search' unless current_controller?(:search)
%li.visible-sm-inline-block.visible-xs-inline-block
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search')
- if current_user
- - if session[:impersonator_id]
- %li.impersonation
- = link_to admin_impersonation_path, method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
- = icon('user-secret fw')
- - if current_user.admin?
- %li
- = link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('wrench fw')
- = render 'layouts/header/new_dropdown'
- - if Gitlab::Sherlock.enabled?
- %li
- = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
- data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('tachometer fw')
- %li
+ %li.user-counter
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('issues')
- issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
- %li
+ %li.user-counter
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold')
- merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count)
- %li
+ %li.user-counter
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('check-circle fw')
+ = custom_icon('todo_done')
%span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count)
%li.header-user.dropdown
- = link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
- = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar"
- = icon('chevron-down')
+ = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
+ = image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar"
+ = custom_icon('caret_down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
%li.current-user
@@ -68,13 +56,20 @@
= link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
%li
= link_to "Settings", profile_path
+ - if current_user
+ %li
+ = link_to "Help", help_path
%li.divider
%li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link"
+ - if session[:impersonator_id]
+ %li.impersonation
+ = link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ = icon('user-secret')
- else
%li
%div
- = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
+ = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
%button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
%span.sr-only Toggle navigation
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index 9da739b0974..9cf2739b368 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -1,11 +1,11 @@
%li.header-new.dropdown
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do
- if show_new_nav?
- = icon('plus')
- = icon('chevron-down')
+ = custom_icon('plus_square')
+ = custom_icon('caret_down')
- else
= icon('plus fw')
- = icon('caret-down')
+ = custom_icon('caret_down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
- if @group&.persisted?
diff --git a/app/views/layouts/nav/_new_dashboard.html.haml b/app/views/layouts/nav/_new_dashboard.html.haml
index e670e04928c..8a39c4d775f 100644
--- a/app/views/layouts/nav/_new_dashboard.html.haml
+++ b/app/views/layouts/nav/_new_dashboard.html.haml
@@ -1,26 +1,38 @@
%ul.list-unstyled.navbar-sub-nav
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown" }) do
- %a{ href: '#', title: 'Projects', data: { toggle: 'dropdown' } }
+ %a{ href: "#", data: { toggle: "dropdown" } }
Projects
- = icon("chevron-down", class: "dropdown-chevron")
+ = custom_icon('caret_down')
.dropdown-menu.projects-dropdown-menu
= render "layouts/nav/projects_dropdown/show"
- = nav_link(controller: ['dashboard/groups', 'explore/groups']) do
+ = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "hidden-xs" }) do
= link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
Groups
- = nav_link(path: 'dashboard#activity', html_options: { class: "hidden-xs hidden-sm hidden-md" }) do
+ = nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
Activity
- %li.dropdown
+ = nav_link(controller: 'dashboard/milestones', html_options: { class: "visible-lg" }) do
+ = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
+ Milestones
+
+ = nav_link(controller: 'dashboard/snippets', html_options: { class: "visible-lg" }) do
+ = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
+ Snippets
+
+ %li.dropdown.hidden-lg
%a{ href: "#", data: { toggle: "dropdown" } }
More
- = icon("chevron-down", class: "dropdown-chevron")
+ = custom_icon('caret_down')
.dropdown-menu
%ul
- = nav_link(path: 'dashboard#activity', html_options: { class: "visible-xs visible-sm visible-md" }) do
+ = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do
+ = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
+ Groups
+
+ = nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, title: 'Activity' do
Activity
@@ -31,11 +43,20 @@
= nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
Snippets
- %li.divider
- %li
- = link_to "Help", help_path, title: 'About GitLab CE'
-# Shortcut to Dashboard > Projects
%li.hidden
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
Projects
+
+ - if current_user.admin? || Gitlab::Sherlock.enabled?
+ %li.line-separator.hidden-xs
+ - if current_user.admin?
+ = nav_link(controller: 'admin/dashboard') do
+ = link_to admin_root_path, class: 'admin-icon', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('wrench fw')
+ - if Gitlab::Sherlock.enabled?
+ %li
+ = link_to sherlock_transactions_path, class: 'admin-icon', title: 'Sherlock Transactions',
+ data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('tachometer fw')
diff --git a/app/views/layouts/nav/_new_explore.html.haml b/app/views/layouts/nav/_new_explore.html.haml
index 40385f251e3..cd1c39f3226 100644
--- a/app/views/layouts/nav/_new_explore.html.haml
+++ b/app/views/layouts/nav/_new_explore.html.haml
@@ -5,15 +5,8 @@
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do
Groups
- %li.dropdown
- %a{ href: "#", data: { toggle: "dropdown" } }
- More
- = icon("chevron-down", class: "dropdown-chevron")
- .dropdown-menu
- %ul
- = nav_link(controller: :snippets) do
- = link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do
- Snippets
- %li.divider
- %li
- = link_to "Help", help_path, title: 'About GitLab CE'
+ = nav_link(controller: :snippets) do
+ = link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do
+ Snippets
+ %li
+ = link_to "Help", help_path, title: 'About GitLab CE'
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index fd7ff176c5e..04b4ed95a2d 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -37,8 +37,7 @@
%ul
- if can_update_issue
%li= link_to 'Edit', edit_project_issue_path(@project, @issue)
- / TODO: simplify condition back #36860
- - if @issue.author && current_user != @issue.author
+ - unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue
%li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue'
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
index 10e6c49ae9f..0ef9de5fed6 100644
--- a/app/views/shared/_logo.svg
+++ b/app/views/shared/_logo.svg
@@ -1,4 +1,4 @@
-<svg width="28" height="28" class="tanuki-logo" viewBox="0 0 36 36">
+<svg width="24" height="24" class="tanuki-logo" viewBox="0 0 36 36">
<path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/>
<path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/>
<path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/>
diff --git a/app/views/shared/icons/_caret_down.svg b/app/views/shared/icons/_caret_down.svg
new file mode 100644
index 00000000000..fd80fd0f651
--- /dev/null
+++ b/app/views/shared/icons/_caret_down.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="caret-down" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></svg>
diff --git a/app/views/shared/icons/_mr_bold.svg b/app/views/shared/icons/_mr_bold.svg
index 5468545da2e..0f5be6e2bc8 100644
--- a/app/views/shared/icons/_mr_bold.svg
+++ b/app/views/shared/icons/_mr_bold.svg
@@ -1,2 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg>
-
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg>
diff --git a/app/views/shared/icons/_plus_square.svg b/app/views/shared/icons/_plus_square.svg
new file mode 100644
index 00000000000..7263d924f1f
--- /dev/null
+++ b/app/views/shared/icons/_plus_square.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M9 7V4c0-.552-.448-1-1-1s-1 .448-1 1v3H4c-.552 0-1 .448-1 1s.448 1 1 1h3v3c0 .552.448 1 1 1s1-.448 1-1V9h3c.552 0 1-.448 1-1s-.448-1-1-1H9zM3 0h10c1.657 0 3 1.343 3 3v10c0 1.657-1.343 3-3 3H3c-1.657 0-3-1.343-3-3V3c0-1.657 1.343-3 3-3z"/></svg>
diff --git a/app/views/shared/icons/_todo_done.svg b/app/views/shared/icons/_todo_done.svg
new file mode 100644
index 00000000000..156dfa11df1
--- /dev/null
+++ b/app/views/shared/icons/_todo_done.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0l-2.83-2.83a1 1 0 0 1 1.415-1.413l2.123 2.12zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></svg>
diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml
index cb706d80f23..f16bc8dd430 100644
--- a/app/views/shared/issuable/_close_reopen_button.html.haml
+++ b/app/views/shared/issuable/_close_reopen_button.html.haml
@@ -9,7 +9,6 @@
class: "hidden-xs hidden-sm btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
- elsif can_update && !is_current_user
= render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
-- elsif issuable.author
- / TODO: change back to else #36860
+- else
= link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
class: 'hidden-xs hidden-sm btn btn-grouped btn-close-color', title: 'Report abuse'
diff --git a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
index d8144a39b23..a38cd319e3c 100644
--- a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
+++ b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
@@ -37,15 +37,13 @@
%li.divider.droplab-item-ignore
- / TODO: remove condition #36860
- - if issuable.author
- %li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
- button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } }
- %button.btn.btn-transparent
- = icon('check', class: 'icon')
- .description
- %strong.title Report abuse
- %p.text
- Report
- = display_issuable_type.pluralize
- that are abusive, inappropriate or spam.
+ %li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
+ button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } }
+ %button.btn.btn-transparent
+ = icon('check', class: 'icon')
+ .description
+ %strong.title Report abuse
+ %p.text
+ Report
+ = display_issuable_type.pluralize
+ that are abusive, inappropriate or spam.
diff --git a/changelogs/unreleased/36860-migrate-issues-author.yml b/changelogs/unreleased/36860-migrate-issues-author.yml
new file mode 100644
index 00000000000..3e9fcc55836
--- /dev/null
+++ b/changelogs/unreleased/36860-migrate-issues-author.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate issues authored by deleted user to the Ghost user
+merge_request:
+author:
+type: fixed
diff --git a/db/migrate/20170825104051_migrate_issues_to_ghost_user.rb b/db/migrate/20170825104051_migrate_issues_to_ghost_user.rb
new file mode 100644
index 00000000000..294141e4fdb
--- /dev/null
+++ b/db/migrate/20170825104051_migrate_issues_to_ghost_user.rb
@@ -0,0 +1,36 @@
+class MigrateIssuesToGhostUser < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class User < ActiveRecord::Base
+ self.table_name = 'users'
+ end
+
+ class Issue < ActiveRecord::Base
+ self.table_name = 'issues'
+
+ include ::EachBatch
+ end
+
+ def reset_column_in_migration_models
+ ActiveRecord::Base.clear_cache!
+
+ ::User.reset_column_information
+ end
+
+ def up
+ reset_column_in_migration_models
+
+ # we use the model method because rewriting it is too complicated and would require copying multiple methods
+ ghost_id = ::User.ghost.id
+
+ Issue.where('NOT EXISTS (?)', User.unscoped.select(1).where('issues.author_id = users.id')).each_batch do |relation|
+ relation.update_all(author_id: ghost_id)
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20170901071411_add_foreign_key_to_issue_author.rb b/db/migrate/20170901071411_add_foreign_key_to_issue_author.rb
new file mode 100644
index 00000000000..ab6e9fb565a
--- /dev/null
+++ b/db/migrate/20170901071411_add_foreign_key_to_issue_author.rb
@@ -0,0 +1,14 @@
+class AddForeignKeyToIssueAuthor < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key(:issues, :users, column: :author_id, on_delete: :nullify)
+ end
+
+ def down
+ remove_foreign_key(:issues, column: :author_id)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 40b84f2bddd..f980667a38f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170831195038) do
+ActiveRecord::Schema.define(version: 20170901071411) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1708,6 +1708,7 @@ ActiveRecord::Schema.define(version: 20170831195038) do
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade
+ add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
diff --git a/doc/api/README.md b/doc/api/README.md
index c2a08dcff07..a947eed2db8 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -61,16 +61,7 @@ following locations:
## Road to GraphQL
-Going forward, we will start on moving to
-[GraphQL](http://graphql.org/learn/best-practices/) and deprecate the use of
-controller-specific endpoints. GraphQL has a number of benefits:
-
-1. We avoid having to maintain two different APIs.
-2. Callers of the API can request only what they need.
-3. It is versioned by default.
-
-It will co-exist with the current v4 REST API. If we have a v5 API, this should
-be a compatibility layer on top of GraphQL.
+We have changed our plans to move to GraphQL. After reviewing the GraphQL license, anything related to the Facebook BSD plus patent license will not be allowed at GitLab.
## Basic usage
diff --git a/doc/user/project/issues/img/confidential_issues_system_notes.png b/doc/user/project/issues/img/confidential_issues_system_notes.png
index 82e0dd8e85e..355be80ecb6 100755..100644
--- a/doc/user/project/issues/img/confidential_issues_system_notes.png
+++ b/doc/user/project/issues/img/confidential_issues_system_notes.png
Binary files differ
diff --git a/doc/user/search/img/issue_search_by_term.png b/doc/user/search/img/issue_search_by_term.png
index 66e612c4ea6..3cefa3adb8b 100644
--- a/doc/user/search/img/issue_search_by_term.png
+++ b/doc/user/search/img/issue_search_by_term.png
Binary files differ
diff --git a/features/support/gitaly.rb b/features/support/gitaly.rb
new file mode 100644
index 00000000000..3cd5f4ce497
--- /dev/null
+++ b/features/support/gitaly.rb
@@ -0,0 +1,3 @@
+Spinach.hooks.before_scenario do
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
+end
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index c470cb7c716..28b636f9359 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -40,18 +40,4 @@ feature 'Issue Detail', :js do
end
end
end
-
- context 'when authored by a user who is later deleted' do
- before do
- issue.update_attribute(:author_id, nil)
- sign_in(user)
- visit project_issue_path(project, issue)
- end
-
- it 'shows the issue' do
- page.within('.issuable-details') do
- expect(find('h2')).to have_content(issue.title)
- end
- end
- end
end
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 10fcc590c89..dcb8dbce178 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -4,7 +4,10 @@ import '~/gl_dropdown';
import '~/lib/utils/common_utils';
import '~/lib/utils/url_utility';
-(() => {
+describe('glDropdown', function describeDropdown() {
+ preloadFixtures('static/gl_dropdown.html.raw');
+ loadJSONFixtures('projects.json');
+
const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
const SEARCH_INPUT_SELECTOR = '.dropdown-input-field';
const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
@@ -39,187 +42,217 @@ import '~/lib/utils/url_utility';
remoteCallback = callback.bind({}, data);
};
- describe('Dropdown', function describeDropdown() {
- preloadFixtures('static/gl_dropdown.html.raw');
- loadJSONFixtures('projects.json');
-
- function initDropDown(hasRemote, isFilterable, extraOpts = {}) {
- const options = Object.assign({
- selectable: true,
- filterable: isFilterable,
- data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
- search: {
- fields: ['name']
- },
- text: project => (project.name_with_namespace || project.name),
- id: project => project.id,
- }, extraOpts);
- this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown(options);
- }
+ function initDropDown(hasRemote, isFilterable, extraOpts = {}) {
+ const options = Object.assign({
+ selectable: true,
+ filterable: isFilterable,
+ data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
+ search: {
+ fields: ['name']
+ },
+ text: project => (project.name_with_namespace || project.name),
+ id: project => project.id,
+ }, extraOpts);
+ this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown(options);
+ }
+
+ beforeEach(() => {
+ loadFixtures('static/gl_dropdown.html.raw');
+ this.dropdownContainerElement = $('.dropdown.inline');
+ this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
+ this.projectsData = getJSONFixture('projects.json');
+ });
- beforeEach(() => {
- loadFixtures('static/gl_dropdown.html.raw');
- this.dropdownContainerElement = $('.dropdown.inline');
- this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
- this.projectsData = getJSONFixture('projects.json');
- });
+ afterEach(() => {
+ $('body').unbind('keydown');
+ this.dropdownContainerElement.unbind('keyup');
+ });
- afterEach(() => {
- $('body').unbind('keydown');
- this.dropdownContainerElement.unbind('keyup');
- });
+ it('should open on click', () => {
+ initDropDown.call(this, false);
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ this.dropdownButtonElement.click();
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ });
- it('should open on click', () => {
- initDropDown.call(this, false);
- expect(this.dropdownContainerElement).not.toHaveClass('open');
- this.dropdownButtonElement.click();
- expect(this.dropdownContainerElement).toHaveClass('open');
- });
+ it('escapes HTML as text', () => {
+ this.projectsData[0].name_with_namespace = '<script>alert("testing");</script>';
- it('escapes HTML as text', () => {
- this.projectsData[0].name_with_namespace = '<script>alert("testing");</script>';
+ initDropDown.call(this, false);
- initDropDown.call(this, false);
+ this.dropdownButtonElement.click();
- this.dropdownButtonElement.click();
+ expect(
+ $('.dropdown-content li:first-child').text(),
+ ).toBe('<script>alert("testing");</script>');
+ });
- expect(
- $('.dropdown-content li:first-child').text(),
- ).toBe('<script>alert("testing");</script>');
- });
+ it('should output HTML when highlighting', () => {
+ this.projectsData[0].name_with_namespace = 'testing';
+ $('.dropdown-input .dropdown-input-field').val('test');
- it('should output HTML when highlighting', () => {
- this.projectsData[0].name_with_namespace = 'testing';
- $('.dropdown-input .dropdown-input-field').val('test');
+ initDropDown.call(this, false, true, {
+ highlight: true,
+ });
- initDropDown.call(this, false, true, {
- highlight: true,
- });
+ this.dropdownButtonElement.click();
- this.dropdownButtonElement.click();
+ expect(
+ $('.dropdown-content li:first-child').text(),
+ ).toBe('testing');
- expect(
- $('.dropdown-content li:first-child').text(),
- ).toBe('testing');
+ expect(
+ $('.dropdown-content li:first-child a').html(),
+ ).toBe('<b>t</b><b>e</b><b>s</b><b>t</b>ing');
+ });
- expect(
- $('.dropdown-content li:first-child a').html(),
- ).toBe('<b>t</b><b>e</b><b>s</b><b>t</b>ing');
+ describe('that is open', () => {
+ beforeEach(() => {
+ initDropDown.call(this, false, false);
+ this.dropdownButtonElement.click();
});
- describe('that is open', () => {
- beforeEach(() => {
- initDropDown.call(this, false, false);
- this.dropdownButtonElement.click();
+ it('should select a following item on DOWN keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
+ const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
+ navigateWithKeys('down', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
});
+ });
- it('should select a following item on DOWN keypress', () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
- const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
- navigateWithKeys('down', randomIndex, () => {
+ it('should select a previous item on UP keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
+ navigateWithKeys('down', (this.projectsData.length - 1), () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
+ const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
+ navigateWithKeys('up', randomIndex, () => {
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
- expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
+ expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
});
});
+ });
- it('should select a previous item on UP keypress', () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
- navigateWithKeys('down', (this.projectsData.length - 1), () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
- const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
- navigateWithKeys('up', randomIndex, () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
- expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
- });
+ it('should click the selected item on ENTER keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
+ navigateWithKeys('down', randomIndex, () => {
+ spyOn(gl.utils, 'visitUrl').and.stub();
+ navigateWithKeys('enter', null, () => {
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
+ expect(link).toHaveClass('is-active');
+ const linkedLocation = link.attr('href');
+ if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation);
});
});
+ });
- it('should click the selected item on ENTER keypress', () => {
- expect(this.dropdownContainerElement).toHaveClass('open');
- const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
- navigateWithKeys('down', randomIndex, () => {
- spyOn(gl.utils, 'visitUrl').and.stub();
- navigateWithKeys('enter', null, () => {
- expect(this.dropdownContainerElement).not.toHaveClass('open');
- const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
- expect(link).toHaveClass('is-active');
- const linkedLocation = link.attr('href');
- if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation);
- });
- });
+ it('should close on ESC keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ this.dropdownContainerElement.trigger({
+ type: 'keyup',
+ which: ARROW_KEYS.ESC,
+ keyCode: ARROW_KEYS.ESC
});
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ });
+ });
- it('should close on ESC keypress', () => {
- expect(this.dropdownContainerElement).toHaveClass('open');
- this.dropdownContainerElement.trigger({
- type: 'keyup',
- which: ARROW_KEYS.ESC,
- keyCode: ARROW_KEYS.ESC
- });
- expect(this.dropdownContainerElement).not.toHaveClass('open');
+ describe('opened and waiting for a remote callback', () => {
+ beforeEach(() => {
+ initDropDown.call(this, true, true);
+ this.dropdownButtonElement.click();
+ });
+
+ it('should show loading indicator while search results are being fetched by backend', () => {
+ const dropdownMenu = document.querySelector('.dropdown-menu');
+
+ expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(true);
+ remoteCallback();
+ expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(false);
+ });
+
+ it('should not focus search input while remote task is not complete', () => {
+ expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
+ remoteCallback();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+
+ it('should focus search input after remote task is complete', () => {
+ remoteCallback();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+
+ it('should focus on input when opening for the second time after transition', () => {
+ remoteCallback();
+ this.dropdownContainerElement.trigger({
+ type: 'keyup',
+ which: ARROW_KEYS.ESC,
+ keyCode: ARROW_KEYS.ESC
});
+ this.dropdownButtonElement.click();
+ this.dropdownContainerElement.trigger('transitionend');
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
});
+ });
+
+ describe('input focus with array data', () => {
+ it('should focus input when passing array data to drop down', () => {
+ initDropDown.call(this, false, true);
+ this.dropdownButtonElement.click();
+ this.dropdownContainerElement.trigger('transitionend');
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+ });
+
+ it('should still have input value on close and restore', () => {
+ const $searchInput = $(SEARCH_INPUT_SELECTOR);
+ initDropDown.call(this, false, true);
+ $searchInput
+ .trigger('focus')
+ .val('g')
+ .trigger('input');
+ expect($searchInput.val()).toEqual('g');
+ this.dropdownButtonElement.trigger('hidden.bs.dropdown');
+ $searchInput
+ .trigger('blur')
+ .trigger('focus');
+ expect($searchInput.val()).toEqual('g');
+ });
+
+ describe('renderItem', () => {
+ describe('without selected value', () => {
+ let dropdown;
- describe('opened and waiting for a remote callback', () => {
beforeEach(() => {
- initDropDown.call(this, true, true);
- this.dropdownButtonElement.click();
+ const dropdownOptions = {
+
+ };
+ const $dropdownDiv = $('<div />');
+ $dropdownDiv.glDropdown(dropdownOptions);
+ dropdown = $dropdownDiv.data('glDropdown');
});
- it('should show loading indicator while search results are being fetched by backend', () => {
- const dropdownMenu = document.querySelector('.dropdown-menu');
+ it('marks items without ID as active', () => {
+ const dummyData = { };
- expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(true);
- remoteCallback();
- expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(false);
- });
+ const html = dropdown.renderItem(dummyData, null, null);
- it('should not focus search input while remote task is not complete', () => {
- expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
- remoteCallback();
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ const link = html.querySelector('a');
+ expect(link).toHaveClass('is-active');
});
- it('should focus search input after remote task is complete', () => {
- remoteCallback();
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
- });
+ it('does not mark items with ID as active', () => {
+ const dummyData = {
+ id: 'ea'
+ };
- it('should focus on input when opening for the second time after transition', () => {
- remoteCallback();
- this.dropdownContainerElement.trigger({
- type: 'keyup',
- which: ARROW_KEYS.ESC,
- keyCode: ARROW_KEYS.ESC
- });
- this.dropdownButtonElement.click();
- this.dropdownContainerElement.trigger('transitionend');
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
- });
- });
+ const html = dropdown.renderItem(dummyData, null, null);
- describe('input focus with array data', () => {
- it('should focus input when passing array data to drop down', () => {
- initDropDown.call(this, false, true);
- this.dropdownButtonElement.click();
- this.dropdownContainerElement.trigger('transitionend');
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ const link = html.querySelector('a');
+ expect(link).not.toHaveClass('is-active');
});
});
-
- it('should still have input value on close and restore', () => {
- const $searchInput = $(SEARCH_INPUT_SELECTOR);
- initDropDown.call(this, false, true);
- $searchInput
- .trigger('focus')
- .val('g')
- .trigger('input');
- expect($searchInput.val()).toEqual('g');
- this.dropdownButtonElement.trigger('hidden.bs.dropdown');
- $searchInput
- .trigger('blur')
- .trigger('focus');
- expect($searchInput.val()).toEqual('g');
- });
});
-})();
+});
diff --git a/spec/migrations/migrate_issues_to_ghost_user_spec.rb b/spec/migrations/migrate_issues_to_ghost_user_spec.rb
new file mode 100644
index 00000000000..cfd4021fbac
--- /dev/null
+++ b/spec/migrations/migrate_issues_to_ghost_user_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20170825104051_migrate_issues_to_ghost_user.rb')
+
+describe MigrateIssuesToGhostUser, :migration do
+ describe '#up' do
+ let(:projects) { table(:projects) }
+ let(:issues) { table(:issues) }
+ let(:users) { table(:users) }
+
+ before do
+ projects.create!(name: 'gitlab')
+ user = users.create(email: 'test@example.com')
+ issues.create(title: 'Issue 1', author_id: nil, project_id: 1)
+ issues.create(title: 'Issue 2', author_id: user.id, project_id: 1)
+ end
+
+ context 'when ghost user exists' do
+ let!(:ghost) { users.create(ghost: true, email: 'ghost@example.com') }
+
+ it 'does not create a new user' do
+ expect { schema_migrate_up! }.not_to change { User.count }
+ end
+
+ it 'migrates issues where author = nil to the ghost user' do
+ schema_migrate_up!
+
+ expect(issues.first.reload.author_id).to eq(ghost.id)
+ end
+
+ it 'does not change issues authored by an existing user' do
+ expect { schema_migrate_up! }.not_to change { issues.second.reload.author_id}
+ end
+ end
+
+ context 'when ghost user does not exist' do
+ it 'creates a new user' do
+ expect { schema_migrate_up! }.to change { User.count }.by(1)
+ end
+
+ it 'migrates issues where author = nil to the ghost user' do
+ schema_migrate_up!
+
+ expect(issues.first.reload.author_id).to eq(User.ghost.id)
+ end
+
+ it 'does not change issues authored by an existing user' do
+ expect { schema_migrate_up! }.not_to change { issues.second.reload.author_id}
+ end
+ end
+ end
+end