diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-30 12:10:12 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-30 12:10:12 +0000 |
commit | b51258eac273fa8aad9ec7d8a2888b9fd747b107 (patch) | |
tree | a4a0d580fb6be3596aceed8910f1db59eb65a8be | |
parent | a16109b67fdee47ff1ccf4c0c831a0b47a17ab34 (diff) | |
download | gitlab-ce-b51258eac273fa8aad9ec7d8a2888b9fd747b107.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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']) |