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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/admin/users/components/actions/delete.vue1
-rw-r--r--app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue1
-rw-r--r--app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue6
-rw-r--r--app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql10
-rw-r--r--app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql10
-rw-r--r--app/assets/javascripts/boards/stores/actions.js49
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js16
-rw-r--r--app/assets/javascripts/boards/stores/state.js2
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue1
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue6
-rw-r--r--app/controllers/projects/templates_controller.rb2
-rw-r--r--app/helpers/admin/user_actions_helper.rb4
-rw-r--r--app/helpers/users_helper.rb2
-rw-r--r--config/feature_categories.yml1
-rw-r--r--doc/raketasks/backup_restore.md2
-rw-r--r--doc/topics/autodevops/upgrading_postgresql.md4
-rw-r--r--lib/api/project_templates.rb2
-rw-r--r--lib/api/tags.rb2
-rw-r--r--lib/api/templates.rb19
-rw-r--r--lib/gitlab/github_import/importer/repository_importer.rb2
-rw-r--r--lib/gitlab/sidekiq_cluster/cli.rb16
-rw-r--r--spec/features/admin/users/user_spec.rb33
-rw-r--r--spec/frontend/admin/users/components/actions/actions_spec.js49
-rw-r--r--spec/frontend/admin/users/mock_data.js2
-rw-r--r--spec/frontend/boards/mock_data.js11
-rw-r--r--spec/frontend/boards/stores/actions_spec.js87
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js11
-rw-r--r--spec/helpers/admin/user_actions_helper_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/repository_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_cluster/cli_spec.rb23
33 files changed, 332 insertions, 52 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 66f5311ad3d..65d7bf8485f 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-fb9de5f27b55e28a5d4737e0aa639a50ccc3dd75
+a8520a1568f0c0515eef6931c01b3fa8e55e7985
diff --git a/app/assets/javascripts/admin/users/components/actions/delete.vue b/app/assets/javascripts/admin/users/components/actions/delete.vue
index 6f4f272154a..a0f4a4bf382 100644
--- a/app/assets/javascripts/admin/users/components/actions/delete.vue
+++ b/app/assets/javascripts/admin/users/components/actions/delete.vue
@@ -28,6 +28,7 @@ export default {
modal-type="delete"
:username="username"
:paths="paths"
+ :delete-path="paths.delete"
:oncall-schedules="oncallSchedules"
>
<slot></slot>
diff --git a/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue b/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue
index 82b09c04ab2..02fd3efafa1 100644
--- a/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue
+++ b/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue
@@ -28,6 +28,7 @@ export default {
modal-type="delete-with-contributions"
:username="username"
:paths="paths"
+ :delete-path="paths.deleteWithContributions"
:oncall-schedules="oncallSchedules"
>
<slot></slot>
diff --git a/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue b/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue
index b3b68442e80..a1589c9d46d 100644
--- a/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue
+++ b/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue
@@ -14,6 +14,10 @@ export default {
type: Object,
required: true,
},
+ deletePath: {
+ type: String,
+ required: true,
+ },
modalType: {
type: String,
required: true,
@@ -27,7 +31,7 @@ export default {
modalAttributes() {
return {
'data-block-user-url': this.paths.block,
- 'data-delete-user-url': this.paths.delete,
+ 'data-delete-user-url': this.deletePath,
'data-gl-modal-action': this.modalType,
'data-username': this.username,
'data-oncall-schedules': JSON.stringify(this.oncallSchedules),
diff --git a/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql b/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
new file mode 100644
index 00000000000..73aa9137dec
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
@@ -0,0 +1,10 @@
+query GroupBoardMilestones($fullPath: ID!, $searchTerm: String) {
+ group(fullPath: $fullPath) {
+ milestones(includeAncestors: true, searchTitle: $searchTerm) {
+ nodes {
+ id
+ title
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql b/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
new file mode 100644
index 00000000000..8dd4d256caa
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
@@ -0,0 +1,10 @@
+query ProjectBoardMilestones($fullPath: ID!, $searchTerm: String) {
+ project(fullPath: $fullPath) {
+ milestones(searchTitle: $searchTerm, includeAncestors: true) {
+ nodes {
+ id
+ title
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 5643f548a0f..38b5bb5c19d 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -36,10 +36,13 @@ import {
filterVariables,
} from '../boards_util';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
+import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
+import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
+
import * as types from './mutation_types';
export const gqlClient = createGqClient(
@@ -216,6 +219,52 @@ export default {
});
},
+ fetchMilestones({ state, commit }, searchTerm) {
+ commit(types.RECEIVE_MILESTONES_REQUEST);
+
+ const { fullPath, boardType } = state;
+
+ const variables = {
+ fullPath,
+ searchTerm,
+ };
+
+ let query;
+ if (boardType === BoardType.project) {
+ query = projectBoardMilestonesQuery;
+ }
+ if (boardType === BoardType.group) {
+ query = groupBoardMilestonesQuery;
+ }
+
+ if (!query) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Unknown board type');
+ }
+
+ return gqlClient
+ .query({
+ query,
+ variables,
+ })
+ .then(({ data }) => {
+ const errors = data[boardType]?.errors;
+ const milestones = data[boardType]?.milestones.nodes;
+
+ if (errors?.[0]) {
+ throw new Error(errors[0]);
+ }
+
+ commit(types.RECEIVE_MILESTONES_SUCCESS, milestones);
+
+ return milestones;
+ })
+ .catch((e) => {
+ commit(types.RECEIVE_MILESTONES_FAILURE);
+ throw e;
+ });
+ },
+
moveList: (
{ state: { boardLists }, commit, dispatch },
{
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 0a6ee59955c..31b78014525 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -18,6 +18,9 @@ export const RESET_ITEMS_FOR_LIST = 'RESET_ITEMS_FOR_LIST';
export const REQUEST_ITEMS_FOR_LIST = 'REQUEST_ITEMS_FOR_LIST';
export const RECEIVE_ITEMS_FOR_LIST_FAILURE = 'RECEIVE_ITEMS_FOR_LIST_FAILURE';
export const RECEIVE_ITEMS_FOR_LIST_SUCCESS = 'RECEIVE_ITEMS_FOR_LIST_SUCCESS';
+export const RECEIVE_MILESTONES_REQUEST = 'RECEIVE_MILESTONES_REQUEST';
+export const RECEIVE_MILESTONES_SUCCESS = 'RECEIVE_MILESTONES_SUCCESS';
+export const RECEIVE_MILESTONES_FAILURE = 'RECEIVE_MILESTONES_FAILURE';
export const UPDATE_BOARD_ITEM = 'UPDATE_BOARD_ITEM';
export const REMOVE_BOARD_ITEM = 'REMOVE_BOARD_ITEM';
export const MUTATE_ISSUE_SUCCESS = 'MUTATE_ISSUE_SUCCESS';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index ca795dfb10c..668a3dbaa7e 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,7 +1,7 @@
import { cloneDeep, pull, union } from 'lodash';
import Vue from 'vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
import { formatIssue } from '../boards_util';
import { issuableTypes } from '../constants';
import * as mutationTypes from './mutation_types';
@@ -133,6 +133,20 @@ export default {
Vue.set(state.listsFlags, listId, { [fetchNext ? 'isLoadingMore' : 'isLoading']: true });
},
+ [mutationTypes.RECEIVE_MILESTONES_SUCCESS](state, milestones) {
+ state.milestones = milestones;
+ state.milestonesLoading = false;
+ },
+
+ [mutationTypes.RECEIVE_MILESTONES_REQUEST](state) {
+ state.milestonesLoading = true;
+ },
+
+ [mutationTypes.RECEIVE_MILESTONES_FAILURE](state) {
+ state.milestonesLoading = false;
+ state.error = __('Failed to load milestones.');
+ },
+
[mutationTypes.RECEIVE_ITEMS_FOR_LIST_SUCCESS]: (state, { listItems, listPageInfo, listId }) => {
const { listData, boardItems } = listItems;
Vue.set(state, 'boardItems', { ...state.boardItems, ...boardItems });
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index 7be5ae8b583..264a03ff39d 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -19,6 +19,8 @@ export default () => ({
boardConfig: {},
labelsLoading: false,
labels: [],
+ milestones: [],
+ milestonesLoading: false,
highlightedLists: [],
selectedBoardItems: [],
groupProjects: [],
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
index 1f3ec7092bc..e2f3f9cad7b 100644
--- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -75,6 +75,7 @@ export default {
:key="note.id"
:img-src="note.author.avatar_url"
:tooltip-text="getTooltipText(note)"
+ lazy
class="diff-comment-avatar js-diff-comment-avatar"
@click.native="$emit('toggleLineDiscussions')"
/>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 5ea431224ce..89782142349 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -392,6 +392,7 @@ export default {
:img-src="author.avatar_url"
:img-alt="author.name"
:img-size="40"
+ lazy
>
<template #avatar-badge>
<slot name="avatar-badge"></slot>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index 55e2a786c8f..04423aac651 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -30,6 +30,11 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
+ lazy: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
linkHref: {
type: String,
required: false,
@@ -91,6 +96,7 @@ export default {
:size="imgSize"
:tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement"
+ :lazy="lazy"
>
<slot></slot> </user-avatar-image
><span
diff --git a/app/controllers/projects/templates_controller.rb b/app/controllers/projects/templates_controller.rb
index df945a99c73..4bad6dc1b3d 100644
--- a/app/controllers/projects/templates_controller.rb
+++ b/app/controllers/projects/templates_controller.rb
@@ -5,7 +5,7 @@ class Projects::TemplatesController < Projects::ApplicationController
before_action :authorize_can_read_issuable!
before_action :get_template_class
- feature_category :templates
+ feature_category :source_code_management
def index
templates = @template_type.template_subsets(project)
diff --git a/app/helpers/admin/user_actions_helper.rb b/app/helpers/admin/user_actions_helper.rb
index 5719d8f5ffd..dc31c06477e 100644
--- a/app/helpers/admin/user_actions_helper.rb
+++ b/app/helpers/admin/user_actions_helper.rb
@@ -48,9 +48,9 @@ module Admin
end
def delete_actions
- return unless can?(current_user, :destroy_user, @user) && !@user.blocked_pending_approval? && @user.can_be_removed?
+ return unless can?(current_user, :destroy_user, @user) && !@user.blocked_pending_approval?
- @actions << 'delete'
+ @actions << 'delete' if @user.can_be_removed?
@actions << 'delete_with_contributions'
end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 93a0166f43e..c64c2ab35fb 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -184,7 +184,7 @@ module UsersHelper
activate: activate_admin_user_path(:id),
unlock: unlock_admin_user_path(:id),
delete: admin_user_path(:id),
- delete_with_contributions: admin_user_path(:id),
+ delete_with_contributions: admin_user_path(:id, hard_delete: true),
admin_user: admin_user_path(:id),
ban: ban_admin_user_path(:id),
unban: unban_admin_user_path(:id)
diff --git a/config/feature_categories.yml b/config/feature_categories.yml
index 982b512cd9a..4177978ffea 100644
--- a/config/feature_categories.yml
+++ b/config/feature_categories.yml
@@ -120,7 +120,6 @@
- static_site_editor
- subgroups
- synthetic_monitoring
-- templates
- time_tracking
- tracing
- usability_testing
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index b393be18910..0f37df69323 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -119,7 +119,7 @@ script on the GitLab task runner pod. For more details, see
[backing up a GitLab installation](https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/backup-restore/backup.md#backing-up-a-gitlab-installation).
```shell
-kubectl exec -it <gitlab task-runner pod> backup-utility
+kubectl exec -it <gitlab task-runner pod> -- backup-utility
```
Similar to the Kubernetes case, if you have scaled out your GitLab cluster to
diff --git a/doc/topics/autodevops/upgrading_postgresql.md b/doc/topics/autodevops/upgrading_postgresql.md
index c03c4171d6d..4d725ddd3e3 100644
--- a/doc/topics/autodevops/upgrading_postgresql.md
+++ b/doc/topics/autodevops/upgrading_postgresql.md
@@ -103,7 +103,7 @@ being modified after the database dump is created.
1. Connect to the pod with:
```shell
- kubectl exec -it production-postgres-5db86568d7-qxlxv --namespace "$APP_NAMESPACE" bash
+ kubectl exec -it production-postgres-5db86568d7-qxlxv --namespace "$APP_NAMESPACE" -- bash
```
1. Once, connected, create a dump file with the following command.
@@ -221,7 +221,7 @@ higher*. This is the
1. Connect to the pod:
```shell
- kubectl exec -it production-postgresql-0 --namespace "$APP_NAMESPACE" bash
+ kubectl exec -it production-postgresql-0 --namespace "$APP_NAMESPACE" -- bash
```
1. Once connected to the pod, run the following command to restore the database.
diff --git a/lib/api/project_templates.rb b/lib/api/project_templates.rb
index acf9bfece65..fe0e837c596 100644
--- a/lib/api/project_templates.rb
+++ b/lib/api/project_templates.rb
@@ -12,7 +12,7 @@ module API
before { authenticate_non_get! }
- feature_category :templates
+ feature_category :source_code_management
params do
requires :id, type: String, desc: 'The ID of a project'
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 6c8e2c69a6d..395aacced78 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -59,8 +59,6 @@ module API
optional :message, type: String, desc: 'Specifying a message creates an annotated tag'
end
post ':id/repository/tags', :release_orchestration do
- deprecate_release_notes unless params[:release_description].blank?
-
authorize_admin_tag
result = ::Tags::CreateService.new(user_project, current_user)
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index b7fb35eac03..a595129fd6a 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -4,17 +4,18 @@ module API
class Templates < ::API::Base
include PaginationParams
- feature_category :templates
-
GLOBAL_TEMPLATE_TYPES = {
gitignores: {
- gitlab_version: 8.8
+ gitlab_version: 8.8,
+ feature_category: :source_code_management
},
gitlab_ci_ymls: {
- gitlab_version: 8.9
+ gitlab_version: 8.9,
+ feature_category: :continuous_integration
},
dockerfiles: {
- gitlab_version: 8.15
+ gitlab_version: 8.15,
+ feature_category: :source_code_management
}
}.freeze
@@ -33,7 +34,7 @@ module API
optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
use :pagination
end
- get "templates/licenses" do
+ get "templates/licenses", feature_category: :source_code_management do
popular = declared(params)[:popular]
popular = to_boolean(popular) if popular.present?
@@ -49,7 +50,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the template'
end
- get "templates/licenses/:name", requirements: { name: /[\w\.-]+/ } do
+ get "templates/licenses/:name", requirements: { name: /[\w\.-]+/ }, feature_category: :source_code_management do
template = TemplateFinder.build(:licenses, nil, name: params[:name]).execute
not_found!('License') unless template.present?
@@ -72,7 +73,7 @@ module API
params do
use :pagination
end
- get "templates/#{template_type}" do
+ get "templates/#{template_type}", feature_category: properties[:feature_category] do
templates = ::Kaminari.paginate_array(TemplateFinder.build(template_type, nil).execute)
present paginate(templates), with: Entities::TemplatesList
end
@@ -84,7 +85,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the template'
end
- get "templates/#{template_type}/:name", requirements: { name: /[\w\.-]+/ } do
+ get "templates/#{template_type}/:name", requirements: { name: /[\w\.-]+/ }, feature_category: properties[:feature_category] do
finder = TemplateFinder.build(template_type, nil, name: declared(params)[:name])
new_template = finder.execute
diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb
index 1401c92a44e..b4a40811d6e 100644
--- a/lib/gitlab/github_import/importer/repository_importer.rb
+++ b/lib/gitlab/github_import/importer/repository_importer.rb
@@ -50,7 +50,7 @@ module Gitlab
project.ensure_repository
refmap = Gitlab::GithubImport.refmap
- project.repository.fetch_as_mirror(project.import_url, refmap: refmap, forced: true, remote_name: 'github')
+ project.repository.fetch_as_mirror(project.import_url, refmap: refmap, forced: true)
project.change_head(default_branch) if default_branch
diff --git a/lib/gitlab/sidekiq_cluster/cli.rb b/lib/gitlab/sidekiq_cluster/cli.rb
index e20834fa912..05319ba17a2 100644
--- a/lib/gitlab/sidekiq_cluster/cli.rb
+++ b/lib/gitlab/sidekiq_cluster/cli.rb
@@ -37,6 +37,7 @@ module Gitlab
@logger.formatter = ::Gitlab::SidekiqLogging::JSONFormatter.new
@rails_path = Dir.pwd
@dryrun = false
+ @list_queues = false
end
def run(argv = ARGV)
@@ -47,6 +48,11 @@ module Gitlab
option_parser.parse!(argv)
+ if @dryrun && @list_queues
+ raise CommandError,
+ 'The --dryrun and --list-queues options are mutually exclusive'
+ end
+
worker_metadatas = SidekiqConfig::CliMethods.worker_metadatas(@rails_path)
worker_queues = SidekiqConfig::CliMethods.worker_queues(@rails_path)
@@ -73,6 +79,12 @@ module Gitlab
'No queues found, you must select at least one queue'
end
+ if @list_queues
+ puts queue_groups.map(&:sort) # rubocop:disable Rails/Output
+
+ return
+ end
+
unless @dryrun
@logger.info("Starting cluster with #{queue_groups.length} processes")
end
@@ -202,6 +214,10 @@ module Gitlab
opt.on('-d', '--dryrun', 'Print commands that would be run without this flag, and quit') do |int|
@dryrun = true
end
+
+ opt.on('--list-queues', 'List matching queues, and quit') do |int|
+ @list_queues = true
+ end
end
end
end
diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb
index e6eb76b13eb..c2033913a67 100644
--- a/spec/features/admin/users/user_spec.rb
+++ b/spec/features/admin/users/user_spec.rb
@@ -90,6 +90,39 @@ RSpec.describe 'Admin::Users::User' do
end
end
+ context 'when user is the sole owner of a group' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user_sole_owner_of_group) { create(:user) }
+
+ before do
+ group.add_owner(user_sole_owner_of_group)
+ end
+
+ it 'shows `Delete user and contributions` action but not `Delete user` action', :js do
+ visit admin_user_path(user_sole_owner_of_group)
+
+ click_user_dropdown_toggle(user_sole_owner_of_group.id)
+
+ expect(page).to have_button('Delete user and contributions')
+ expect(page).not_to have_button('Delete user', exact: true)
+ end
+
+ it 'allows user to be deleted by using the `Delete user and contributions` action', :js do
+ visit admin_user_path(user_sole_owner_of_group)
+
+ click_action_in_user_dropdown(user_sole_owner_of_group.id, 'Delete user and contributions')
+
+ page.within('[role="dialog"]') do
+ fill_in('username', with: user_sole_owner_of_group.name)
+ click_button('Delete user and contributions')
+ end
+
+ wait_for_requests
+
+ expect(page).to have_content('The user is being deleted.')
+ end
+ end
+
describe 'Impersonation' do
let_it_be(:another_user) { create(:user) }
diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js
index 67d9bac8580..fd05b08a3fb 100644
--- a/spec/frontend/admin/users/components/actions/actions_spec.js
+++ b/spec/frontend/admin/users/components/actions/actions_spec.js
@@ -5,8 +5,8 @@ import { nextTick } from 'vue';
import Actions from '~/admin/users/components/actions';
import SharedDeleteAction from '~/admin/users/components/actions/shared/shared_delete_action.vue';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-
import { CONFIRMATION_ACTIONS, DELETE_ACTIONS } from '../../constants';
+import { paths } from '../../mock_data';
describe('Action components', () => {
let wrapper;
@@ -47,32 +47,33 @@ describe('Action components', () => {
describe('DELETE_ACTION_COMPONENTS', () => {
const oncallSchedules = [{ name: 'schedule1' }, { name: 'schedule2' }];
- it.each(DELETE_ACTIONS)('renders a dropdown item for "%s"', async (action) => {
- initComponent({
- component: Actions[capitalizeFirstCharacter(action)],
- props: {
- username: 'John Doe',
- paths: {
- delete: '/delete',
- block: '/block',
+
+ it.each(DELETE_ACTIONS.map((action) => [action, paths[action]]))(
+ 'renders a dropdown item for "%s"',
+ async (action, expectedPath) => {
+ initComponent({
+ component: Actions[capitalizeFirstCharacter(action)],
+ props: {
+ username: 'John Doe',
+ paths,
+ oncallSchedules,
},
- oncallSchedules,
- },
- stubs: { SharedDeleteAction },
- });
+ stubs: { SharedDeleteAction },
+ });
- await nextTick();
+ await nextTick();
- const sharedAction = wrapper.find(SharedDeleteAction);
+ const sharedAction = wrapper.find(SharedDeleteAction);
- expect(sharedAction.attributes('data-block-user-url')).toBe('/block');
- expect(sharedAction.attributes('data-delete-user-url')).toBe('/delete');
- expect(sharedAction.attributes('data-gl-modal-action')).toBe(kebabCase(action));
- expect(sharedAction.attributes('data-username')).toBe('John Doe');
- expect(sharedAction.attributes('data-oncall-schedules')).toBe(
- JSON.stringify(oncallSchedules),
- );
- expect(findDropdownItem().exists()).toBe(true);
- });
+ expect(sharedAction.attributes('data-block-user-url')).toBe(paths.block);
+ expect(sharedAction.attributes('data-delete-user-url')).toBe(expectedPath);
+ expect(sharedAction.attributes('data-gl-modal-action')).toBe(kebabCase(action));
+ expect(sharedAction.attributes('data-username')).toBe('John Doe');
+ expect(sharedAction.attributes('data-oncall-schedules')).toBe(
+ JSON.stringify(oncallSchedules),
+ );
+ expect(findDropdownItem().exists()).toBe(true);
+ },
+ );
});
});
diff --git a/spec/frontend/admin/users/mock_data.js b/spec/frontend/admin/users/mock_data.js
index ded3e6f7edf..73fa73c0b47 100644
--- a/spec/frontend/admin/users/mock_data.js
+++ b/spec/frontend/admin/users/mock_data.js
@@ -30,7 +30,7 @@ export const paths = {
activate: '/admin/users/id/activate',
unlock: '/admin/users/id/unlock',
delete: '/admin/users/id',
- deleteWithContributions: '/admin/users/id',
+ deleteWithContributions: '/admin/users/id?hard_delete=true',
adminUser: '/admin/users/id',
ban: '/admin/users/id/ban',
unban: '/admin/users/id/unban',
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 6ac4db8cdaa..420f5aa293b 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -101,6 +101,17 @@ export const mockMilestone = {
due_date: '2019-12-31',
};
+export const mockMilestones = [
+ {
+ id: 'gid://gitlab/Milestone/1',
+ title: 'Milestone 1',
+ },
+ {
+ id: 'gid://gitlab/Milestone/2',
+ title: 'Milestone 2',
+ },
+];
+
export const assignees = [
{
id: 'gid://gitlab/User/2',
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 37817eecebc..9d3ba5b105e 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -1,5 +1,7 @@
import * as Sentry from '@sentry/browser';
import { cloneDeep } from 'lodash';
+import Vue from 'vue';
+import Vuex from 'vuex';
import {
inactiveId,
ISSUABLE,
@@ -22,6 +24,7 @@ import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutati
import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql';
import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
+import mutations from '~/boards/stores/mutations';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import {
@@ -38,6 +41,7 @@ import {
mockMoveState,
mockMoveData,
mockList,
+ mockMilestones,
} from '../mock_data';
jest.mock('~/flash');
@@ -46,6 +50,8 @@ jest.mock('~/flash');
// subgroups when the movIssue action is called.
const getProjectPath = (path) => path.split('#')[0];
+Vue.use(Vuex);
+
beforeEach(() => {
window.gon = { features: {} };
});
@@ -261,6 +267,87 @@ describe('fetchLists', () => {
);
});
+describe('fetchMilestones', () => {
+ const queryResponse = {
+ data: {
+ project: {
+ milestones: {
+ nodes: mockMilestones,
+ },
+ },
+ },
+ };
+
+ const queryErrors = {
+ data: {
+ project: {
+ errors: ['You cannot view these milestones'],
+ milestones: {},
+ },
+ },
+ };
+
+ function createStore({
+ state = {
+ boardType: 'project',
+ fullPath: 'gitlab-org/gitlab',
+ milestones: [],
+ milestonesLoading: false,
+ },
+ } = {}) {
+ return new Vuex.Store({
+ state,
+ mutations,
+ });
+ }
+
+ it('throws error if state.boardType is not group or project', () => {
+ const store = createStore({
+ state: {
+ boardType: 'invalid',
+ },
+ });
+
+ expect(() => actions.fetchMilestones(store)).toThrow(new Error('Unknown board type'));
+ });
+
+ it('sets milestonesLoading to true', async () => {
+ jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
+
+ const store = createStore();
+
+ actions.fetchMilestones(store);
+
+ expect(store.state.milestonesLoading).toBe(true);
+ });
+
+ describe('success', () => {
+ it('sets state.milestones from query result', async () => {
+ jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
+
+ const store = createStore();
+
+ await actions.fetchMilestones(store);
+
+ expect(store.state.milestonesLoading).toBe(false);
+ expect(store.state.milestones).toBe(mockMilestones);
+ });
+ });
+
+ describe('failure', () => {
+ it('sets state.milestones from query result', async () => {
+ jest.spyOn(gqlClient, 'query').mockResolvedValue(queryErrors);
+
+ const store = createStore();
+
+ await expect(actions.fetchMilestones(store)).rejects.toThrow();
+
+ expect(store.state.milestonesLoading).toBe(false);
+ expect(store.state.error).toBe('Failed to load milestones.');
+ });
+ });
+});
+
describe('createList', () => {
it('should dispatch createIssueList action', () => {
testAction({
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
index d62c4a98b10..d3fec680b54 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -104,4 +104,15 @@ describe('User Avatar Link Component', () => {
);
});
});
+
+ describe('lazy', () => {
+ it('passes lazy prop to avatar image', () => {
+ createWrapper({
+ username: '',
+ lazy: true,
+ });
+
+ expect(wrapper.find(UserAvatarImage).props('lazy')).toBe(true);
+ });
+ });
});
diff --git a/spec/helpers/admin/user_actions_helper_spec.rb b/spec/helpers/admin/user_actions_helper_spec.rb
index d945b13cad6..3bc380fbc99 100644
--- a/spec/helpers/admin/user_actions_helper_spec.rb
+++ b/spec/helpers/admin/user_actions_helper_spec.rb
@@ -106,7 +106,7 @@ RSpec.describe Admin::UserActionsHelper do
group.add_owner(user)
end
- it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate") }
+ it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete_with_contributions") }
end
context 'the user is a bot' do
diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
index 3839303b881..b85a8f82af4 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -202,7 +202,7 @@ RSpec.describe Gitlab::GithubImport::Importer::RepositoryImporter do
expect(repository)
.to receive(:fetch_as_mirror)
- .with(project.import_url, refmap: Gitlab::GithubImport.refmap, forced: true, remote_name: 'github')
+ .with(project.import_url, refmap: Gitlab::GithubImport.refmap, forced: true)
service = double
expect(Repositories::HousekeepingService)
diff --git a/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb b/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb
index 5347680b253..3dd5ac8ee6c 100644
--- a/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb
+++ b/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do
end
end
- context '-timeout flag' do
+ context 'with --timeout flag' do
it 'when given', 'starts Sidekiq workers with given timeout' do
expect(Gitlab::SidekiqCluster).to receive(:start)
.with([['foo']], default_options.merge(timeout: 10))
@@ -97,6 +97,27 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do
end
end
+ context 'with --list-queues flag' do
+ it 'errors when given --list-queues and --dryrun' do
+ expect { cli.run(%w(foo --list-queues --dryrun)) }.to raise_error(described_class::CommandError)
+ end
+
+ it 'prints out a list of queues in alphabetical order' do
+ expected_queues = [
+ 'epics:epics_update_epics_dates',
+ 'epics_new_epic_issue',
+ 'new_epic',
+ 'todos_destroyer:todos_destroyer_confidential_epic'
+ ]
+
+ allow(Gitlab::SidekiqConfig::CliMethods).to receive(:query_queues).and_return(expected_queues.shuffle)
+
+ expect(cli).to receive(:puts).with([expected_queues])
+
+ cli.run(%w(--queue-selector feature_category=epics --list-queues))
+ end
+ end
+
context 'queue namespace expansion' do
it 'starts Sidekiq workers for all queues in all_queues.yml with a namespace in argv' do
expect(Gitlab::SidekiqConfig::CliMethods).to receive(:worker_queues).and_return(['cronjob:foo', 'cronjob:bar'])