Commit ab650e66 authored by Sean McGivern's avatar Sean McGivern

Merge branch '7249-group-bulk-edit-issues-milestone' into 'master'

Allow bulk update for group issues - milestones

See merge request gitlab-org/gitlab-ee!14141
parents d78a88bd 6d10d7c2
/* eslint-disable consistent-return, func-names, array-callback-return, prefer-arrow-callback, no-unused-vars */
/* eslint-disable consistent-return, func-names, array-callback-return, prefer-arrow-callback */
import $ from 'jquery';
import _ from 'underscore';
......@@ -7,7 +7,7 @@ import Flash from './flash';
import { __ } from './locale';
export default {
init({ container, form, issues, prefixId } = {}) {
init({ form, issues, prefixId } = {}) {
this.prefixId = prefixId || 'issue_';
this.form = form || this.getElement('.bulk-update');
this.$labelDropdown = this.form.find('.js-label-select');
......
......@@ -2,26 +2,13 @@ import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import flash from './flash';
import { s__, __ } from './locale';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import issuableInitBulkUpdateSidebar from './issuable_init_bulk_update_sidebar';
export default class IssuableIndex {
constructor(pagePrefix) {
this.initBulkUpdate(pagePrefix);
issuableInitBulkUpdateSidebar.init(pagePrefix);
IssuableIndex.resetIncomingEmailToken();
}
initBulkUpdate(pagePrefix) {
const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
const alreadyInitialized = Boolean(this.bulkUpdateSidebar);
if (userCanBulkUpdate && !alreadyInitialized) {
IssuableBulkUpdateActions.init({
prefixId: pagePrefix,
});
this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
}
}
static resetIncomingEmailToken() {
const $resetToken = $('.incoming-email-token-reset');
......
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import issuableBulkUpdateActions from './issuable_bulk_update_actions';
export default {
bulkUpdateSidebar: null,
init(prefixId) {
const bulkUpdateEl = document.querySelector('.issues-bulk-update');
const alreadyInitialized = Boolean(this.bulkUpdateSidebar);
if (bulkUpdateEl && !alreadyInitialized) {
issuableBulkUpdateActions.init({ prefixId });
this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
}
return this.bulkUpdateSidebar;
},
};
import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
import { FILTERED_SEARCH } from '~/pages/constants';
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import initManualOrdering from '~/manual_ordering';
const ISSUE_BULK_UPDATE_PREFIX = 'issue_';
document.addEventListener('DOMContentLoaded', () => {
IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
issuableInitBulkUpdateSidebar.init(ISSUE_BULK_UPDATE_PREFIX);
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
......
......@@ -92,7 +92,7 @@ module IssuableActions
end
def bulk_update
result = Issuable::BulkUpdateService.new(project, current_user, bulk_update_params).execute(resource_name)
result = Issuable::BulkUpdateService.new(current_user, bulk_update_params).execute(resource_name)
quantity = result[:count]
render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" }
......@@ -181,7 +181,7 @@ module IssuableActions
end
def authorize_admin_issuable!
unless can?(current_user, :"admin_#{resource_name}", @project) # rubocop:disable Gitlab/ModuleWithInstanceVariables
unless can?(current_user, :"admin_#{resource_name}", parent)
return access_denied!
end
end
......
# frozen_string_literal: true
module Issuable
class BulkUpdateService < IssuableBaseService
class BulkUpdateService
include Gitlab::Allowable
attr_accessor :current_user, :params
def initialize(user = nil, params = {})
@current_user, @params = user, params.dup
end
# rubocop: disable CodeReuse/ActiveRecord
def execute(type)
model_class = type.classify.constantize
......
- @can_bulk_update = can?(current_user, :admin_issue, @group)
- page_title "Issues"
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@group.name} issues")
......@@ -9,8 +11,15 @@
= render 'shared/issuable/nav', type: :issues
.nav-controls
= render 'shared/issuable/feed_buttons'
- if @can_bulk_update
= render_if_exists 'shared/issuable/bulk_update_button'
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues, with_feature_enabled: 'issues', with_shared: false, include_projects_in_subgroups: true
= render 'shared/issuable/search_bar', type: :issues
- if @can_bulk_update
= render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :issues
= render 'shared/issues'
# frozen_string_literal: true
class Groups::BulkUpdateController < Groups::ApplicationController
include IssuableActions
before_action :authorize_admin_group!
before_action :verify_group_bulk_edit_enabled!, only: [:bulk_update]
private
def verify_group_bulk_edit_enabled!
render_404 unless group.feature_available?(:group_bulk_edit)
end
end
# frozen_string_literal: true
class Groups::IssuesController < Groups::BulkUpdateController
end
......@@ -15,6 +15,7 @@ class License < ApplicationRecord
contribution_analytics
elastic_search
export_issues
group_bulk_edit
group_burndown_charts
group_webhooks
issuable_default_templates
......
= button_tag _('Edit issues'), class: 'btn btn-default append-right-10 js-bulk-update-toggle'
- group = local_assigns.fetch(:group)
- type = local_assigns.fetch(:type)
%aside.issues-bulk-update.js-right-sidebar.right-sidebar{ 'aria-live' => 'polite', data: { 'signed-in': current_user.present? } }
.issuable-sidebar.hidden
= form_tag [:bulk_update, group, type], method: :post, class: "bulk-update" do
.block.issuable-sidebar-header
.filter-item.inline.update-issues-btn.float-left
= button_tag _('Update all'), class: "btn update-selected-issues btn-info", disabled: true
= button_tag _('Cancel'), class: "btn btn-default js-bulk-update-menu-hide float-right"
.block
.title
= _('Milestone')
.filter-item
= dropdown_tag(_('Select milestone'), options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: 'dropdown-menu-selectable dropdown-menu-milestone', placeholder: _('Search milestones'), data: { show_no: true, field_name: 'update[milestone_id]', milestones: milestones_filter_path(only_group_milestones: true, format: :json), use_id: true, default_label: _('Milestone') } })
= hidden_field_tag 'update[issuable_ids]', []
= hidden_field_tag :state_event, params[:state_event]
---
title: Allow bulk editing group issues
merge_request: 14141
author:
type: added
......@@ -72,6 +72,12 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
end
end
resources :issues, only: [] do
collection do
post :bulk_update
end
end
resources :todos, only: [:create]
resources :boards, only: [:create, :update, :destroy] do
collection do
......
......@@ -80,4 +80,12 @@ describe 'Group routing', "routing" do
expect(get('/groups/gitlabhq/-/packages')).to route_to('groups/packages#index', group_id: 'gitlabhq')
end
end
describe 'issues' do
it 'routes post to #bulk_update' do
allow(Group).to receive(:find_by_full_path).with('gitlabhq', any_args).and_return(true)
expect(post('/groups/gitlabhq/-/issues/bulk_update')).to route_to('groups/issues#bulk_update', group_id: 'gitlabhq')
end
end
end
......@@ -12226,6 +12226,9 @@ msgstr ""
msgid "Select merge moment"
msgstr ""
msgid "Select milestone"
msgstr ""
msgid "Select private project"
msgstr ""
......@@ -14946,6 +14949,9 @@ msgstr ""
msgid "Update"
msgstr ""
msgid "Update all"
msgstr ""
msgid "Update approvers"
msgstr ""
......
......@@ -2,14 +2,14 @@ import $ from 'jquery';
import MockAdaptor from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import IssuableIndex from '~/issuable_index';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
describe('Issuable', () => {
let Issuable;
describe('initBulkUpdate', () => {
it('should not set bulkUpdateSidebar', () => {
Issuable = new IssuableIndex('issue_');
new IssuableIndex('issue_'); // eslint-disable-line no-new
expect(Issuable.bulkUpdateSidebar).not.toBeDefined();
expect(issuableInitBulkUpdateSidebar.bulkUpdateSidebar).toBeNull();
});
it('should set bulkUpdateSidebar', () => {
......@@ -17,9 +17,9 @@ describe('Issuable', () => {
element.classList.add('issues-bulk-update');
document.body.appendChild(element);
Issuable = new IssuableIndex('issue_');
new IssuableIndex('issue_'); // eslint-disable-line no-new
expect(Issuable.bulkUpdateSidebar).toBeDefined();
expect(issuableInitBulkUpdateSidebar.bulkUpdateSidebar).toBeDefined();
});
});
......@@ -36,7 +36,7 @@ describe('Issuable', () => {
input.setAttribute('id', 'issuable_email');
document.body.appendChild(input);
Issuable = new IssuableIndex('issue_');
new IssuableIndex('issue_'); // eslint-disable-line no-new
mock = new MockAdaptor(axios);
......
......@@ -11,9 +11,27 @@ describe Issuable::BulkUpdateService do
.reverse_merge(issuable_ids: Array(issuables).map(&:id).join(','))
type = Array(issuables).first.model_name.param_key
Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute(type)
Issuable::BulkUpdateService.new(user, bulk_update_params).execute(type)
end
shared_examples 'updates milestones' do
it 'succeeds' do
result = bulk_update(issues, milestone_id: milestone.id)
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(issues.count)
end
it 'updates the issues milestone' do
bulk_update(issues, milestone_id: milestone.id)
issues.each do |issue|
expect(issue.reload.milestone).to eq(milestone)
end
end
end
context 'with project issues' do
describe 'close issues' do
let(:issues) { create_list(:issue, 2, project: project) }
......@@ -153,20 +171,10 @@ describe Issuable::BulkUpdateService do
end
describe 'updating milestones' do
let(:issue) { create(:issue, project: project) }
let(:issues) { [create(:issue, project: project)] }
let(:milestone) { create(:milestone, project: project) }
it 'succeeds' do
result = bulk_update(issue, milestone_id: milestone.id)
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(1)
end
it 'updates the issue milestone' do
expect { bulk_update(issue, milestone_id: milestone.id) }
.to change { issue.reload.milestone }.from(nil).to(milestone)
end
it_behaves_like 'updates milestones'
end
describe 'updating labels' do
......@@ -350,4 +358,24 @@ describe Issuable::BulkUpdateService do
end
end
end
end
context 'with group issues' do
let(:group) { create(:group) }
context 'updating milestone' do
let(:milestone) { create(:milestone, group: group) }
let(:project1) { create(:project, :repository, group: group) }
let(:project2) { create(:project, :repository, group: group) }
let(:issue1) { create(:issue, project: project1) }
let(:issue2) { create(:issue, project: project2) }
let(:issues) { [issue1, issue2] }
before do
group.add_maintainer(user)
end
it_behaves_like 'updates milestones'
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment