summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/diffs/store/actions.js2
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/pageInfo.fragment.graphql4
-rw-r--r--app/assets/javascripts/mr_notes/init_notes.js23
-rw-r--r--app/assets/javascripts/mr_notes/stores/index.js2
-rw-r--r--app/assets/javascripts/mr_notes/stores/modules/index.js4
-rw-r--r--app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue47
-rw-r--r--app/assets/javascripts/notes/stores/getters.js9
-rw-r--r--app/assets/javascripts/repository/queries/getFiles.query.graphql7
-rw-r--r--app/assets/stylesheets/pages/members.scss2
-rw-r--r--app/controllers/groups/group_members_controller.rb13
-rw-r--r--app/models/member.rb4
-rw-r--r--app/views/groups/group_members/index.html.haml78
-rw-r--r--app/views/help/_shortcuts.html.haml10
-rw-r--r--app/views/shared/members/_filter_2fa_dropdown.html.haml4
-rw-r--r--app/views/shared/members/_sort_dropdown.html.haml3
-rw-r--r--changelogs/unreleased/59590-keyboard-shortcut-for-jump-to-next-unresolved-discussion.yml5
-rw-r--r--changelogs/unreleased/sh-break-out-invited-group-members.yml5
-rw-r--r--doc/ci/examples/artifactory_and_gitlab/index.md2
-rw-r--r--doc/ci/examples/code_quality.md2
-rw-r--r--doc/ci/examples/laravel_with_gitlab_and_envoy/index.md2
-rw-r--r--doc/development/documentation/index.md6
-rw-r--r--doc/user/application_security/sast/index.md2
-rw-r--r--doc/user/discussions/index.md5
-rw-r--r--doc/user/project/import/index.md2
-rw-r--r--doc/user/project/import/tfs.md45
-rw-r--r--doc/user/project/import/tfvc.md46
-rw-r--r--doc/user/project/merge_requests/code_quality.md1
-rw-r--r--doc/user/project/merge_requests/code_quality_diff.md1
-rw-r--r--doc/user/project/pages/custom_domains_ssl_tls_certification/index.md2
-rw-r--r--doc/workflow/shortcuts.md2
-rw-r--r--locale/gitlab.pot18
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb3
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb33
-rw-r--r--spec/factories/group_members.rb4
-rw-r--r--spec/features/groups/members/filter_members_spec.rb6
-rw-r--r--spec/features/groups/members/manage_members_spec.rb4
-rw-r--r--spec/features/groups/members/sort_members_spec.rb18
-rw-r--r--spec/features/projects/members/sorting_spec.rb18
-rw-r--r--spec/frontend/notes/components/discussion_keyboard_navigator_spec.js77
-rw-r--r--spec/javascripts/diffs/components/compare_versions_spec.js4
-rw-r--r--spec/javascripts/diffs/components/diff_expansion_cell_spec.js4
-rw-r--r--spec/javascripts/diffs/components/diff_file_spec.js6
-rw-r--r--spec/javascripts/diffs/components/diff_line_gutter_content_spec.js6
-rw-r--r--spec/javascripts/diffs/components/diff_line_note_form_spec.js4
-rw-r--r--spec/javascripts/diffs/components/diff_table_cell_spec.js4
-rw-r--r--spec/javascripts/diffs/components/inline_diff_expansion_row_spec.js4
-rw-r--r--spec/javascripts/diffs/components/inline_diff_table_row_spec.js4
-rw-r--r--spec/javascripts/diffs/components/inline_diff_view_spec.js4
-rw-r--r--spec/javascripts/diffs/components/parallel_diff_expansion_row_spec.js4
-rw-r--r--spec/javascripts/diffs/components/parallel_diff_view_spec.js4
-rw-r--r--spec/javascripts/notes/stores/getters_spec.js48
-rw-r--r--spec/models/member_spec.rb13
52 files changed, 482 insertions, 148 deletions
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 8434ef5cc7a..6695d9fe96c 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -109,7 +109,7 @@ export const toggleLineDiscussions = ({ commit }, options) => {
export const renderFileForDiscussionId = ({ commit, rootState, state }, discussionId) => {
const discussion = rootState.notes.discussions.find(d => d.id === discussionId);
- if (discussion) {
+ if (discussion && discussion.diff_file) {
const file = state.diffFiles.find(f => f.file_hash === discussion.diff_file.file_hash);
if (file) {
diff --git a/app/assets/javascripts/graphql_shared/fragments/pageInfo.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/pageInfo.fragment.graphql
new file mode 100644
index 00000000000..7403fd6d3c2
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/fragments/pageInfo.fragment.graphql
@@ -0,0 +1,4 @@
+fragment PageInfo on PageInfo {
+ hasNextPage
+ endCursor
+}
diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js
index 842a209a545..8caac68e0d4 100644
--- a/app/assets/javascripts/mr_notes/init_notes.js
+++ b/app/assets/javascripts/mr_notes/init_notes.js
@@ -3,6 +3,7 @@ import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import store from 'ee_else_ce/mr_notes/stores';
import notesApp from '../notes/components/notes_app.vue';
+import discussionKeyboardNavigator from '../notes/components/discussion_keyboard_navigator.vue';
export default () => {
// eslint-disable-next-line no-new
@@ -56,15 +57,19 @@ export default () => {
},
},
render(createElement) {
- return createElement('notes-app', {
- props: {
- noteableData: this.noteableData,
- notesData: this.notesData,
- userData: this.currentUserData,
- shouldShow: this.activeTab === 'show',
- helpPagePath: this.helpPagePath,
- },
- });
+ const isDiffView = this.activeTab === 'diffs';
+
+ return createElement(discussionKeyboardNavigator, { props: { isDiffView } }, [
+ createElement('notes-app', {
+ props: {
+ noteableData: this.noteableData,
+ notesData: this.notesData,
+ userData: this.currentUserData,
+ shouldShow: this.activeTab === 'show',
+ helpPagePath: this.helpPagePath,
+ },
+ }),
+ ]);
},
});
};
diff --git a/app/assets/javascripts/mr_notes/stores/index.js b/app/assets/javascripts/mr_notes/stores/index.js
index c4225c8ec08..8fbd0291a7d 100644
--- a/app/assets/javascripts/mr_notes/stores/index.js
+++ b/app/assets/javascripts/mr_notes/stores/index.js
@@ -9,7 +9,7 @@ Vue.use(Vuex);
export const createStore = () =>
new Vuex.Store({
modules: {
- page: mrPageModule,
+ page: mrPageModule(),
notes: notesModule(),
diffs: diffsModule(),
},
diff --git a/app/assets/javascripts/mr_notes/stores/modules/index.js b/app/assets/javascripts/mr_notes/stores/modules/index.js
index 660081f76c8..c28e666943b 100644
--- a/app/assets/javascripts/mr_notes/stores/modules/index.js
+++ b/app/assets/javascripts/mr_notes/stores/modules/index.js
@@ -2,11 +2,11 @@ import actions from '../actions';
import getters from '../getters';
import mutations from '../mutations';
-export default {
+export default () => ({
state: {
activeTab: null,
},
actions,
getters,
mutations,
-};
+});
diff --git a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
new file mode 100644
index 00000000000..5fc2b6ba04c
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
@@ -0,0 +1,47 @@
+<script>
+/* global Mousetrap */
+import 'mousetrap';
+import { mapGetters, mapActions } from 'vuex';
+import discussionNavigation from '~/notes/mixins/discussion_navigation';
+
+export default {
+ mixins: [discussionNavigation],
+ props: {
+ isDiffView: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ currentDiscussionId: null,
+ };
+ },
+ computed: {
+ ...mapGetters(['nextUnresolvedDiscussionId', 'previousUnresolvedDiscussionId']),
+ },
+ mounted() {
+ Mousetrap.bind('n', () => this.jumpToNextDiscussion());
+ Mousetrap.bind('p', () => this.jumpToPreviousDiscussion());
+ },
+ methods: {
+ ...mapActions(['expandDiscussion']),
+ jumpToNextDiscussion() {
+ const nextId = this.nextUnresolvedDiscussionId(this.currentDiscussionId, this.isDiffView);
+
+ this.jumpToDiscussion(nextId);
+ this.currentDiscussionId = nextId;
+ },
+ jumpToPreviousDiscussion() {
+ const prevId = this.previousUnresolvedDiscussionId(this.currentDiscussionId, this.isDiffView);
+
+ this.jumpToDiscussion(prevId);
+ this.currentDiscussionId = prevId;
+ },
+ },
+ render() {
+ return this.$slots.default;
+ },
+};
+</script>
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 8aa8f5037b3..52410f18d4a 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -183,6 +183,15 @@ export const nextUnresolvedDiscussionId = (state, getters) => (discussionId, dif
return slicedIds.length ? idsOrdered.slice(currentIndex + 1, currentIndex + 2)[0] : idsOrdered[0];
};
+export const previousUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) => {
+ const idsOrdered = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
+ const currentIndex = idsOrdered.indexOf(discussionId);
+ const slicedIds = idsOrdered.slice(currentIndex - 1, currentIndex);
+
+ // Get the last ID if there is none after the currentIndex
+ return slicedIds.length ? slicedIds[0] : idsOrdered[idsOrdered.length - 1];
+};
+
// @param {Boolean} diffOrder - is ordered by diff?
export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => {
if (diffOrder) {
diff --git a/app/assets/javascripts/repository/queries/getFiles.query.graphql b/app/assets/javascripts/repository/queries/getFiles.query.graphql
index b3cc0878cad..c4814f8e63a 100644
--- a/app/assets/javascripts/repository/queries/getFiles.query.graphql
+++ b/app/assets/javascripts/repository/queries/getFiles.query.graphql
@@ -1,3 +1,5 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+
fragment TreeEntry on Entry {
id
name
@@ -5,11 +7,6 @@ fragment TreeEntry on Entry {
type
}
-fragment PageInfo on PageInfo {
- hasNextPage
- endCursor
-}
-
query getFiles(
$projectPath: ID!
$path: String
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index d54d264bc5d..45408c9ab3c 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -67,7 +67,7 @@
position: absolute;
right: 4px;
top: 0;
- height: 35px;
+ height: $input-height;
padding-left: 10px;
padding-right: 10px;
color: $gray-darkest;
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index f1d6fb00cfc..557888711ec 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -5,6 +5,8 @@ class Groups::GroupMembersController < Groups::ApplicationController
include MembersPresentation
include SortingHelper
+ MEMBER_PER_PAGE_LIMIT = 50
+
def self.admin_not_required_endpoints
%i[index leave request_access]
end
@@ -24,7 +26,14 @@ class Groups::GroupMembersController < Groups::ApplicationController
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = GroupMembersFinder.new(@group).execute
- @members = @members.non_invite unless can_manage_members
+
+ if can_manage_members
+ @invited_members = @members.invite
+ @invited_members = @invited_members.search_invite_email(params[:search_invited]) if params[:search_invited].present?
+ @invited_members = present_members(@invited_members.page(params[:invited_members_page]).per(MEMBER_PER_PAGE_LIMIT))
+ end
+
+ @members = @members.non_invite
@members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort_by_attribute(@sort)
@@ -32,7 +41,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
@members = @members.filter_by_2fa(params[:two_factor])
end
- @members = @members.page(params[:page]).per(50)
+ @members = @members.page(params[:page]).per(MEMBER_PER_PAGE_LIMIT)
@members = present_members(@members)
@requesters = present_members(
diff --git a/app/models/member.rb b/app/models/member.rb
index c7583434148..dbae1076670 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -107,6 +107,10 @@ class Member < ApplicationRecord
joins(:user).merge(User.search(query))
end
+ def search_invite_email(query)
+ invite.where(['invite_email ILIKE ?', "%#{query}%"])
+ end
+
def filter_by_2fa(value)
case value
when 'enabled'
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 2b8c9f65d43..882fcc79421 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,39 +1,67 @@
-- page_title "Members"
+- page_title _("Members")
- can_manage_members = can?(current_user, :admin_group_member, @group)
+- show_invited_members = can_manage_members && @invited_members.exists?
+- pending_active = params[:search_invited].present?
.project-members-page.prepend-top-default
%h4
- Members
+ = _("Members")
%hr
- if can_manage_members
.project-members-new.append-bottom-default
%p.clearfix
- Add new member to
- %strong= @group.name
+ = _("Add new member to %{strong_start}%{group_name}%{strong_end}").html_safe % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
= render "new_group_member"
= render 'shared/members/requests', membership_source: @group, requesters: @requesters
= render_if_exists 'groups/group_members/ldap_sync'
- .clearfix
- %h5.member.existing-title
- Existing members
- .card
- .card-header.flex-project-members-panel
- %span.flex-project-title
- Members with access to
- %strong= @group.name
- %span.badge.badge-pill= @members.total_count
- = form_tag group_group_members_path(@group), method: :get, class: 'form-inline user-search-form flex-users-form' do
- .form-group
- .position-relative.append-right-8
- = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
- %button.user-search-btn{ type: "submit", "aria-label" => "Submit search" }
- = icon("search")
- - if can_manage_members
- = render 'shared/members/filter_2fa_dropdown'
- = render 'shared/members/sort_dropdown'
- %ul.content-list.members-list
- = render partial: 'shared/members/member', collection: @members, as: :member
- = paginate @members, theme: 'gitlab'
+ %ul.nav-links.mobile-separator.nav.nav-tabs.clearfix
+ %li.nav-item
+ = link_to "#existing_members", class: ["nav-link", ("active" unless pending_active)] , 'data-toggle' => 'tab' do
+ %span
+ = _("Existing")
+ %span.badge.badge-pill= @members.total_count
+ - if show_invited_members
+ %li.nav-item
+ = link_to "#invited_members", class: ["nav-link", ("active" if pending_active)], 'data-toggle' => 'tab' do
+ %span
+ = _("Pending")
+ %span.badge.badge-pill= @invited_members.total_count
+
+ .tab-content
+ #existing_members.tab-pane{ :class => ("active" unless pending_active) }
+ .card.card-without-border
+ .d-flex.flex-column.flex-md-row.row-content-block.second-block
+ %span.flex-grow-1.align-self-md-center.col-form-label
+ = _("Members with access to %{strong_start}%{group_name}%{strong_end}").html_safe % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ = form_tag group_group_members_path(@group), method: :get, class: 'form-inline user-search-form' do
+ .form-group.flex-grow
+ .position-relative.mr-md-2
+ = search_field_tag :search, params[:search], { placeholder: _('Search'), class: 'form-control', spellcheck: false }
+ %button.user-search-btn.border-left{ type: "submit", "aria-label" => _("Submit search") }
+ = icon("search")
+ - if can_manage_members
+ = label_tag '2fa', '2FA', class: 'col-form-label label-bold pr-md-2'
+ = render 'shared/members/filter_2fa_dropdown'
+ = render 'shared/members/sort_dropdown'
+ %ul.content-list.members-list
+ = render partial: 'shared/members/member', collection: @members, as: :member
+ = paginate @members, theme: 'gitlab'
+
+ - if show_invited_members
+ #invited_members.tab-pane{ :class => ("active" if pending_active) }
+ .card.card-without-border
+ .d-flex.flex-column.flex-md-row.row-content-block.second-block
+ %span.flex-grow-1
+ = _("Members with pending access to %{strong_start}%{group_name}%{strong_end}").html_safe % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ = form_tag group_group_members_path(@group), method: :get, class: 'form-inline user-search-form' do
+ .form-group
+ .position-relative.mr-md-2
+ = search_field_tag :search_invited, params[:search_invited], { placeholder: _('Search'), class: 'form-control', spellcheck: false }
+ %button.user-search-btn.border-left{ type: "submit", "aria-label" => _("Submit search") }
+ = icon("search")
+ %ul.content-list.members-list
+ = render partial: 'shared/members/member', collection: @invited_members, as: :member
+ = paginate @invited_members, param_name: 'invited_members_page', theme: 'gitlab'
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 46d7c367aa7..0624f66b3a5 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -332,7 +332,7 @@
%td.shortcut
%kbd l
%td Change Label
- %tbody.hidden-shortcut{ style: 'display:none' }
+ %tbody.hidden-shortcut.merge_requests{ style: 'display:none' }
%tr
%th
%th Merge Requests
@@ -368,6 +368,14 @@
\/
%kbd k
%td Move to previous file
+ %tr
+ %td.shortcut
+ %kbd n
+ %td= _("Move to next unresolved discussion")
+ %tr
+ %td.shortcut
+ %kbd p
+ %td= _("Move to previous unresolved discussion")
%tbody.hidden-shortcut{ style: 'display:none' }
%tr
%th
diff --git a/app/views/shared/members/_filter_2fa_dropdown.html.haml b/app/views/shared/members/_filter_2fa_dropdown.html.haml
index 3e98587aeaa..44ea844028e 100644
--- a/app/views/shared/members/_filter_2fa_dropdown.html.haml
+++ b/app/views/shared/members/_filter_2fa_dropdown.html.haml
@@ -1,7 +1,7 @@
- filter = params[:two_factor] || 'everyone'
- filter_options = { 'everyone' => _('Everyone'), 'enabled' => _('Enabled'), 'disabled' => _('Disabled') }
-.dropdown.inline.member-filter-2fa-dropdown
- = dropdown_toggle('2FA: ' + filter_options[filter], { toggle: 'dropdown' })
+.dropdown.inline.member-filter-2fa-dropdown.pr-md-2
+ = dropdown_toggle(filter_options[filter], { toggle: 'dropdown' })
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
%li.dropdown-header
= _("Filter by two-factor authentication")
diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml
index f5ebab035db..feca109dade 100644
--- a/app/views/shared/members/_sort_dropdown.html.haml
+++ b/app/views/shared/members/_sort_dropdown.html.haml
@@ -1,4 +1,5 @@
-.dropdown.inline.user-sort-dropdown
+= label_tag :sort_by, 'Sort by', class: 'col-form-label label-bold pr-2'
+.dropdown.inline.qa-user-sort-dropdown
= dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' })
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
diff --git a/changelogs/unreleased/59590-keyboard-shortcut-for-jump-to-next-unresolved-discussion.yml b/changelogs/unreleased/59590-keyboard-shortcut-for-jump-to-next-unresolved-discussion.yml
new file mode 100644
index 00000000000..02e81c7fc87
--- /dev/null
+++ b/changelogs/unreleased/59590-keyboard-shortcut-for-jump-to-next-unresolved-discussion.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Keyboard shortcut for jump to NEXT unresolved discussion
+merge_request: 30144
+author:
+type: added
diff --git a/changelogs/unreleased/sh-break-out-invited-group-members.yml b/changelogs/unreleased/sh-break-out-invited-group-members.yml
new file mode 100644
index 00000000000..091f1d48843
--- /dev/null
+++ b/changelogs/unreleased/sh-break-out-invited-group-members.yml
@@ -0,0 +1,5 @@
+---
+title: Make it easier to find invited group members
+merge_request: 28436
+author:
+type: fixed
diff --git a/doc/ci/examples/artifactory_and_gitlab/index.md b/doc/ci/examples/artifactory_and_gitlab/index.md
index 940c4711132..e5f307e570d 100644
--- a/doc/ci/examples/artifactory_and_gitlab/index.md
+++ b/doc/ci/examples/artifactory_and_gitlab/index.md
@@ -1,5 +1,5 @@
---
-redirect_from: 'https://docs.gitlab.com/ee/articles/artifactory_and_gitlab/index.html'
+disqus_identifier: 'https://docs.gitlab.com/ee/articles/artifactory_and_gitlab/index.html'
author: Fabio Busatto
author_gitlab: bikebilly
level: intermediate
diff --git a/doc/ci/examples/code_quality.md b/doc/ci/examples/code_quality.md
index 69bad6b4c25..9c65de115b4 100644
--- a/doc/ci/examples/code_quality.md
+++ b/doc/ci/examples/code_quality.md
@@ -1,5 +1,5 @@
---
-redirect_from: 'https://docs.gitlab.com/ee/ci/examples/code_climate.html'
+disqus_identifier: 'https://docs.gitlab.com/ee/ci/examples/code_climate.html'
type: reference, howto
---
diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
index 1576efd5a7d..808e4285f2f 100644
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
+++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
@@ -1,5 +1,5 @@
---
-redirect_from: 'https://docs.gitlab.com/ee/articles/laravel_with_gitlab_and_envoy/index.html'
+disqus_identifier: 'https://docs.gitlab.com/ee/articles/laravel_with_gitlab_and_envoy/index.html'
author: Mehran Rasulian
author_gitlab: mehranrasulian
level: intermediate
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index 19b26618882..c9ae00d148a 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -170,7 +170,7 @@ Disqus uses an identifier per page, and for docs.gitlab.com, the page identifier
is configured to be the page URL. Therefore, when we change the document location,
we need to preserve the old URL as the same Disqus identifier.
-To do that, add to the frontmatter the variable `redirect_from`,
+To do that, add to the frontmatter the variable `disqus_identifier`,
using the old URL as value. For example, let's say I moved the document
available under `https://docs.gitlab.com/my-old-location/README.html` to a new location,
`https://docs.gitlab.com/my-new-location/index.html`.
@@ -179,11 +179,11 @@ Into the **new document** frontmatter add the following:
```yaml
---
-redirect_from: 'https://docs.gitlab.com/my-old-location/README.html'
+disqus_identifier: 'https://docs.gitlab.com/my-old-location/README.html'
---
```
-Note: it is necessary to include the file name in the `redirect_from` URL,
+Note: it is necessary to include the file name in the `disqus_identifier` URL,
even if it's `index.html` or `README.html`.
## Branch naming
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index 5149f628345..2f15d997b5b 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -164,7 +164,7 @@ Some analyzers make it possible to filter out vulnerabilities under a given thre
| `SAST_BRAKEMAN_LEVEL` | 1 | Ignore Brakeman vulnerabilities under given confidence level. Integer, 1=Low 3=High. |
| `SAST_FLAWFINDER_LEVEL` | 1 | Ignore Flawfinder vulnerabilities under given risk level. Integer, 0=No risk, 5=High risk. |
| `SAST_GITLEAKS_ENTROPY_LEVEL` | 8.0 | Minimum entropy for secret detection. Float, 0.0 = low, 8.0 = high. |
-| `SAST_GOSEC_LEVEL` | 0 | Ignore gosec vulnerabilities under given confidence level. Integer, 0=Undefined, 1=Low, 1=Medium, 3=High. |
+| `SAST_GOSEC_LEVEL` | 0 | Ignore gosec vulnerabilities under given confidence level. Integer, 0=Undefined, 1=Low, 2=Medium, 3=High. |
| `SAST_EXCLUDED_PATHS` | - | Exclude vulnerabilities from output based on the paths. This is a comma-separated list of patterns. Patterns can be globs, file or folder paths. Parent directories will also match patterns. |
### Timeouts
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 3cb765c0463..f844f56557f 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -88,6 +88,11 @@ Jump button next to the Reply field on a thread.
You can also jump to the first unresolved thread from the button next to the
resolved threads tracker.
+You can also use keyboard shortcuts to navigate among threads:
+
+- Use <kbd>n</kbd> to jump to the next unresolved thread.
+- Use <kbd>p</kbd> to jump to the previous unresolved thread.
+
!["8/9 threads resolved"](img/threads_resolved.png)
### Marking a comment or thread as resolved
diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md
index 9d7463416d8..ecd491d8e5b 100644
--- a/doc/user/project/import/index.md
+++ b/doc/user/project/import/index.md
@@ -10,7 +10,7 @@
1. [From Gitea](gitea.md)
1. [From Perforce](perforce.md)
1. [From SVN](svn.md)
-1. [From TFS](tfs.md)
+1. [From TFVC](tfvc.md)
1. [From repo by URL](repo_by_url.md)
1. [By uploading a manifest file (AOSP)](manifest.md)
1. [From Gemnasium](gemnasium.md)
diff --git a/doc/user/project/import/tfs.md b/doc/user/project/import/tfs.md
index 01bbb7e6ffd..7b3b11b9519 100644
--- a/doc/user/project/import/tfs.md
+++ b/doc/user/project/import/tfs.md
@@ -1,42 +1,5 @@
-# Migrating from TFS
+---
+redirect_to: 'tfvc.md'
+---
-[TFS](https://azure.microsoft.com/en-us/services/devops/server/) is a set of tools developed by Microsoft
-which also includes a centralized version control system (TFVC) similar to Git.
-
-In this document, we emphasize on the TFVC to Git migration.
-
-## TFVC vs Git
-
-The following list illustrates the main differences between TFVC and Git:
-
-- **Git is distributed** whereas TFVC is centralized using a client-server
- architecture. This translates to Git having a more flexible workflow since
- your working area is a copy of the entire repository. This decreases the
- overhead when switching branches or merging for example, since you don't have
- to communicate with a remote server.
-- **Storage method.** Changes in CVS are per file (changeset), while in Git
- a committed file(s) is stored in its entirety (snapshot). That means that's
- very easy in Git to revert or undo a whole change.
-
-Check also Microsoft's documentation on the
-[comparison of Git and TFVC](https://docs.microsoft.com/en-us/azure/devops/repos/tfvc/comparison-git-tfvc?view=azure-devops)
-and the Wikipedia
-[comparison of version control software](https://en.wikipedia.org/wiki/Comparison_of_version_control_software).
-
-## Why migrate
-
-Migrating to Git/GitLab there is:
-
-- **No licensing costs**, Git is GPL while TFVC is proprietary.
-- **Shorter learning curve**, Git has a big community and a vast number of
- tutorials to get you started (see our [Git topic](../../../topics/git/index.md)).
-- **Integration with modern tools**, migrating to Git and GitLab you can have
- an open source end-to-end software development platform with built-in version
- control, issue tracking, code review, CI/CD, and more.
-
-## How to migrate
-
-The best option to migrate from TFVC to Git is to use the
-[`git-tfs`](https://github.com/git-tfs/git-tfs) tool. A specific guide for the
-migration exists:
-[Migrate TFS to Git](https://github.com/git-tfs/git-tfs/blob/master/doc/usecases/migrate_tfs_to_git.md).
+This document was moved to [another location](tfvc.md).
diff --git a/doc/user/project/import/tfvc.md b/doc/user/project/import/tfvc.md
new file mode 100644
index 00000000000..375522b77d0
--- /dev/null
+++ b/doc/user/project/import/tfvc.md
@@ -0,0 +1,46 @@
+---
+type: concepts
+---
+
+# Migrating from TFVC to Git
+
+Team Foundation Server (TFS), renamed [Azure DevOps Server](https://azure.microsoft.com/en-us/services/devops/server/)
+in 2019, is a set of tools developed by Microsoft which also includes
+[Team Foundation Version Control](https://docs.microsoft.com/en-us/azure/devops/repos/tfvc/overview)
+(TFVC), a centralized version control system similar to Git.
+
+In this document, we focus on the TFVC to Git migration.
+
+## TFVC vs Git
+
+The main differences between TFVC and Git are:
+
+- **Git is distributed:** While TFVC is centralized using a client-server architecture,
+ Git is distributed. This translates to Git having a more flexible workflow since
+ you work with a copy of the entire repository. This allows you to quickly
+ switch branches or merge, for example, without needing to communicate with a remote server.
+- **Storage:** Changes in a centralized version control system are per file (changeset),
+ while in Git a committed file is stored in its entirety (snapshot). That means that it is
+ very easy to revert or undo a whole change in Git.
+
+For more information, see:
+
+- Microsoft's [comparison of Git and TFVC](https://docs.microsoft.com/en-us/azure/devops/repos/tfvc/comparison-git-tfvc?view=azure-devops).
+- The Wikipedia [comparison of version control software](https://en.wikipedia.org/wiki/Comparison_of_version_control_software).
+
+## Why migrate
+
+Advantages of migrating to Git/GitLab:
+
+- **No licensing costs:** Git is open source, while TFVC is proprietary.
+- **Shorter learning curve:** Git has a big community and a vast number of
+ tutorials to get you started (see our [Git topic](../../../topics/git/index.md)).
+- **Integration with modern tools:** After migrating to Git and GitLab, you will have
+ an open source, end-to-end software development platform with built-in version
+ control, issue tracking, code review, CI/CD, and more.
+
+## How to migrate
+
+The best option to migrate from TFVC to Git is to use the [`git-tfs`](https://github.com/git-tfs/git-tfs)
+tool. Read the [Migrate TFS to Git](https://github.com/git-tfs/git-tfs/blob/master/doc/usecases/migrate_tfs_to_git.md)
+guide for more details.
diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md
index eb6e454062a..3a409bab19d 100644
--- a/doc/user/project/merge_requests/code_quality.md
+++ b/doc/user/project/merge_requests/code_quality.md
@@ -1,5 +1,6 @@
---
type: reference, howto
+disqus_identifier: 'https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html'
---
# Code Quality **(STARTER)**
diff --git a/doc/user/project/merge_requests/code_quality_diff.md b/doc/user/project/merge_requests/code_quality_diff.md
index ccc694672a6..0aa108a2e73 100644
--- a/doc/user/project/merge_requests/code_quality_diff.md
+++ b/doc/user/project/merge_requests/code_quality_diff.md
@@ -1,5 +1,4 @@
---
-redirect_from: 'code_quality_diff.md'
redirect_to: 'code_quality.md'
---
diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md
index 29f8e3fa0f7..1821d954af3 100644
--- a/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md
+++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md
@@ -1,7 +1,7 @@
---
last_updated: 2019-07-04
type: reference, howto
-redirect_from: 'https://docs.gitlab.com/ee/user/project/pages/getting_started_part_three.html'
+disqus_identifier: 'https://docs.gitlab.com/ee/user/project/pages/getting_started_part_three.html'
---
# Custom domains and SSL/TLS Certificates
diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index d61d7eafd18..5d08bf5e77d 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -84,6 +84,8 @@ You can see GitLab's keyboard shortcuts by using <kbd>shift</kbd> + <kbd>?</kbd>
| <kbd>l</kbd> | Change label |
| <kbd>]</kbd> or <kbd>j</kbd> | Move to next file |
| <kbd>[</kbd> or <kbd>k</kbd> | Move to previous file |
+| <kbd>n</kbd> | Move to next unresolved discussion |
+| <kbd>p</kbd> | Move to previous unresolved discussion |
## Epics **(ULTIMATE)**
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 7dd6e0f2a93..b96ceb970ae 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -714,6 +714,9 @@ msgstr ""
msgid "Add new directory"
msgstr ""
+msgid "Add new member to %{strong_start}%{group_name}%{strong_end}"
+msgstr ""
+
msgid "Add or subtract spent time"
msgstr ""
@@ -4643,6 +4646,9 @@ msgstr ""
msgid "Except policy:"
msgstr ""
+msgid "Existing"
+msgstr ""
+
msgid "Existing members and groups"
msgstr ""
@@ -6625,6 +6631,12 @@ msgstr ""
msgid "Members of <strong>%{project_name}</strong>"
msgstr ""
+msgid "Members with access to %{strong_start}%{group_name}%{strong_end}"
+msgstr ""
+
+msgid "Members with pending access to %{strong_start}%{group_name}%{strong_end}"
+msgstr ""
+
msgid "Merge"
msgstr ""
@@ -6952,6 +6964,12 @@ msgstr ""
msgid "Move this issue to another project."
msgstr ""
+msgid "Move to next unresolved discussion"
+msgstr ""
+
+msgid "Move to previous unresolved discussion"
+msgstr ""
+
msgid "MoveIssue|Cannot move issue due to insufficient permissions!"
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb
index 680c5e21fa4..f915d412bf3 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb
@@ -2,8 +2,7 @@
module QA
context 'Create' do
- # failure reported: https://gitlab.com/gitlab-org/quality/nightly/issues/42
- describe 'Commit data', :quarantine do
+ describe 'Commit data' do
before(:context) do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 413598ddde0..908c564e761 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -16,6 +16,39 @@ describe Groups::GroupMembersController do
expect(response).to have_gitlab_http_status(200)
expect(response).to render_template(:index)
end
+
+ context 'user with owner access' do
+ let!(:invited) { create_list(:group_member, 3, :invited, group: group) }
+
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it 'assigns invited members' do
+ get :index, params: { group_id: group }
+
+ expect(assigns(:invited_members).map(&:invite_email)).to match_array(invited.map(&:invite_email))
+ end
+
+ it 'restricts search to one email' do
+ get :index, params: { group_id: group, search_invited: invited.first.invite_email }
+
+ expect(assigns(:invited_members).map(&:invite_email)).to match_array(invited.first.invite_email)
+ end
+
+ it 'paginates invited list' do
+ stub_const('Groups::GroupMembersController::MEMBER_PER_PAGE_LIMIT', 2)
+
+ get :index, params: { group_id: group, invited_members_page: 1 }
+
+ expect(assigns(:invited_members).count).to eq(2)
+
+ get :index, params: { group_id: group, invited_members_page: 2 }
+
+ expect(assigns(:invited_members).count).to eq(1)
+ end
+ end
end
describe 'POST create' do
diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb
index 3bf9cdef253..8dab6c71b06 100644
--- a/spec/factories/group_members.rb
+++ b/spec/factories/group_members.rb
@@ -16,7 +16,9 @@ FactoryBot.define do
trait(:invited) do
user_id nil
invite_token 'xxx'
- invite_email 'email@email.com'
+ sequence :invite_email do |n|
+ "email#{n}@email.com"
+ end
end
end
end
diff --git a/spec/features/groups/members/filter_members_spec.rb b/spec/features/groups/members/filter_members_spec.rb
index 7aad66ec08f..fc62c92db4e 100644
--- a/spec/features/groups/members/filter_members_spec.rb
+++ b/spec/features/groups/members/filter_members_spec.rb
@@ -19,7 +19,7 @@ describe 'Groups > Members > Filter members' do
expect(first_member).to include(user.name)
expect(second_member).to include(user_with_2fa.name)
- expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Everyone')
+ expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: 'Everyone')
end
it 'shows only 2FA members' do
@@ -27,7 +27,7 @@ describe 'Groups > Members > Filter members' do
expect(first_member).to include(user_with_2fa.name)
expect(members_list.size).to eq(1)
- expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Enabled')
+ expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: 'Enabled')
end
it 'shows only non 2FA members' do
@@ -35,7 +35,7 @@ describe 'Groups > Members > Filter members' do
expect(first_member).to include(user.name)
expect(members_list.size).to eq(1)
- expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Disabled')
+ expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: 'Disabled')
end
def visit_members_list(options = {})
diff --git a/spec/features/groups/members/manage_members_spec.rb b/spec/features/groups/members/manage_members_spec.rb
index 779fa74501a..cdd16ae9441 100644
--- a/spec/features/groups/members/manage_members_spec.rb
+++ b/spec/features/groups/members/manage_members_spec.rb
@@ -98,7 +98,9 @@ describe 'Groups > Members > Manage members' do
add_user('test@example.com', 'Reporter')
- page.within(second_row) do
+ click_link('Pending')
+
+ page.within('.content-list.members-list') do
expect(page).to have_content('test@example.com')
expect(page).to have_content('Invited')
expect(page).to have_button('Reporter')
diff --git a/spec/features/groups/members/sort_members_spec.rb b/spec/features/groups/members/sort_members_spec.rb
index 48b0136227e..76709199942 100644
--- a/spec/features/groups/members/sort_members_spec.rb
+++ b/spec/features/groups/members/sort_members_spec.rb
@@ -19,7 +19,7 @@ describe 'Groups > Members > Sort members' do
expect(first_member).to include(owner.name)
expect(second_member).to include(developer.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
end
it 'sorts by access level ascending' do
@@ -27,7 +27,7 @@ describe 'Groups > Members > Sort members' do
expect(first_member).to include(developer.name)
expect(second_member).to include(owner.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
end
it 'sorts by access level descending' do
@@ -35,7 +35,7 @@ describe 'Groups > Members > Sort members' do
expect(first_member).to include(owner.name)
expect(second_member).to include(developer.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
end
it 'sorts by last joined' do
@@ -43,7 +43,7 @@ describe 'Groups > Members > Sort members' do
expect(first_member).to include(developer.name)
expect(second_member).to include(owner.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
end
it 'sorts by oldest joined' do
@@ -51,7 +51,7 @@ describe 'Groups > Members > Sort members' do
expect(first_member).to include(owner.name)
expect(second_member).to include(developer.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
end
it 'sorts by name ascending' do
@@ -59,7 +59,7 @@ describe 'Groups > Members > Sort members' do
expect(first_member).to include(owner.name)
expect(second_member).to include(developer.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
end
it 'sorts by name descending' do
@@ -67,7 +67,7 @@ describe 'Groups > Members > Sort members' do
expect(first_member).to include(developer.name)
expect(second_member).to include(owner.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
end
it 'sorts by recent sign in', :clean_gitlab_redis_shared_state do
@@ -75,7 +75,7 @@ describe 'Groups > Members > Sort members' do
expect(first_member).to include(owner.name)
expect(second_member).to include(developer.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
end
it 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do
@@ -83,7 +83,7 @@ describe 'Groups > Members > Sort members' do
expect(first_member).to include(developer.name)
expect(second_member).to include(owner.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
end
def visit_members_list(sort:)
diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb
index 88240fbbedc..12f485317d8 100644
--- a/spec/features/projects/members/sorting_spec.rb
+++ b/spec/features/projects/members/sorting_spec.rb
@@ -18,7 +18,7 @@ describe 'Projects > Members > Sorting' do
expect(first_member).to include(maintainer.name)
expect(second_member).to include(developer.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
end
it 'sorts by access level ascending' do
@@ -26,7 +26,7 @@ describe 'Projects > Members > Sorting' do
expect(first_member).to include(developer.name)
expect(second_member).to include(maintainer.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
end
it 'sorts by access level descending' do
@@ -34,7 +34,7 @@ describe 'Projects > Members > Sorting' do
expect(first_member).to include(maintainer.name)
expect(second_member).to include(developer.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
end
it 'sorts by last joined' do
@@ -42,7 +42,7 @@ describe 'Projects > Members > Sorting' do
expect(first_member).to include(maintainer.name)
expect(second_member).to include(developer.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
end
it 'sorts by oldest joined' do
@@ -50,7 +50,7 @@ describe 'Projects > Members > Sorting' do
expect(first_member).to include(developer.name)
expect(second_member).to include(maintainer.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
end
it 'sorts by name ascending' do
@@ -58,7 +58,7 @@ describe 'Projects > Members > Sorting' do
expect(first_member).to include(maintainer.name)
expect(second_member).to include(developer.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
end
it 'sorts by name descending' do
@@ -66,7 +66,7 @@ describe 'Projects > Members > Sorting' do
expect(first_member).to include(developer.name)
expect(second_member).to include(maintainer.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
end
it 'sorts by recent sign in', :clean_gitlab_redis_shared_state do
@@ -74,7 +74,7 @@ describe 'Projects > Members > Sorting' do
expect(first_member).to include(maintainer.name)
expect(second_member).to include(developer.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
end
it 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do
@@ -82,7 +82,7 @@ describe 'Projects > Members > Sorting' do
expect(first_member).to include(developer.name)
expect(second_member).to include(maintainer.name)
- expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
end
def visit_members_list(sort:)
diff --git a/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js b/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js
new file mode 100644
index 00000000000..6d50713999d
--- /dev/null
+++ b/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js
@@ -0,0 +1,77 @@
+/* global Mousetrap */
+import 'mousetrap';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import DiscussionKeyboardNavigator from '~/notes/components/discussion_keyboard_navigator.vue';
+import notesModule from '~/notes/stores/modules';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const NEXT_ID = 'abc123';
+const PREV_ID = 'def456';
+const NEXT_DIFF_ID = 'abc123_diff';
+const PREV_DIFF_ID = 'def456_diff';
+
+describe('notes/components/discussion_keyboard_navigator', () => {
+ let storeOptions;
+ let wrapper;
+ let store;
+
+ const createComponent = (options = {}) => {
+ store = new Vuex.Store(storeOptions);
+
+ wrapper = shallowMount(DiscussionKeyboardNavigator, {
+ localVue,
+ store,
+ ...options,
+ });
+
+ wrapper.vm.jumpToDiscussion = jest.fn();
+ };
+
+ beforeEach(() => {
+ const notes = notesModule();
+
+ notes.getters.nextUnresolvedDiscussionId = () => (currId, isDiff) =>
+ isDiff ? NEXT_DIFF_ID : NEXT_ID;
+ notes.getters.previousUnresolvedDiscussionId = () => (currId, isDiff) =>
+ isDiff ? PREV_DIFF_ID : PREV_ID;
+
+ storeOptions = {
+ modules: {
+ notes,
+ },
+ };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ storeOptions = null;
+ store = null;
+ });
+
+ describe.each`
+ isDiffView | expectedNextId | expectedPrevId
+ ${true} | ${NEXT_DIFF_ID} | ${PREV_DIFF_ID}
+ ${false} | ${NEXT_ID} | ${PREV_ID}
+ `('when isDiffView is $isDiffView', ({ isDiffView, expectedNextId, expectedPrevId }) => {
+ beforeEach(() => {
+ createComponent({ propsData: { isDiffView } });
+ });
+
+ it('calls jumpToNextDiscussion when pressing `n`', () => {
+ Mousetrap.trigger('n');
+
+ expect(wrapper.vm.jumpToDiscussion).toHaveBeenCalledWith(expectedNextId);
+ expect(wrapper.vm.currentDiscussionId).toEqual(expectedNextId);
+ });
+
+ it('calls jumpToPreviousDiscussion when pressing `p`', () => {
+ Mousetrap.trigger('p');
+
+ expect(wrapper.vm.jumpToDiscussion).toHaveBeenCalledWith(expectedPrevId);
+ expect(wrapper.vm.currentDiscussionId).toEqual(expectedPrevId);
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js
index 77f8352047c..ef4bb470734 100644
--- a/spec/javascripts/diffs/components/compare_versions_spec.js
+++ b/spec/javascripts/diffs/components/compare_versions_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import CompareVersionsComponent from '~/diffs/components/compare_versions.vue';
-import store from '~/mr_notes/stores';
+import { createStore } from '~/mr_notes/stores';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffsMockData from '../mock_data/merge_request_diffs';
import getDiffWithCommit from '../mock_data/diff_with_commit';
@@ -10,6 +10,8 @@ describe('CompareVersions', () => {
const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 };
beforeEach(() => {
+ const store = createStore();
+
store.state.diffs.addedLines = 10;
store.state.diffs.removedLines = 20;
store.state.diffs.diffFiles.push('test');
diff --git a/spec/javascripts/diffs/components/diff_expansion_cell_spec.js b/spec/javascripts/diffs/components/diff_expansion_cell_spec.js
index cb1966b2122..63c50c09fce 100644
--- a/spec/javascripts/diffs/components/diff_expansion_cell_spec.js
+++ b/spec/javascripts/diffs/components/diff_expansion_cell_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import store from '~/mr_notes/stores';
+import { createStore } from '~/mr_notes/stores';
import DiffExpansionCell from '~/diffs/components/diff_expansion_cell.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffFileMockData from '../mock_data/diff_file';
@@ -22,7 +22,7 @@ describe('DiffExpansionCell', () => {
};
const props = Object.assign({}, defaults, options);
- return createComponentWithStore(cmp, store, props).$mount();
+ return createComponentWithStore(cmp, createStore(), props).$mount();
};
describe('top row', () => {
diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js
index 18da0a473d6..3ca2d1dc934 100644
--- a/spec/javascripts/diffs/components/diff_file_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import DiffFileComponent from '~/diffs/components/diff_file.vue';
import { diffViewerModes, diffViewerErrors } from '~/ide/constants';
-import store from 'ee_else_ce/mr_notes/stores';
+import { createStore } from 'ee_else_ce/mr_notes/stores';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffFileMockData from '../mock_data/diff_file';
@@ -9,7 +9,7 @@ describe('DiffFile', () => {
let vm;
beforeEach(() => {
- vm = createComponentWithStore(Vue.extend(DiffFileComponent), store, {
+ vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), {
file: JSON.parse(JSON.stringify(diffFileMockData)),
canCurrentUserFork: false,
}).$mount();
@@ -36,7 +36,7 @@ describe('DiffFile', () => {
vm.$nextTick()
.then(() => {
- expect(el.querySelectorAll('.line_content').length).toBeGreaterThanOrEqual(5);
+ expect(el.querySelectorAll('.line_content').length).toBe(5);
expect(el.querySelectorAll('.js-line-expansion-content').length).toBe(1);
})
.then(done)
diff --git a/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js
index 99b5496c24b..6bb704658fb 100644
--- a/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js
+++ b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import DiffLineGutterContent from '~/diffs/components/diff_line_gutter_content.vue';
-import store from '~/mr_notes/stores';
+import { createStore } from '~/mr_notes/stores';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import discussionsMockData from '../mock_data/diff_discussions';
import diffFileMockData from '../mock_data/diff_file';
@@ -23,7 +23,7 @@ describe('DiffLineGutterContent', () => {
props.fileHash = getDiffFileMock().file_hash;
props.contextLinesPath = '/context/lines/path';
- return createComponentWithStore(cmp, store, props).$mount();
+ return createComponentWithStore(cmp, createStore(), props).$mount();
};
describe('computed', () => {
@@ -61,7 +61,7 @@ describe('DiffLineGutterContent', () => {
contextLinesPath: '/context/lines/path',
};
props.line.discussions = [Object.assign({}, discussionsMockData)];
- const component = createComponentWithStore(cmp, store, props).$mount();
+ const component = createComponentWithStore(cmp, createStore(), props).$mount();
expect(component.hasDiscussions).toEqual(true);
expect(component.shouldShowAvatarsOnGutter).toEqual(true);
diff --git a/spec/javascripts/diffs/components/diff_line_note_form_spec.js b/spec/javascripts/diffs/components/diff_line_note_form_spec.js
index b983dc35a57..237cfccfa29 100644
--- a/spec/javascripts/diffs/components/diff_line_note_form_spec.js
+++ b/spec/javascripts/diffs/components/diff_line_note_form_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue';
-import store from '~/mr_notes/stores';
+import { createStore } from '~/mr_notes/stores';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffFileMockData from '../mock_data/diff_file';
import { noteableDataMock } from '../../notes/mock_data';
@@ -15,7 +15,7 @@ describe('DiffLineNoteForm', () => {
diffFile = getDiffFileMock();
diffLines = diffFile.highlighted_diff_lines;
- component = createComponentWithStore(Vue.extend(DiffLineNoteForm), store, {
+ component = createComponentWithStore(Vue.extend(DiffLineNoteForm), createStore(), {
diffFileHash: diffFile.file_hash,
diffLines,
line: diffLines[0],
diff --git a/spec/javascripts/diffs/components/diff_table_cell_spec.js b/spec/javascripts/diffs/components/diff_table_cell_spec.js
index 170e661beea..a5a042c577c 100644
--- a/spec/javascripts/diffs/components/diff_table_cell_spec.js
+++ b/spec/javascripts/diffs/components/diff_table_cell_spec.js
@@ -1,12 +1,12 @@
import Vue from 'vue';
-import store from '~/mr_notes/stores';
+import { createStore } from '~/mr_notes/stores';
import DiffTableCell from '~/diffs/components/diff_table_cell.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffFileMockData from '../mock_data/diff_file';
describe('DiffTableCell', () => {
const createComponent = options =>
- createComponentWithStore(Vue.extend(DiffTableCell), store, {
+ createComponentWithStore(Vue.extend(DiffTableCell), createStore(), {
line: diffFileMockData.highlighted_diff_lines[0],
fileHash: diffFileMockData.file_hash,
contextLinesPath: 'contextLinesPath',
diff --git a/spec/javascripts/diffs/components/inline_diff_expansion_row_spec.js b/spec/javascripts/diffs/components/inline_diff_expansion_row_spec.js
index bf50070a4f5..290b3d7c803 100644
--- a/spec/javascripts/diffs/components/inline_diff_expansion_row_spec.js
+++ b/spec/javascripts/diffs/components/inline_diff_expansion_row_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import store from '~/mr_notes/stores';
+import { createStore } from '~/mr_notes/stores';
import InlineDiffExpansionRow from '~/diffs/components/inline_diff_expansion_row.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffFileMockData from '../mock_data/diff_file';
@@ -18,7 +18,7 @@ describe('InlineDiffExpansionRow', () => {
};
const props = Object.assign({}, defaults, options);
- return createComponentWithStore(cmp, store, props).$mount();
+ return createComponentWithStore(cmp, createStore(), props).$mount();
};
describe('template', () => {
diff --git a/spec/javascripts/diffs/components/inline_diff_table_row_spec.js b/spec/javascripts/diffs/components/inline_diff_table_row_spec.js
index 97926f6625e..0ddffe926d9 100644
--- a/spec/javascripts/diffs/components/inline_diff_table_row_spec.js
+++ b/spec/javascripts/diffs/components/inline_diff_table_row_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import store from '~/mr_notes/stores';
+import { createStore } from '~/mr_notes/stores';
import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffFileMockData from '../mock_data/diff_file';
@@ -9,7 +9,7 @@ describe('InlineDiffTableRow', () => {
const thisLine = diffFileMockData.highlighted_diff_lines[0];
beforeEach(() => {
- vm = createComponentWithStore(Vue.extend(InlineDiffTableRow), store, {
+ vm = createComponentWithStore(Vue.extend(InlineDiffTableRow), createStore(), {
line: thisLine,
fileHash: diffFileMockData.file_hash,
contextLinesPath: 'contextLinesPath',
diff --git a/spec/javascripts/diffs/components/inline_diff_view_spec.js b/spec/javascripts/diffs/components/inline_diff_view_spec.js
index 3963e37fae2..486d9629e26 100644
--- a/spec/javascripts/diffs/components/inline_diff_view_spec.js
+++ b/spec/javascripts/diffs/components/inline_diff_view_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import '~/behaviors/markdown/render_gfm';
import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
-import store from 'ee_else_ce/mr_notes/stores';
+import { createStore } from 'ee_else_ce/mr_notes/stores';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffFileMockData from '../mock_data/diff_file';
import discussionsMockData from '../mock_data/diff_discussions';
@@ -15,6 +15,8 @@ describe('InlineDiffView', () => {
beforeEach(done => {
const diffFile = getDiffFileMock();
+ const store = createStore();
+
store.dispatch('diffs/setInlineDiffViewType');
component = createComponentWithStore(Vue.extend(InlineDiffView), store, {
diffFile,
diff --git a/spec/javascripts/diffs/components/parallel_diff_expansion_row_spec.js b/spec/javascripts/diffs/components/parallel_diff_expansion_row_spec.js
index 134738932b3..a766ebb5efb 100644
--- a/spec/javascripts/diffs/components/parallel_diff_expansion_row_spec.js
+++ b/spec/javascripts/diffs/components/parallel_diff_expansion_row_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import store from '~/mr_notes/stores';
+import { createStore } from '~/mr_notes/stores';
import ParallelDiffExpansionRow from '~/diffs/components/parallel_diff_expansion_row.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffFileMockData from '../mock_data/diff_file';
@@ -18,7 +18,7 @@ describe('ParallelDiffExpansionRow', () => {
};
const props = Object.assign({}, defaults, options);
- return createComponentWithStore(cmp, store, props).$mount();
+ return createComponentWithStore(cmp, createStore(), props).$mount();
};
describe('template', () => {
diff --git a/spec/javascripts/diffs/components/parallel_diff_view_spec.js b/spec/javascripts/diffs/components/parallel_diff_view_spec.js
index 236bda96145..191313bf487 100644
--- a/spec/javascripts/diffs/components/parallel_diff_view_spec.js
+++ b/spec/javascripts/diffs/components/parallel_diff_view_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue';
-import store from 'ee_else_ce/mr_notes/stores';
+import { createStore } from 'ee_else_ce/mr_notes/stores';
import * as constants from '~/diffs/constants';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffFileMockData from '../mock_data/diff_file';
@@ -12,7 +12,7 @@ describe('ParallelDiffView', () => {
beforeEach(() => {
const diffFile = getDiffFileMock();
- component = createComponentWithStore(Vue.extend(ParallelDiffView), store, {
+ component = createComponentWithStore(Vue.extend(ParallelDiffView), createStore(), {
diffFile,
diffLines: diffFile.parallel_diff_lines,
}).$mount();
diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js
index c3ed079e33b..71dcba114a9 100644
--- a/spec/javascripts/notes/stores/getters_spec.js
+++ b/spec/javascripts/notes/stores/getters_spec.js
@@ -256,6 +256,54 @@ describe('Getters Notes Store', () => {
});
});
+ describe('previousUnresolvedDiscussionId', () => {
+ describe('with unresolved discussions', () => {
+ const localGetters = {
+ unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'],
+ };
+
+ it('with bogus returns falsey', () => {
+ expect(getters.previousUnresolvedDiscussionId(state, localGetters)('bogus')).toBe('456');
+ });
+
+ [
+ { id: '123', expected: '789' },
+ { id: '456', expected: '123' },
+ { id: '789', expected: '456' },
+ ].forEach(({ id, expected }) => {
+ it(`with ${id}, returns previous value`, () => {
+ expect(getters.previousUnresolvedDiscussionId(state, localGetters)(id)).toBe(expected);
+ });
+ });
+ });
+
+ describe('with 1 unresolved discussion', () => {
+ const localGetters = {
+ unresolvedDiscussionsIdsOrdered: () => ['123'],
+ };
+
+ it('with bogus returns id', () => {
+ expect(getters.previousUnresolvedDiscussionId(state, localGetters)('bogus')).toBe('123');
+ });
+
+ it('with match, returns value', () => {
+ expect(getters.previousUnresolvedDiscussionId(state, localGetters)('123')).toEqual('123');
+ });
+ });
+
+ describe('with 0 unresolved discussions', () => {
+ const localGetters = {
+ unresolvedDiscussionsIdsOrdered: () => [],
+ };
+
+ it('returns undefined', () => {
+ expect(
+ getters.previousUnresolvedDiscussionId(state, localGetters)('bogus'),
+ ).toBeUndefined();
+ });
+ });
+ });
+
describe('firstUnresolvedDiscussionId', () => {
const localGetters = {
unresolvedDiscussionsIdsByDate: ['123', '456'],
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 782a84f922b..2cb4f222ea4 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -172,6 +172,19 @@ describe Member do
it { expect(described_class.non_request).to include @accepted_request_member }
end
+ describe '.search_invite_email' do
+ it 'returns only members the matching e-mail' do
+ create(:group_member, :invited)
+
+ invited = described_class.search_invite_email(@invited_member.invite_email)
+
+ expect(invited.count).to eq(1)
+ expect(invited.first).to eq(@invited_member)
+
+ expect(described_class.search_invite_email('bad-email@example.com').count).to eq(0)
+ end
+ end
+
describe '.developers' do
subject { described_class.developers.to_a }