Commit 321c64b4 authored by Max Woolf's avatar Max Woolf

Add ability to export MRs to CSV

This commit adds the button required
to trigger an async download of MRs as
a CSV.

It is behind a feature flag
'export_merge_requests_as_csv'

Add ability to export MRs to CSV

This commit adds the button required
to trigger an async download of MRs as
a CSV.

It is behind a feature flag
'export_merge_requests_as_csv'
parent 5621392d
......@@ -128,7 +128,7 @@ body.modal-open {
}
.issues-import-modal,
.issues-export-modal {
.issuable-export-modal {
.modal-header {
justify-content: flex-start;
......
......@@ -12,7 +12,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include SourcegraphDecorator
include DiffHelper
skip_before_action :merge_request, only: [:index, :bulk_update]
skip_before_action :merge_request, only: [:index, :bulk_update, :export_csv]
before_action :apply_diff_view_cookie!, only: [:show]
before_action :whitelist_query_limiting, only: [:assign_related_issues, :update]
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
......@@ -318,6 +318,16 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
super
end
def export_csv
return render_404 unless Feature.enabled?(:export_merge_requests_as_csv, project)
IssuableExportCsvWorker.perform_async(:merge_request, current_user.id, project.id, finder_options.to_h) # rubocop:disable CodeReuse/Worker
index_path = project_merge_requests_path(project)
message = _('Your CSV export has started. It will be emailed to %{email} when complete.') % { email: current_user.notification_email }
redirect_to(index_path, notice: message)
end
protected
alias_method :subscribable_resource, :merge_request
......
......@@ -8,7 +8,7 @@
.btn-group
- if show_export_button
= render 'projects/issues/export_csv/button'
= render 'shared/issuable/csv_export/button', issuable_type: 'issues'
- if show_import_button
= render 'projects/issues/import_csv/button'
......@@ -23,7 +23,7 @@
id: "new_issue_link"
- if show_export_button
= render 'projects/issues/export_csv/modal'
= render 'shared/issuable/csv_export/modal', issuable_type: 'issues'
- if show_import_button
= render 'projects/issues/import_csv/modal'
- if current_user
.issues-export-modal.modal
.modal-dialog
.modal-content{ data: { qa_selector: 'export_issues_modal' } }
.modal-header
%h3
= _('Export issues')
.svg-content.import-export-svg-container
= image_tag 'illustrations/export-import.svg', alt: _('Import/Export illustration'), class: 'illustration'
%a.close{ href: '#', 'data-dismiss' => 'modal' }
= sprite_icon('close', css_class: 'gl-icon')
.modal-body
- issues_count = issuables_count_for_state(:issues, params[:state])
- unless issues_count == -1 # The count timed out
.modal-subheader
= sprite_icon('check', css_class: 'gl-text-green-400')
%strong.gl-ml-3
= n_('%d issue selected', '%d issues selected', issues_count) % issues_count
.modal-text
= html_escape(_('The CSV export will be created in the background. Once finished, it will be sent to %{strong_open}%{email}%{strong_close} in an attachment.')) % { email: @current_user.notification_email, strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
.modal-footer
= link_to _('Export issues'), export_csv_project_issues_path(@project, request.query_parameters), method: :post, class: 'btn gl-button btn-success float-left', title: _('Export issues'), data: { track_label: "export_issues_csv", track_event: "click_button", track_value: "", qa_selector: "export_issues_button" }
- if Feature.enabled?(:export_merge_requests_as_csv, @project)
.btn-group
= render 'shared/issuable/csv_export/button', issuable_type: 'merge-requests'
- if @can_bulk_update
= button_tag "Edit merge requests", class: "gl-button btn gl-mr-3 js-bulk-update-toggle"
- if merge_project
= link_to new_merge_request_path, class: "gl-button btn btn-success", title: "New merge request" do
New merge request
- if Feature.enabled?(:export_merge_requests_as_csv, @project)
= render 'shared/issuable/csv_export/modal', issuable_type: 'merge_requests'
- if current_user
%button.csv_download_link.btn.gl-button.has-tooltip{ title: _('Export as CSV'),
data: { toggle: 'modal', target: '.issues-export-modal', qa_selector: 'export_as_csv_button' } }
data: { toggle: 'modal', target: ".#{issuable_type}-export-modal", qa_selector: 'export_as_csv_button' } }
= sprite_icon('export')
- class_name = "#{issuable_type.dasherize}-export-modal"
- if current_user
.modal.issuable-export-modal{ class: class_name }
.modal-dialog
.modal-content{ data: { qa_selector: "export_issuable_modal" } }
.modal-header
%h3
= _("Export %{issuable_type}" % { issuable_type: issuable_type.humanize(capitalize: false) })
.svg-content.import-export-svg-container
= image_tag 'illustrations/export-import.svg', role: "presentation", class: 'illustration'
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
= sprite_icon('close', css_class: 'gl-icon')
.modal-body
- issuable_count = issuables_count_for_state(issuable_type.to_sym, params[:state])
- unless issuable_count == -1 # The count timed out
.modal-subheader
= sprite_icon('check', css_class: 'gl-icon gl-color-green-400')
%strong.gl-ml-3
- if issuable_type.eql?('merge_requests')
= _("%{count} %{pluralized_subject} selected") % { count: issuable_count, pluralized_subject: n_('merge request', 'merge requests', issuable_count) }
- else
= _("%{count} %{pluralized_subject} selected") % { count: issuable_count, pluralized_subject: n_('issue', 'issues', issuable_count) }
.modal-text
= html_escape(_('The CSV export will be created in the background. Once finished, it will be sent to %{strong_open}%{email}%{strong_close} in an attachment.')) % { email: @current_user.notification_email, strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
.modal-footer
- if issuable_type.eql?('merge_requests')
= link_to _("Export merge requests"), export_csv_project_merge_requests_path(@project, request.query_parameters), method: :post, class: 'btn gl-button btn-success', data: { track_label: "export_merge_requests_csv", track_event: "click_button", track_value: "" }
- else
= link_to _('Export issues'), export_csv_project_issues_path(@project, request.query_parameters), method: :post, class: 'btn gl-button btn-success', data: { track_label: "export_issues_csv", track_event: "click_button", track_value: "", qa_selector: "export_issues_button" }
---
name: export_merge_requests_as_csv
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45130
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267129
type: development
group: group::compliance
default_enabled: false
......@@ -47,6 +47,7 @@ resources :merge_requests, concerns: :awardable, except: [:new, :create, :show],
collection do
get :diff_for_path
post :bulk_update
post :export_csv
end
resources :discussions, only: [:show], constraints: { id: /\h{40}/ } do
......
......@@ -220,11 +220,6 @@ msgid_plural "%d issues in this group"
msgstr[0] ""
msgstr[1] ""
msgid "%d issue selected"
msgid_plural "%d issues selected"
msgstr[0] ""
msgstr[1] ""
msgid "%d issue successfully imported with the label"
msgid_plural "%d issues successfully imported with the label"
msgstr[0] ""
......@@ -395,6 +390,9 @@ msgstr ""
msgid "%{cores} cores"
msgstr ""
msgid "%{count} %{pluralized_subject} selected"
msgstr ""
msgid "%{count} %{scope} for term '%{term}'"
msgstr ""
......@@ -10876,6 +10874,9 @@ msgstr ""
msgid "Export issues"
msgstr ""
msgid "Export merge requests"
msgstr ""
msgid "Export project"
msgstr ""
......@@ -31495,7 +31496,9 @@ msgid "is too long (maximum is 1000 entries)"
msgstr ""
msgid "issue"
msgstr ""
msgid_plural "issues"
msgstr[0] ""
msgstr[1] ""
msgid "issues at risk"
msgstr ""
......
......@@ -15,13 +15,13 @@ module QA
element :avatar_counter_content
end
view 'app/views/projects/issues/export_csv/_button.html.haml' do
view 'app/views/shared/issuable/csv_export/_button.html.haml' do
element :export_as_csv_button
end
view 'app/views/projects/issues/export_csv/_modal.html.haml' do
view 'app/views/shared/issuable/csv_export/_modal.html.haml' do
element :export_issues_button
element :export_issues_modal
element :export_issuable_modal
end
view 'app/views/projects/issues/import_csv/_button.html.haml' do
......@@ -64,7 +64,7 @@ module QA
end
def export_issues_modal
find_element(:export_issues_modal)
find_element(:export_issuable_modal)
end
def go_to_jira_import_form
......
......@@ -1994,4 +1994,37 @@ RSpec.describe Projects::MergeRequestsController do
expect(assigns(:noteable)).not_to be_nil
end
end
describe 'POST export_csv' do
subject { post :export_csv, params: { namespace_id: project.namespace, project_id: project } }
before do
stub_feature_flags(export_merge_requests_as_csv: project)
end
it 'redirects to the merge request index' do
subject
expect(response).to redirect_to(project_merge_requests_path(project))
expect(response.flash[:notice]).to match(/\AYour CSV export has started/i)
end
it 'enqueues an IssuableExportCsvWorker worker' do
expect(IssuableExportCsvWorker).to receive(:perform_async).with(:merge_request, user.id, project.id, anything)
subject
end
context 'feature is disabled' do
before do
stub_feature_flags(export_merge_requests_as_csv: false)
end
it 'expects a 404 response' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Merge Requests > Exports as CSV', :js do
let!(:project) { create(:project, :public, :repository) }
let!(:user) { project.creator }
let!(:open_mr) { create(:merge_request, title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1') }
before do
sign_in(user)
end
subject { page.find('.nav-controls') }
context 'feature is not enabled' do
before do
stub_feature_flags(export_merge_requests_as_csv: false)
visit(project_merge_requests_path(project))
end
it { is_expected.not_to have_button('Export as CSV') }
end
context 'feature is enabled for a project' do
before do
stub_feature_flags(export_merge_requests_as_csv: project)
visit(project_merge_requests_path(project))
end
it { is_expected.to have_button('Export as CSV') }
context 'button is clicked' do
before do
click_button('Export as CSV')
end
it 'shows a success message' do
click_link('Export merge requests')
expect(page).to have_content 'Your CSV export has started.'
expect(page).to have_content "It will be emailed to #{user.email} when complete"
end
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