diff options
author | Robert Speicher <rspeicher@gmail.com> | 2019-01-08 05:03:00 +0300 |
---|---|---|
committer | Robert Speicher <rspeicher@gmail.com> | 2019-01-08 05:03:00 +0300 |
commit | d0bb16fcdc38c6491ff48daf139ae41e55b355ca (patch) | |
tree | 66679c94edf0e98eda497305fadc49d404d89548 /app | |
parent | f2fee7bc6b363774fb1c8e1b154b529db2dff02c (diff) | |
parent | 710f2ec50c49d1e773acc20058ed584f1402de33 (diff) |
Merge branch 'master' into 11-7-stable
Diffstat (limited to 'app')
44 files changed, 457 insertions, 94 deletions
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 8178821be3d..570d3b712e0 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -221,13 +221,13 @@ class GfmAutoComplete { displayTpl(value) { let tmpl = GfmAutoComplete.Loading.template; if (value.title != null) { - tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title); + tmpl = GfmAutoComplete.Issues.templateFunction(value); } return tmpl; }, data: GfmAutoComplete.defaultLoadingData, - // eslint-disable-next-line no-template-curly-in-string - insertTpl: '${atwho-at}${id}', + insertTpl: GfmAutoComplete.Issues.insertTemplateFunction, + skipSpecialCharacterTest: true, callbacks: { ...this.getDefaultCallbacks(), beforeSave(issues) { @@ -238,6 +238,7 @@ class GfmAutoComplete { return { id: i.iid, title: sanitize(i.title), + reference: i.reference, search: `${i.iid} ${i.title}`, }; }); @@ -287,13 +288,13 @@ class GfmAutoComplete { displayTpl(value) { let tmpl = GfmAutoComplete.Loading.template; if (value.title != null) { - tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title); + tmpl = GfmAutoComplete.Issues.templateFunction(value); } return tmpl; }, data: GfmAutoComplete.defaultLoadingData, - // eslint-disable-next-line no-template-curly-in-string - insertTpl: '${atwho-at}${id}', + insertTpl: GfmAutoComplete.Issues.insertTemplateFunction, + skipSpecialCharacterTest: true, callbacks: { ...this.getDefaultCallbacks(), beforeSave(merges) { @@ -304,6 +305,7 @@ class GfmAutoComplete { return { id: m.iid, title: sanitize(m.title), + reference: m.reference, search: `${m.iid} ${m.title}`, }; }); @@ -397,7 +399,7 @@ class GfmAutoComplete { displayTpl(value) { let tmpl = GfmAutoComplete.Loading.template; if (value.title != null) { - tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title); + tmpl = GfmAutoComplete.Issues.templateFunction(value); } return tmpl; }, @@ -596,8 +598,12 @@ GfmAutoComplete.Labels = { }; // Issues, MergeRequests and Snippets GfmAutoComplete.Issues = { - templateFunction(id, title) { - return `<li><small>${id}</small> ${_.escape(title)}</li>`; + insertTemplateFunction(value) { + // eslint-disable-next-line no-template-curly-in-string + return value.reference || '${atwho-at}${id}'; + }, + templateFunction({ id, title, reference }) { + return `<li><small>${reference || id}</small> ${_.escape(title)}</li>`; }, }; // Milestones diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index 12224e36ba2..e2cffe0b4b4 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -6,6 +6,7 @@ export default { components: { GlAreaChart, }, + inheritAttrs: false, props: { graphData: { type: Object, @@ -25,6 +26,11 @@ export default { ); }, }, + alertData: { + type: Object, + required: false, + default: () => ({}), + }, }, computed: { chartData() { @@ -74,9 +80,6 @@ export default { const [date, value] = params; return [dateFormat(date, 'dd mmm yyyy, h:MMtt'), value.toFixed(3)]; }, - onCreated(chart) { - this.$emit('created', chart); - }, }, }; </script> @@ -88,10 +91,11 @@ export default { <div class="prometheus-graph-widgets"><slot></slot></div> </div> <gl-area-chart + v-bind="$attrs" :data="chartData" :option="chartOptions" :format-tooltip-text="formatTooltipText" - @created="onCreated" + :thresholds="alertData" /> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 2d9c5050c9b..cea5c1a56ca 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -144,6 +144,9 @@ export default { } }, methods: { + getGraphAlerts(graphId) { + return this.alertData ? this.alertData[graphId] || {} : {}; + }, getGraphsData() { this.state = 'loading'; Promise.all([ @@ -223,6 +226,8 @@ export default { :tags-path="tagsPath" :show-legend="showLegend" :small-graph="forceSmallGraph" + :alert-data="getGraphAlerts(graphData.id)" + group-id="monitor-area-chart" > <!-- EE content --> {{ null }} 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/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index 0eea0cdd50f..c114e16edf8 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -7,12 +7,12 @@ module UploadsActions UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze def create - link_to_file = UploadService.new(model, params[:file], uploader_class).execute + uploader = UploadService.new(model, params[:file], uploader_class).execute respond_to do |format| - if link_to_file + if uploader format.json do - render json: { link: link_to_file } + render json: { link: uploader.to_h } end else format.json do diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 5ed46fc0545..21688e54481 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 @@ -37,6 +37,8 @@ class Projects::IssuesController < Projects::ApplicationController # Allow create a new branch and empty WIP merge request from current issue before_action :authorize_create_merge_request_from!, only: [:create_merge_request] + before_action :authorize_import_issues!, only: [:import_csv] + before_action :set_suggested_issues_feature_flags, only: [:new] respond_to :html @@ -175,6 +177,20 @@ class Projects::IssuesController < Projects::ApplicationController end end + def import_csv + return render_404 unless Feature.enabled?(:issues_import_csv) + + if uploader = UploadService.new(project, params[:file]).execute + ImportIssuesCsvWorker.perform_async(current_user.id, project.id, uploader.upload.id) + + flash[:notice] = _("Your issues are being imported. Once finished, you'll get a confirmation email.") + else + flash[:alert] = _("File upload error.") + end + + redirect_to project_issues_path(project) + end + protected # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb index ae8ac61ad46..521ec2acebb 100644 --- a/app/controllers/projects/settings/operations_controller.rb +++ b/app/controllers/projects/settings/operations_controller.rb @@ -6,6 +6,8 @@ module Projects before_action :check_license before_action :authorize_update_environment! + helper_method :error_tracking_setting + def show end @@ -22,13 +24,18 @@ module Projects private + def error_tracking_setting + @error_tracking_setting ||= project.error_tracking_setting || + project.build_error_tracking_setting + end + def update_params params.require(:project).permit(permitted_project_params) end # overridden in EE def permitted_project_params - {} + { error_tracking_setting_attributes: [:enabled, :api_url, :token] } end def check_license diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index af7a262e32c..e67c327f7f8 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -285,7 +285,7 @@ module ProjectsHelper # overridden in EE def settings_operations_available? - false + Feature.enabled?(:error_tracking, @project) && can?(current_user, :read_environment, @project) end private @@ -549,6 +549,7 @@ module ProjectsHelper services#edit repository#show ci_cd#show + operations#show badges#index pages#show ] diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 370e6d2f90b..654ae211310 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -77,6 +77,17 @@ module Emails mail_answer_thread(issue, issue_thread_options(updated_by_user.id, recipient.id, reason)) end + def import_issues_csv_email(user_id, project_id, results) + @user = User.find(user_id) + @project = Project.find(project_id) + @results = results + + mail(to: @user.notification_email, subject: subject('Imported issues')) do |format| + format.html { render layout: 'mailer' } + format.text { render layout: 'mailer' } + end + end + private def setup_issue_mail(issue_id, recipient_id) diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index 2ac4610967d..80e0a17c312 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -76,6 +76,10 @@ class NotifyPreview < ActionMailer::Preview Notify.changed_milestone_issue_email(user.id, issue.id, milestone, user.id) end + def import_issues_csv_email + Notify.import_issues_csv_email(user, project, { success: 3, errors: [5, 6, 7], valid_file: true }) + end + def closed_merge_request_email Notify.closed_merge_request_email(user.id, issue.id, user.id).message end diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb new file mode 100644 index 00000000000..632c64c2f1c --- /dev/null +++ b/app/models/error_tracking/project_error_tracking_setting.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module ErrorTracking + class ProjectErrorTrackingSetting < ActiveRecord::Base + belongs_to :project + + validates :api_url, length: { maximum: 255 }, public_url: true, url: { enforce_sanitization: true } + + attr_encrypted :token, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-gcm' + end +end diff --git a/app/models/note.rb b/app/models/note.rb index becf14e9785..1578ae9c4cc 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -456,6 +456,10 @@ class Note < ActiveRecord::Base Upload.find_by(model: self, path: paths) end + def parent + project + end + private def keep_around_commit diff --git a/app/models/project.rb b/app/models/project.rb index e2f010a0432..cab173503ce 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -187,6 +187,7 @@ class Project < ActiveRecord::Base has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :project_repository, inverse_of: :project + has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting' # Merge Requests for target project should be removed with it has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project @@ -295,6 +296,8 @@ class Project < ActiveRecord::Base allow_destroy: true, reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? } + accepts_nested_attributes_for :error_tracking_setting, update_only: true + delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true delegate :add_user, :add_users, to: :team @@ -1786,6 +1789,24 @@ class Project < ActiveRecord::Base handle_update_attribute_error(e, value) end + # Tries to set repository as read_only, checking for existing Git transfers in progress beforehand + # + # @return [Boolean] true when set to read_only or false when an existing git transfer is in progress + def set_repository_read_only! + with_lock do + break false if git_transfer_in_progress? + + update_column(:repository_read_only, true) + end + end + + # Set repository as writable again + def set_repository_writable! + with_lock do + update_column(repository_read_only, false) + end + end + def pushes_since_gc Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i } end @@ -1900,15 +1921,17 @@ class Project < ActiveRecord::Base def migrate_to_hashed_storage! return unless storage_upgradable? - update!(repository_read_only: true) - - if repo_reference_count > 0 || wiki_reference_count > 0 + if git_transfer_in_progress? ProjectMigrateHashedStorageWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id) else ProjectMigrateHashedStorageWorker.perform_async(id) end end + def git_transfer_in_progress? + repo_reference_count > 0 || wiki_reference_count > 0 + end + def storage_version=(value) super diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 3146f26bed5..d70417e710e 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -222,6 +222,8 @@ class ProjectPolicy < BasePolicy rule { owner | admin | guest | group_member }.prevent :request_access rule { ~request_access_enabled }.prevent :request_access + rule { can?(:developer_access) & can?(:create_issue) }.enable :import_issues + rule { can?(:developer_access) }.policy do enable :admin_merge_request enable :admin_milestone diff --git a/app/services/issues/import_csv_service.rb b/app/services/issues/import_csv_service.rb new file mode 100644 index 00000000000..ef08fafa7cc --- /dev/null +++ b/app/services/issues/import_csv_service.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Issues + class ImportCsvService + def initialize(user, project, csv_io) + @user = user + @project = project + @csv_io = csv_io + @results = { success: 0, error_lines: [], parse_error: false } + end + + def execute + process_csv + email_results_to_user + + @results + end + + private + + def process_csv + csv_data = @csv_io.open(&:read).force_encoding(Encoding::UTF_8) + + CSV.new(csv_data, col_sep: detect_col_sep(csv_data.lines.first), headers: true).each.with_index(2) do |row, line_no| + issue = Issues::CreateService.new(@project, @user, title: row[0], description: row[1]).execute + + if issue.persisted? + @results[:success] += 1 + else + @results[:error_lines].push(line_no) + end + end + rescue ArgumentError, CSV::MalformedCSVError + @results[:parse_error] = true + end + + def email_results_to_user + Notify.import_issues_csv_email(@user.id, @project.id, @results).deliver_now + end + + def detect_col_sep(header) + if header.include?(",") + "," + elsif header.include?(";") + ";" + elsif header.include?("\t") + "\t" + else + raise CSV::MalformedCSVError + end + end + end +end diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb index 4c14d834949..7ee9732040d 100644 --- a/app/services/notes/quick_actions_service.rb +++ b/app/services/notes/quick_actions_service.rb @@ -31,7 +31,7 @@ module Notes return if command_params.empty? return unless supported?(note) - self.class.noteable_update_service(note).new(project, current_user, command_params).execute(note.noteable) + self.class.noteable_update_service(note).new(note.parent, current_user, command_params).execute(note.noteable) end end end diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb index f3e026ba38c..2d851866a18 100644 --- a/app/services/projects/hashed_storage/migrate_repository_service.rb +++ b/app/services/projects/hashed_storage/migrate_repository_service.rb @@ -2,6 +2,8 @@ module Projects module HashedStorage + RepositoryMigrationError = Class.new(StandardError) + class MigrateRepositoryService < BaseService include Gitlab::ShellAdapter @@ -16,6 +18,8 @@ module Projects end def execute + try_to_set_repository_read_only! + @old_storage_version = project.storage_version project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository] project.ensure_storage_path_exists @@ -48,6 +52,16 @@ module Projects private + def try_to_set_repository_read_only! + # Mitigate any push operation to start during migration + unless project.set_repository_read_only! + migration_error = "Target repository '#{old_disk_path}' cannot be made read-only as there is a git transfer in progress" + logger.error migration_error + + raise RepositoryMigrationError, migration_error + end + end + # rubocop: disable CodeReuse/ActiveRecord def has_wiki? gitlab_shell.exists?(project.repository_storage, "#{old_wiki_disk_path}.git") diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb index 7ff0599ee95..abd6d8de750 100644 --- a/app/services/projects/operations/update_service.rb +++ b/app/services/projects/operations/update_service.rb @@ -12,7 +12,7 @@ module Projects private def project_update_params - {} + params.slice(:error_tracking_setting_attributes) end end end diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index d248b10f41e..5c58caee8cd 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -2,6 +2,7 @@ module QuickActions class InterpretService < BaseService + include Gitlab::Utils::StrongMemoize include Gitlab::QuickActions::Dsl attr_reader :issuable @@ -210,15 +211,9 @@ module QuickActions end params '~label1 ~"label 2"' condition do - if project - available_labels = LabelsFinder - .new(current_user, project_id: project.id, include_ancestor_groups: true) - .execute - end - - project && - current_user.can?(:"admin_#{issuable.to_ability_name}", project) && - available_labels.any? + parent && + current_user.can?(:"admin_#{issuable.to_ability_name}", parent) && + find_labels.any? end command :label do |labels_param| label_ids = find_label_ids(labels_param) @@ -245,7 +240,7 @@ module QuickActions issuable.is_a?(Issuable) && issuable.persisted? && issuable.labels.any? && - current_user.can?(:"admin_#{issuable.to_ability_name}", project) + current_user.can?(:"admin_#{issuable.to_ability_name}", parent) end command :unlabel do |labels_param = nil| if labels_param.present? @@ -674,9 +669,25 @@ module QuickActions MilestonesFinder.new(params.merge(project_ids: [project.id], group_ids: [project.group&.id])).execute end - def find_labels(labels_param) - extract_references(labels_param, :label) | - LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split, include_ancestor_groups: true).execute + def parent + project || group + end + + def group + strong_memoize(:group) do + issuable.group if issuable.respond_to?(:group) + end + end + + def find_labels(labels_params = nil) + finder_params = { include_ancestor_groups: true } + finder_params[:project_id] = project.id if project + finder_params[:group_id] = group.id if group + finder_params[:name] = labels_params.split if labels_params + + result = LabelsFinder.new(current_user, finder_params).execute + + extract_references(labels_params, :label) | result end def find_label_references(labels_param) @@ -707,9 +718,11 @@ module QuickActions # rubocop: disable CodeReuse/ActiveRecord def extract_references(arg, type) + return [] unless arg + ext = Gitlab::ReferenceExtractor.new(project, current_user) - ext.analyze(arg, author: current_user) + ext.analyze(arg, author: current_user, group: group) ext.references(type) end diff --git a/app/services/upload_service.rb b/app/services/upload_service.rb index 39909ee4f82..41ca95b3b6f 100644 --- a/app/services/upload_service.rb +++ b/app/services/upload_service.rb @@ -11,7 +11,7 @@ class UploadService uploader = @uploader_class.new(@model, nil, @uploader_context) uploader.store!(@file) - uploader.to_h + uploader end private diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb index 5feb0b0f05b..d3e32818dc7 100644 --- a/app/validators/url_validator.rb +++ b/app/validators/url_validator.rb @@ -26,6 +26,7 @@ # - allow_local_network: Allow urls pointing to private network addresses. Default: true # - ports: Allowed ports. Default: all. # - enforce_user: Validate user format. Default: false +# - enforce_sanitization: Validate that there are no html/css/js tags. Default: false # # Example: # class User < ActiveRecord::Base @@ -70,7 +71,8 @@ class UrlValidator < ActiveModel::EachValidator allow_localhost: true, allow_local_network: true, ascii_only: false, - enforce_user: false + enforce_user: false, + enforce_sanitization: false } end diff --git a/app/views/notify/import_issues_csv_email.html.haml b/app/views/notify/import_issues_csv_email.html.haml new file mode 100644 index 00000000000..f30d2b5f078 --- /dev/null +++ b/app/views/notify/import_issues_csv_email.html.haml @@ -0,0 +1,18 @@ +- text_style = 'font-size:16px; text-align:center; line-height:30px;' + +%p{ style: text_style } + Your CSV import for project + %a{ href: project_url(@project), style: "color:#3777b0; text-decoration:none;" } + = @project.full_name + has been completed. + +%p{ style: text_style } + #{pluralize(@results[:success], 'issue')} imported. + +- if @results[:error_lines].present? + %p{ style: text_style } + Errors found on line #{'number'.pluralize(@results[:error_lines].size)}: #{@results[:error_lines].join(', ')}. Please check if these lines have an issue title. + +- if @results[:parse_error] + %p{ style: text_style } + Error parsing CSV file. Please make sure it has the correct format: a delimited text file that uses a comma to separate values. diff --git a/app/views/notify/import_issues_csv_email.text.erb b/app/views/notify/import_issues_csv_email.text.erb new file mode 100644 index 00000000000..1117f90714d --- /dev/null +++ b/app/views/notify/import_issues_csv_email.text.erb @@ -0,0 +1,11 @@ +Your CSV import for project <%= @project.full_name %> (<%= project_url(@project) %>) has been completed. + +<%= pluralize(@results[:success], 'issue') %> imported. + +<% if @results[:error_lines].present? %> +Errors found on line <%= 'number'.pluralize(@results[:error_lines].size) %>: <%= @results[:error_lines].join(', ') %>. Please check if these lines have an issue title. +<% end %> + +<% if @results[:parse_error] %> +Error parsing CSV file. Please make sure it has the correct format: a delimited text file that uses a comma to separate values. +<% end %> diff --git a/app/views/projects/cleanup/_show.html.haml b/app/views/projects/cleanup/_show.html.haml index cecc139b183..888be4ee282 100644 --- a/app/views/projects/cleanup/_show.html.haml +++ b/app/views/projects/cleanup/_show.html.haml @@ -24,6 +24,6 @@ = _("No file selected") = f.file_field :bfg_object_map, accept: 'text/plain', class: "hidden js-object-map-input", required: true .form-text.text-muted - = _("The maximum file size allowed is %{max_attachment_size}mb") % { max_attachment_size: Gitlab::CurrentSettings.max_attachment_size } + = _("The maximum file size allowed is %{size}.") % { size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes) } = f.submit _('Start cleanup'), class: 'btn btn-success' 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..fd6559e37ba 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) && can?(current_user, :import_issues, @project) +- 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..18768307341 --- /dev/null +++ b/app/views/projects/issues/import_csv/_modal.html.haml @@ -0,0 +1,24 @@ +.issues-import-modal.modal + .modal-dialog + .modal-content + = form_tag import_csv_namespace_project_issues_path, 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,text/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.') + = _('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/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml new file mode 100644 index 00000000000..71335e4dfd0 --- /dev/null +++ b/app/views/projects/settings/operations/_error_tracking.html.haml @@ -0,0 +1,30 @@ +- return unless Feature.enabled?(:error_tracking, @project) && can?(current_user, :read_environment, @project) + +- setting = error_tracking_setting + +%section.settings.expanded.border-0.no-animate + .settings-header + %h4 + = _('Error Tracking') + %p + = _('To link Sentry to GitLab, enter your Sentry URL and Auth Token.') + .settings-content + = form_for @project, url: project_settings_operations_path(@project), method: :patch do |f| + = form_errors(@project) + .form-group + = f.fields_for :error_tracking_setting_attributes, setting do |form| + .form-check.form-group + = form.check_box :enabled, class: 'form-check-input' + = form.label :enabled, _('Active'), class: 'form-check-label' + .form-group + = form.label :api_url, _('Sentry API URL'), class: 'label-bold' + = form.url_field :api_url, class: 'form-control', placeholder: _('http://<sentry-host>/api/0/projects/{organization_slug}/{project_slug}/issues/') + %p.form-text.text-muted + = _('Enter your Sentry API URL') + .form-group + = form.label :token, _('Auth Token'), class: 'label-bold' + = form.text_field :token, class: 'form-control' + %p.form-text.text-muted + = _('Find and manage Auth Tokens in your Sentry account settings page.') + + = f.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml index 0782029dbcd..b36fa9a5f51 100644 --- a/app/views/projects/settings/operations/show.html.haml +++ b/app/views/projects/settings/operations/show.html.haml @@ -1,4 +1,5 @@ - @content_class = 'limit-container-width' unless fluid_layout - page_title _('Operations') += render 'projects/settings/operations/error_tracking', expanded: true = render_if_exists 'projects/settings/operations/tracing' diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index 0ddc56dc6c3..0434860dec4 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) && Feature.enabled?(:issues_import_csv) && can?(current_user, :import_issues, @project) - 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/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml index 0674c822d63..f487c0bf0d5 100644 --- a/app/views/shared/notes/_comment_button.html.haml +++ b/app/views/shared/notes/_comment_button.html.haml @@ -1,30 +1,31 @@ - noteable_name = @note.noteable.human_class_name .float-left.btn-group.append-right-10.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown - %input.btn.btn-nr.btn-success.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: 'Comment' } + %input.btn.btn-nr.btn-success.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: _('Comment') } - if @note.can_be_discussion_note? - = button_tag type: 'button', class: 'btn btn-nr dropdown-toggle comment-btn js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => 'Open comment type dropdown' do + = button_tag type: 'button', class: 'btn btn-nr dropdown-toggle comment-btn js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => _('Open comment type dropdown') do = icon('caret-down', class: 'toggle-icon') %ul#resolvable-comment-menu.dropdown-menu.dropdown-open-top{ data: { dropdown: true } } - %li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => 'Comment', 'close-text' => "Comment & close #{noteable_name}", 'reopen-text' => "Comment & reopen #{noteable_name}" } } + %li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => _('Comment'), 'close-text' => _("Comment & close %{noteable_name}") % { noteable_name: noteable_name }, 'reopen-text' => _("Comment & reopen %{noteable_name}") % { noteable_name: noteable_name } } } %button.btn.btn-transparent = icon('check', class: 'icon') .description - %strong Comment + %strong= _("Comment") %p - Add a general comment to this #{noteable_name}. + = _("Add a general comment to this %{noteable_name}.") % { noteable_name: noteable_name } %li.divider.droplab-item-ignore - %li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => 'Start discussion', 'close-text' => "Start discussion & close #{noteable_name}", 'reopen-text' => "Start discussion & reopen #{noteable_name}" } } + %li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => _('Start discussion'), 'close-text' => _("Start discussion & close %{noteable_name}") % { noteable_name: noteable_name }, 'reopen-text' => _("Start discussion & reopen %{noteable_name}") % { noteable_name: noteable_name } } } %button.btn.btn-transparent = icon('check', class: 'icon') .description - %strong Start discussion + %strong= _("Start discussion") %p = succeed '.' do - Discuss a specific suggestion or question - if @note.noteable.supports_resolvable_notes? - that needs to be resolved + = _('Discuss a specific suggestion or question that needs to be resolved') + - else + = _('Discuss a specific suggestion or question') diff --git a/app/views/shared/notes/_edit.html.haml b/app/views/shared/notes/_edit.html.haml index f4b3aac29b4..84a3ef9d8fe 100644 --- a/app/views/shared/notes/_edit.html.haml +++ b/app/views/shared/notes/_edit.html.haml @@ -1 +1 @@ -%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: note_url(note) } }= note.note +%textarea.hidden.js-task-list-field.original-task-list{ data: { update_url: note_url(note) } }= note.note diff --git a/app/views/shared/notes/_edit_form.html.haml b/app/views/shared/notes/_edit_form.html.haml index fec966069b9..6fe511c2999 100644 --- a/app/views/shared/notes/_edit_form.html.haml +++ b/app/views/shared/notes/_edit_form.html.haml @@ -3,12 +3,12 @@ = hidden_field_tag :target_id, '', class: 'js-form-target-id' = hidden_field_tag :target_type, '', class: 'js-form-target-type' = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(project), referenced_users: true } do - = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here…" + = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: _("Write a comment or drag your files here…") = render 'shared/notes/hints' .note-form-actions.clearfix .settings-message.note-edit-warning.js-finish-edit-warning - Finish editing this message first! - = submit_tag 'Save comment', class: 'btn btn-nr btn-success js-comment-save-button' + = _("Finish editing this message first!") + = submit_tag _('Save comment'), class: 'btn btn-nr btn-success js-comment-save-button' %button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' } - Cancel + = _("Cancel") diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml index 493c6241257..6a1eea85fde 100644 --- a/app/views/shared/notes/_form.html.haml +++ b/app/views/shared/notes/_form.html.haml @@ -29,7 +29,7 @@ = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', - placeholder: "Write a comment or drag your files here…", + placeholder: _("Write a comment or drag your files here…"), supports_quick_actions: supports_quick_actions, supports_autocomplete: supports_autocomplete = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions @@ -40,5 +40,5 @@ = yield(:note_actions) - %a.btn.btn-cancel.js-close-discussion-note-form.hide{ role: "button", data: {cancel_text: "Cancel" } } - Cancel + %a.btn.btn-cancel.js-close-discussion-note-form.hide{ role: "button", data: { cancel_text: _("Cancel") } } + = _('Cancel') diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml index 00eae553279..46f3f8428f1 100644 --- a/app/views/shared/notes/_hints.html.haml +++ b/app/views/shared/notes/_hints.html.haml @@ -1,10 +1,10 @@ - supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false) .comment-toolbar.clearfix .toolbar-text - = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1 + = link_to _('Markdown'), help_page_path('user/markdown'), target: '_blank', tabindex: -1 - if supports_quick_actions and - = link_to 'quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1 + = link_to _('quick actions'), help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1 are - else is @@ -24,12 +24,12 @@ = icon('file-image-o', class: 'toolbar-button-icon') %span.uploading-error-message -# Populated by app/assets/javascripts/dropzone_input.js - %button.retry-uploading-link{ type: 'button' } Try again + %button.retry-uploading-link{ type: 'button' }= _("Try again") or - %button.attach-new-file.markdown-selector{ type: 'button' } attach a new file + %button.attach-new-file.markdown-selector{ type: 'button' }= _("attach a new file") %button.markdown-selector.button-attach-file{ type: 'button', tabindex: '-1' } = icon('file-image-o', class: 'toolbar-button-icon') - Attach a file + = _("Attach a file") - %button.btn.btn-default.btn-sm.hide.button-cancel-uploading-files{ type: 'button' } Cancel + %button.btn.btn-default.btn-sm.hide.button-cancel-uploading-files{ type: 'button' }= _("Cancel") diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml index e125d7f108a..cb5c64fb91d 100644 --- a/app/views/shared/notes/_note.html.haml +++ b/app/views/shared/notes/_note.html.haml @@ -62,7 +62,7 @@ = render 'award_emoji/awards_block', awardable: note, inline: false - if note.system .system-note-commit-list-toggler.hide - Toggle commit list + = _("Toggle commit list") %i.fa.fa-angle-down - if note.attachment.url .note-attachment @@ -74,5 +74,5 @@ = icon('paperclip') = note.attachment_identifier = link_to delete_attachment_project_note_path(note.project, note), - title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do + title: _('Delete this attachment'), method: :delete, remote: true, data: { confirm: _('Are you sure you want to remove the attachment?') }, class: 'danger js-note-attachment-delete' do = icon('trash-o', class: 'cred') diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml index 4c4050c6054..002189e6ecd 100644 --- a/app/views/shared/notes/_notes_with_form.html.haml +++ b/app/views/shared/notes/_notes_with_form.html.haml @@ -19,20 +19,14 @@ = render "shared/notes/form", view: diff_view, supports_autocomplete: autocomplete - elsif !current_user .disabled-comment.text-center.prepend-top-default - Please - = link_to "register", new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), class: 'js-register-link' - or - = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link' - to comment + - link_to_register = link_to(_("register"), new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), class: 'js-register-link') + - link_to_sign_in = link_to(_("sign in"), new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link') + = _("Please %{link_to_register} or %{link_to_sign_in} to comment").html_safe % { link_to_register: link_to_register, link_to_sign_in: link_to_sign_in } - elsif discussion_locked .disabled-comment.text-center.prepend-top-default %span.issuable-note-warning = sprite_icon('lock', size: 16, css_class: 'icon') %span - This - = issuable.class.to_s.titleize.downcase - is locked. Only - %b project members - can comment. + = _("This %{issuable} is locked. Only <strong>project members</strong> can comment.").html_safe % { issuable: issuable.class.to_s.titleize.downcase } -# haml-lint:disable InlineJavaScript %script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index d3cf21db335..223ddc80c88 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -140,3 +140,4 @@ - detect_repository_languages - repository_cleanup - delete_stored_files +- import_issues_csv diff --git a/app/workers/import_issues_csv_worker.rb b/app/workers/import_issues_csv_worker.rb new file mode 100644 index 00000000000..d44fdfec8ae --- /dev/null +++ b/app/workers/import_issues_csv_worker.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class ImportIssuesCsvWorker + include ApplicationWorker + + sidekiq_retries_exhausted do |job| + Upload.find(job['args'][2]).destroy + end + + def perform(current_user_id, project_id, upload_id) + @user = User.find(current_user_id) + @project = Project.find(project_id) + @upload = Upload.find(upload_id) + + importer = Issues::ImportCsvService.new(@user, @project, @upload.build_uploader) + importer.execute + + @upload.destroy + end +end |