Commit 876ab436 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu Committed by Heinrich Lee Yu

Add Import CSV Frontend

Added button and modal to accept CSV file for uploading
parent b83be503
...@@ -101,3 +101,41 @@ body.modal-open { ...@@ -101,3 +101,41 @@ body.modal-open {
margin: 0; margin: 0;
} }
} }
.issues-import-modal,
.issues-export-modal {
.modal-header {
justify-content: flex-start;
.import-export-svg-container {
flex-grow: 1;
height: 56px;
padding: $gl-btn-padding $gl-btn-padding 0;
> svg {
float: right;
height: 100%;
}
}
}
.modal-body {
padding: 0;
.modal-subheader {
justify-content: flex-start;
align-items: center;
border-bottom: 1px solid $modal-border-color;
padding: 14px;
}
.modal-text {
padding: $gl-padding-24 $gl-padding;
min-height: $modal-body-height;
}
}
.checkmark {
color: $green-400;
}
}
...@@ -656,6 +656,7 @@ $border-color-settings: #e1e1e1; ...@@ -656,6 +656,7 @@ $border-color-settings: #e1e1e1;
Modals Modals
*/ */
$modal-body-height: 134px; $modal-body-height: 134px;
$modal-border-color: #e9ecef;
$priority-label-empty-state-width: 114px; $priority-label-empty-state-width: 114px;
......
...@@ -155,6 +155,14 @@ ul.related-merge-requests > li { ...@@ -155,6 +155,14 @@ ul.related-merge-requests > li {
} }
} }
.issues-nav-controls {
font-size: 0;
.btn-group:empty {
display: none;
}
}
.issuable-email-modal-btn { .issuable-email-modal-btn {
padding: 0; padding: 0;
color: $blue-600; color: $blue-600;
......
...@@ -10,7 +10,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -10,7 +10,7 @@ class Projects::IssuesController < Projects::ApplicationController
include SpammableActions include SpammableActions
def self.issue_except_actions def self.issue_except_actions
%i[index calendar new create bulk_update] %i[index calendar new create bulk_update import_csv]
end end
def self.set_issuables_index_only_actions def self.set_issuables_index_only_actions
...@@ -155,11 +155,11 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -155,11 +155,11 @@ class Projects::IssuesController < Projects::ApplicationController
def can_create_branch def can_create_branch
can_create = current_user && can_create = current_user &&
can?(current_user, :push_code, @project) && can?(current_user, :push_code, @project) &&
@issue.can_be_worked_on? issue.can_be_worked_on?
respond_to do |format| respond_to do |format|
format.json do format.json do
render json: { can_create_branch: can_create, suggested_branch_name: @issue.suggested_branch_name } render json: { can_create_branch: can_create, suggested_branch_name: issue.suggested_branch_name }
end end
end end
end end
...@@ -175,6 +175,13 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -175,6 +175,13 @@ class Projects::IssuesController < Projects::ApplicationController
end end
end end
def import_csv
redirect_to(
project_issues_path(project),
notice: _("Your issues are being imported. Once finished, you'll get a confirmation email.")
)
end
protected protected
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 238 111" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="4" width="82" rx="3" height="28" fill="#fff"/><path id="5" d="m68.926 12.09v-2.41c0-.665-.437-.888-.975-.507l-6.552 4.631c-.542.383-.539.998 0 1.379l6.552 4.631c.542.383.975.154.975-.507v-2.41h4.874c.668 0 1.2-.538 1.2-1.201v-2.406c0-.668-.537-1.201-1.2-1.201h-4.874" fill="#fc8a51"/><path id="6" d="m4 24h74v-20h-74v20m-4-21c0-1.655 1.338-2.996 2.991-2.996h76.02c1.652 0 2.991 1.35 2.991 2.996v22.01c0 1.655-1.338 2.996-2.991 2.996h-76.02c-1.652 0-2.991-1.35-2.991-2.996v-22.01"/><circle id="2" cx="16" cy="14" r="7"/><circle id="0" cx="16" cy="14" r="7"/><mask id="3" width="14" height="14" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="1" width="14" height="14" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask></defs><g fill="none" fill-rule="evenodd"><rect width="98" height="111" fill="#fff" rx="6"/><path fill="#e5e5e5" fill-rule="nonzero" d="m4 6.01v98.99c0 1.11.897 2.01 2 2.01h85.998c1.105 0 2-.897 2-2.01v-98.99c0-1.11-.897-2.01-2-2.01h-85.998c-1.105 0-2 .897-2 2.01m-4 0c0-3.318 2.685-6.01 6-6.01h85.998c3.314 0 6 2.689 6 6.01v98.99c0 3.318-2.685 6.01-6 6.01h-85.998c-3.314 0-6-2.689-6-6.01v-98.99"/><rect width="76" height="85" x="11" y="12" fill="#f9f9f9" rx="3"/><g transform="translate(37 59)"><use xlink:href="#4"/><path fill="#e5e5e5" fill-rule="nonzero" d="m4 24h74v-20h-74v20m-4-21c0-1.655 1.338-2.996 2.991-2.996h76.02c1.652 0 2.991 1.35 2.991 2.996v22.01c0 1.655-1.338 2.996-2.991 2.996h-76.02c-1.652 0-2.991-1.35-2.991-2.996v-22.01"/><use fill="#fff" stroke="#6b4fbb" stroke-width="8" mask="url(#1)" xlink:href="#0"/><use xlink:href="#5"/></g><g transform="translate(140)"><path fill="#fff" d="m0 4h94v103h-94z"/><path fill="#e5e5e5" fill-rule="nonzero" d="m0 74v30.993c0 3.318 2.687 6.01 6 6.01h85.998c3.316 0 6-2.69 6-6.01v-98.99c0-3.318-2.687-6.01-6-6.01h-85.998c-3.316 0-6 2.69-6 6.01v.993h4v-.993c0-1.11.896-2.01 2-2.01h85.998c1.105 0 2 .897 2 2.01v98.99c0 1.11-.896 2.01-2 2.01h-85.998c-1.105 0-2-.897-2-2.01v-30.993h-4"/><g fill="#f9f9f9"><rect width="82" height="28" x="8" y="12" rx="3"/><rect width="82" height="28" x="8" y="43" rx="3"/></g></g><g fill-rule="nonzero" transform="translate(148 73)"><use fill="#e5e5e5" xlink:href="#6"/><path fill="#6b4fbb" d="m17 17c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3m0 4c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7"/></g><g transform="translate(25 24)"><use xlink:href="#4"/><use fill="#e5e5e5" fill-rule="nonzero" xlink:href="#6"/><use fill="#fff" stroke="#6b4fbb" stroke-width="8" mask="url(#3)" xlink:href="#2"/><use xlink:href="#5"/></g><g transform="translate(107 10)"><use xlink:href="#4"/><use fill="#fc8a51" fill-opacity=".3" fill-rule="nonzero" xlink:href="#6"/><path fill="#6b4fbb" fill-rule="nonzero" d="m16 17c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3m0 4c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7" id="7"/><use xlink:href="#5"/></g><g transform="translate(128 41)"><use xlink:href="#4"/><use fill="#fc8a51" fill-opacity=".3" fill-rule="nonzero" xlink:href="#6"/><use xlink:href="#7"/><path fill="#fc8a51" d="m66.926 12.09v-2.41c0-.665-.437-.888-.975-.507l-6.552 4.631c-.542.383-.539.998 0 1.379l6.552 4.631c.542.383.975.154.975-.507v-2.41h4.874c.668 0 1.2-.538 1.2-1.201v-2.406c0-.668-.537-1.201-1.2-1.201h-4.874"/></g></g></svg>
\ No newline at end of file
= render 'shared/issuable/feed_buttons' - show_feed_buttons = local_assigns.fetch(:show_feed_buttons, true)
- show_import_button = local_assigns.fetch(:show_import_button, true) && Feature.enabled?(:issues_import_csv)
- if @can_bulk_update - show_export_button = local_assigns.fetch(:show_export_button, true)
= button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
- if show_new_issue_link?(@project) .nav-controls.issues-nav-controls
= link_to "New issue", new_project_issue_path(@project, - if show_feed_buttons
issue: { assignee_id: finder.assignee.try(:id), = render 'shared/issuable/feed_buttons'
milestone_id: finder.milestones.first.try(:id) }),
class: "btn btn-success", .btn-group.append-right-10<
title: "New issue", - if show_export_button
id: "new_issue_link" = render_if_exists 'projects/issues/export_csv/button'
- if show_import_button
= render 'projects/issues/import_csv/button'
- if @can_bulk_update
= button_tag _("Edit issues"), class: "btn btn-default append-right-10 js-bulk-update-toggle"
- if show_new_issue_link?(@project)
= link_to _("New issue"), new_project_issue_path(@project,
issue: { assignee_id: finder.assignee.try(:id),
milestone_id: finder.milestones.first.try(:id) }),
class: "btn btn-success",
title: _("New issue"),
id: "new_issue_link"
- if show_export_button
= render_if_exists 'projects/issues/export_csv/modal'
- if show_import_button
= render 'projects/issues/import_csv/modal'
- type = local_assigns.fetch(:type, :icon)
%button.csv-import-button.btn{ title: _('Import CSV'), class: ('has-tooltip' if type == :icon),
data: { toggle: 'modal', target: '.issues-import-modal' } }
- if type == :icon
= sprite_icon('upload')
- else
= _('Import CSV')
.issues-import-modal.modal
.modal-dialog
.modal-content
= form_tag [:import_csv, @project.namespace.becomes(Namespace), @project, :issues], multipart: true do
.modal-header
%h3
= _('Import issues')
.import-export-svg-container
= render 'projects/issues/import_export.svg'
%a.close{ href: '#', 'data-dismiss' => 'modal' } ×
.modal-body
.modal-text
%p
= _("Your issues will be imported in the background. Once finished, you'll get a confirmation email.")
.form-group
= label_tag :file, _('Upload CSV File'), class: 'label-bold'
%div
= file_field_tag :file, accept: '.csv', required: true
%p.text-secondary
= _('It must have a header row and at least two columns: the first column is the issue title and the second column is the issue description. The separator is automatically detected.')
%p.text-secondary
= _('The maximum file size allowed is %{size}.') % {size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes)}
.modal-footer
%button{ type: 'submit', class: 'btn btn-success', title: _('Import issues') }
= _('Import issues')
...@@ -11,8 +11,7 @@ ...@@ -11,8 +11,7 @@
%div{ class: (container_class) } %div{ class: (container_class) }
.top-area .top-area
= render 'shared/issuable/nav', type: :issues = render 'shared/issuable/nav', type: :issues
.nav-controls = render "projects/issues/nav_btns"
= render "projects/issues/nav_btns"
= render 'shared/issuable/search_bar', type: :issues = render 'shared/issuable/search_bar', type: :issues
- if @can_bulk_update - if @can_bulk_update
...@@ -23,4 +22,4 @@ ...@@ -23,4 +22,4 @@
- if new_issue_email - if new_issue_email
= render 'projects/issuable_by_email', email: new_issue_email, issuable_type: 'issue' = render 'projects/issuable_by_email', email: new_issue_email, issuable_type: 'issue'
- else - else
= render 'shared/empty_states/issues', button_path: new_project_issue_path(@project) = render 'shared/empty_states/issues', button_path: new_project_issue_path(@project), show_import_button: true
- button_path = local_assigns.fetch(:button_path, false) - button_path = local_assigns.fetch(:button_path, false)
- project_select_button = local_assigns.fetch(:project_select_button, false) - project_select_button = local_assigns.fetch(:project_select_button, false)
- show_import_button = local_assigns.fetch(:show_import_button, false)
- has_button = button_path || project_select_button - has_button = button_path || project_select_button
.row.empty-state .row.empty-state
...@@ -21,12 +22,20 @@ ...@@ -21,12 +22,20 @@
- if has_button - if has_button
.text-center .text-center
- if project_select_button - if project_select_button
= render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues, with_feature_enabled: 'issues' = render 'shared/new_project_item_select', path: 'issues/new', label: _('New issue'), type: :issues, with_feature_enabled: 'issues'
- else - else
= link_to 'New issue', button_path, class: 'btn btn-success', title: 'New issue', id: 'new_issue_link' = link_to _('New issue'), button_path, class: 'btn btn-success', title: _('New issue'), id: 'new_issue_link'
- if show_import_button
= render 'projects/issues/import_csv/button', type: :text
- else - else
%h4.text-center= _("There are no issues to show") %h4.text-center= _("There are no issues to show")
%p %p
= _("The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.") = _("The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.")
.text-center .text-center
= link_to _('Register / Sign In'), new_user_session_path, class: 'btn btn-success' = link_to _('Register / Sign In'), new_user_session_path, class: 'btn btn-success'
- if show_import_button
= render 'projects/issues/import_csv/modal'
= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe to RSS feed' do = link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to RSS feed') do
= icon('rss') = icon('rss')
= link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe to calendar' do = link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar') do
= custom_icon('icon_calendar') = custom_icon('icon_calendar')
.issues-filters
.issues-details-filters.row-content-block.second-block
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
- if params[:search].present?
= hidden_field_tag :search, params[:search]
.issues-other-filters
.filter-item.inline
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
= dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user&.username, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
.filter-item.inline
- if params[:assignee_id].present?
= hidden_field_tag(:assignee_id, params[:assignee_id])
= dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user&.username, null_user: true, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
.filter-item.inline.milestone-filter
= render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", selected: selected_labels, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
- unless @no_filters_set
.float-right
= render 'shared/issuable/sort_dropdown'
- has_labels = @labels && @labels.any?
.row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
- if has_labels
= render 'shared/labels_row', labels: @labels
...@@ -361,6 +361,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -361,6 +361,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end end
collection do collection do
post :bulk_update post :bulk_update
post :import_csv
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