diff options
author | Heinrich Lee Yu <heinrich@gitlab.com> | 2018-12-04 16:42:27 +0800 |
---|---|---|
committer | Heinrich Lee Yu <hleeyu@gmail.com> | 2019-01-07 11:16:58 +0800 |
commit | 876ab436fabf2f44e2a6912262f980256b7c9736 (patch) | |
tree | d9c94ea5ce9efe8d8ef588da132a143b1f526139 | |
parent | b83be5032716548ea9d738a03e0a20f660dc04ac (diff) | |
download | gitlab-ce-876ab436fabf2f44e2a6912262f980256b7c9736.tar.gz |
Add Import CSV Frontend
Added button and modal to accept CSV file for uploading
-rw-r--r-- | app/assets/stylesheets/framework/modal.scss | 38 | ||||
-rw-r--r-- | app/assets/stylesheets/framework/variables.scss | 1 | ||||
-rw-r--r-- | app/assets/stylesheets/pages/issues.scss | 8 | ||||
-rw-r--r-- | app/controllers/projects/issues_controller.rb | 13 | ||||
-rw-r--r-- | app/views/projects/issues/_import_export.svg | 1 | ||||
-rw-r--r-- | app/views/projects/issues/_nav_btns.html.haml | 41 | ||||
-rw-r--r-- | app/views/projects/issues/import_csv/_button.html.haml | 9 | ||||
-rw-r--r-- | app/views/projects/issues/import_csv/_modal.html.haml | 25 | ||||
-rw-r--r-- | app/views/projects/issues/index.html.haml | 5 | ||||
-rw-r--r-- | app/views/shared/empty_states/_issues.html.haml | 13 | ||||
-rw-r--r-- | app/views/shared/issuable/_feed_buttons.html.haml | 4 | ||||
-rw-r--r-- | app/views/shared/issuable/_filter.html.haml | 32 | ||||
-rw-r--r-- | config/routes/project.rb | 1 |
13 files changed, 138 insertions, 53 deletions
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 46d40ea7aa5..ace46e32b18 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -101,3 +101,41 @@ body.modal-open { 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; + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index d92d81b2cb5..242977e8543 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -656,6 +656,7 @@ $border-color-settings: #e1e1e1; Modals */ $modal-body-height: 134px; +$modal-border-color: #e9ecef; $priority-label-empty-state-width: 114px; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index bb6b6f84849..6c847fc0d53 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -155,6 +155,14 @@ ul.related-merge-requests > li { } } +.issues-nav-controls { + font-size: 0; + + .btn-group:empty { + display: none; + } +} + .issuable-email-modal-btn { padding: 0; color: $blue-600; diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 5ed46fc0545..ae48a02f623 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -10,7 +10,7 @@ class Projects::IssuesController < Projects::ApplicationController include SpammableActions def self.issue_except_actions - %i[index calendar new create bulk_update] + %i[index calendar new create bulk_update import_csv] end def self.set_issuables_index_only_actions @@ -155,11 +155,11 @@ class Projects::IssuesController < Projects::ApplicationController def can_create_branch can_create = current_user && can?(current_user, :push_code, @project) && - @issue.can_be_worked_on? + issue.can_be_worked_on? respond_to do |format| 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 @@ -175,6 +175,13 @@ class Projects::IssuesController < Projects::ApplicationController 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 # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/views/projects/issues/_import_export.svg b/app/views/projects/issues/_import_export.svg new file mode 100644 index 00000000000..53c35d12f57 --- /dev/null +++ b/app/views/projects/issues/_import_export.svg @@ -0,0 +1 @@ +<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 diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml index e4a0d4b8479..4a4b8a9fcad 100644 --- a/app/views/projects/issues/_nav_btns.html.haml +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -1,11 +1,30 @@ -= render 'shared/issuable/feed_buttons' - -- 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" +- 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) +- show_export_button = local_assigns.fetch(:show_export_button, true) + +.nav-controls.issues-nav-controls + - if show_feed_buttons + = render 'shared/issuable/feed_buttons' + + .btn-group.append-right-10< + - if show_export_button + = 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' diff --git a/app/views/projects/issues/import_csv/_button.html.haml b/app/views/projects/issues/import_csv/_button.html.haml new file mode 100644 index 00000000000..acc2c50294f --- /dev/null +++ b/app/views/projects/issues/import_csv/_button.html.haml @@ -0,0 +1,9 @@ +- 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') + diff --git a/app/views/projects/issues/import_csv/_modal.html.haml b/app/views/projects/issues/import_csv/_modal.html.haml new file mode 100644 index 00000000000..e8576a2f17d --- /dev/null +++ b/app/views/projects/issues/import_csv/_modal.html.haml @@ -0,0 +1,25 @@ +.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') diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 1e7737aeb97..39e9e9171cf 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -11,8 +11,7 @@ %div{ class: (container_class) } .top-area = 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 - if @can_bulk_update @@ -23,4 +22,4 @@ - if new_issue_email = render 'projects/issuable_by_email', email: new_issue_email, issuable_type: 'issue' - 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 diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index 0ddc56dc6c3..76f6a294cb3 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -1,5 +1,6 @@ - button_path = local_assigns.fetch(:button_path, 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 .row.empty-state @@ -21,12 +22,20 @@ - if has_button .text-center - 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 - = 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 %h4.text-center= _("There are no issues to show") %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.") .text-center = link_to _('Register / Sign In'), new_user_session_path, class: 'btn btn-success' + +- if show_import_button + = render 'projects/issues/import_csv/modal' + diff --git a/app/views/shared/issuable/_feed_buttons.html.haml b/app/views/shared/issuable/_feed_buttons.html.haml index d4834090413..83f60fa6fe2 100644 --- a/app/views/shared/issuable/_feed_buttons.html.haml +++ b/app/views/shared/issuable/_feed_buttons.html.haml @@ -1,4 +1,4 @@ -= 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') -= 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') diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml deleted file mode 100644 index 2ca4657851c..00000000000 --- a/app/views/shared/issuable/_filter.html.haml +++ /dev/null @@ -1,32 +0,0 @@ -.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 diff --git a/config/routes/project.rb b/config/routes/project.rb index f50bf5ab76f..cf5a57300cf 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -361,6 +361,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end collection do post :bulk_update + post :import_csv end end |