Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2019-11-12 15:06:26 +00:00
parent 1b7381e998
commit 69944ffb68
59 changed files with 846 additions and 401 deletions

View file

@ -1,5 +0,0 @@
// capture anything starting with http:// or https://
// up until a disallowed character or whitespace
export const blobLinkRegex = /https?:\/\/[^"<>\\^`{|}\s]+/g;
export default { blobLinkRegex };

View file

@ -4,10 +4,6 @@ import Flash from '../../flash';
import { handleLocationHash } from '../../lib/utils/common_utils'; import { handleLocationHash } from '../../lib/utils/common_utils';
import axios from '../../lib/utils/axios_utils'; import axios from '../../lib/utils/axios_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { blobLinkRegex } from '~/blob/blob_utils';
const SIMPLE_VIEWER_NAME = 'simple';
const RICH_VIEWER_NAME = 'rich';
export default class BlobViewer { export default class BlobViewer {
constructor() { constructor() {
@ -25,7 +21,7 @@ export default class BlobViewer {
} }
static initRichViewer() { static initRichViewer() {
const viewer = document.querySelector(`.blob-viewer[data-type="${RICH_VIEWER_NAME}"]`); const viewer = document.querySelector('.blob-viewer[data-type="rich"]');
if (!viewer || !viewer.dataset.richType) return; if (!viewer || !viewer.dataset.richType) return;
const initViewer = promise => const initViewer = promise =>
@ -65,12 +61,8 @@ export default class BlobViewer {
this.switcherBtns = document.querySelectorAll('.js-blob-viewer-switch-btn'); this.switcherBtns = document.querySelectorAll('.js-blob-viewer-switch-btn');
this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn'); this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn');
this.simpleViewer = this.$fileHolder[0].querySelector( this.simpleViewer = this.$fileHolder[0].querySelector('.blob-viewer[data-type="simple"]');
`.blob-viewer[data-type="${SIMPLE_VIEWER_NAME}"]`, this.richViewer = this.$fileHolder[0].querySelector('.blob-viewer[data-type="rich"]');
);
this.richViewer = this.$fileHolder[0].querySelector(
`.blob-viewer[data-type="${RICH_VIEWER_NAME}"]`,
);
this.initBindings(); this.initBindings();
@ -79,10 +71,10 @@ export default class BlobViewer {
switchToInitialViewer() { switchToInitialViewer() {
const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)'); const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)');
let initialViewerName = initialViewer.dataset.type; let initialViewerName = initialViewer.getAttribute('data-type');
if (this.switcher && window.location.hash.indexOf('#L') === 0) { if (this.switcher && window.location.hash.indexOf('#L') === 0) {
initialViewerName = SIMPLE_VIEWER_NAME; initialViewerName = 'simple';
} }
this.switchToViewer(initialViewerName); this.switchToViewer(initialViewerName);
@ -99,41 +91,35 @@ export default class BlobViewer {
this.copySourceBtn.addEventListener('click', () => { this.copySourceBtn.addEventListener('click', () => {
if (this.copySourceBtn.classList.contains('disabled')) return this.copySourceBtn.blur(); if (this.copySourceBtn.classList.contains('disabled')) return this.copySourceBtn.blur();
return this.switchToViewer(SIMPLE_VIEWER_NAME); return this.switchToViewer('simple');
}); });
} }
} }
static linkifyURLs(viewer) {
if (viewer.dataset.linkified) return;
document.querySelectorAll('.js-blob-content .code .line').forEach(line => {
// eslint-disable-next-line no-param-reassign
line.innerHTML = line.innerHTML.replace(blobLinkRegex, '<a href="$&">$&</a>');
});
// eslint-disable-next-line no-param-reassign
viewer.dataset.linkified = true;
}
switchViewHandler(e) { switchViewHandler(e) {
const target = e.currentTarget; const target = e.currentTarget;
e.preventDefault(); e.preventDefault();
this.switchToViewer(target.dataset.viewer); this.switchToViewer(target.getAttribute('data-viewer'));
} }
toggleCopyButtonState() { toggleCopyButtonState() {
if (!this.copySourceBtn) return; if (!this.copySourceBtn) return;
if (this.simpleViewer.dataset.loaded) { if (this.simpleViewer.getAttribute('data-loaded')) {
this.copySourceBtn.dataset.title = __('Copy file contents'); this.copySourceBtn.setAttribute('title', __('Copy file contents'));
this.copySourceBtn.classList.remove('disabled'); this.copySourceBtn.classList.remove('disabled');
} else if (this.activeViewer === this.simpleViewer) { } else if (this.activeViewer === this.simpleViewer) {
this.copySourceBtn.dataset.title = __('Wait for the file to load to copy its contents'); this.copySourceBtn.setAttribute(
'title',
__('Wait for the file to load to copy its contents'),
);
this.copySourceBtn.classList.add('disabled'); this.copySourceBtn.classList.add('disabled');
} else { } else {
this.copySourceBtn.dataset.title = __('Switch to the source to copy the file contents'); this.copySourceBtn.setAttribute(
'title',
__('Switch to the source to copy the file contents'),
);
this.copySourceBtn.classList.add('disabled'); this.copySourceBtn.classList.add('disabled');
} }
@ -173,8 +159,6 @@ export default class BlobViewer {
this.$fileHolder.trigger('highlight:line'); this.$fileHolder.trigger('highlight:line');
handleLocationHash(); handleLocationHash();
if (name === SIMPLE_VIEWER_NAME) BlobViewer.linkifyURLs(viewer);
this.toggleCopyButtonState(); this.toggleCopyButtonState();
}) })
.catch(() => new Flash(__('Error loading viewer'))); .catch(() => new Flash(__('Error loading viewer')));
@ -182,17 +166,17 @@ export default class BlobViewer {
static loadViewer(viewerParam) { static loadViewer(viewerParam) {
const viewer = viewerParam; const viewer = viewerParam;
const { url, loaded, loading } = viewer.dataset; const url = viewer.getAttribute('data-url');
if (!url || loaded || loading) { if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
return Promise.resolve(viewer); return Promise.resolve(viewer);
} }
viewer.dataset.loading = true; viewer.setAttribute('data-loading', 'true');
return axios.get(url).then(({ data }) => { return axios.get(url).then(({ data }) => {
viewer.innerHTML = data.html; viewer.innerHTML = data.html;
viewer.dataset.loaded = true; viewer.setAttribute('data-loaded', 'true');
return viewer; return viewer;
}); });

View file

@ -4,8 +4,7 @@ import $ from 'jquery';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { blobLinkRegex } from '~/blob/blob_utils'; import TemplateSelectorMediator from '../blob/file_template_mediator';
import TemplateSelectorMediator from '~/blob/file_template_mediator';
import getModeByFileExtension from '~/lib/utils/ace_utils'; import getModeByFileExtension from '~/lib/utils/ace_utils';
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown'; import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
@ -18,7 +17,6 @@ export default class EditBlob {
this.initModePanesAndLinks(); this.initModePanesAndLinks();
this.initSoftWrap(); this.initSoftWrap();
this.initFileSelectors(); this.initFileSelectors();
this.initBlobContentLinkClickability();
} }
configureAceEditor() { configureAceEditor() {
@ -91,22 +89,6 @@ export default class EditBlob {
return this.editor.focus(); return this.editor.focus();
} }
initBlobContentLinkClickability() {
this.editor.renderer.on('afterRender', () => {
document.querySelectorAll('.ace_text-layer .ace_line > *').forEach(token => {
if (token.dataset.linkified || !token.textContent.includes('http')) return;
// eslint-disable-next-line no-param-reassign
token.innerHTML = token.innerHTML.replace(
blobLinkRegex,
'<a target="_blank" href="$&">$&</a>',
);
// eslint-disable-next-line no-param-reassign
token.dataset.linkified = true;
});
});
}
initSoftWrap() { initSoftWrap() {
this.isSoftWrapped = false; this.isSoftWrapped = false;
this.$toggleButton = $('.soft-wrap-toggle'); this.$toggleButton = $('.soft-wrap-toggle');

View file

@ -84,7 +84,8 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
if ( if (
this.scrollHeight() <= this.listHeight() && this.scrollHeight() <= this.listHeight() &&
this.list.issuesSize > this.list.issues.length this.list.issuesSize > this.list.issues.length &&
this.list.isExpanded
) { ) {
this.list.page += 1; this.list.page += 1;
this.list.getIssues(false).catch(() => { this.list.getIssues(false).catch(() => {

View file

@ -50,8 +50,8 @@ class List {
this.page = 1; this.page = 1;
this.loading = true; this.loading = true;
this.loadingMore = false; this.loadingMore = false;
this.issues = []; this.issues = obj.issues || [];
this.issuesSize = 0; this.issuesSize = obj.issuesSize ? obj.issuesSize : 0;
this.defaultAvatar = defaultAvatar; this.defaultAvatar = defaultAvatar;
if (obj.label) { if (obj.label) {

View file

@ -258,17 +258,6 @@
} }
} }
} }
.file-editor {
.ace_underline {
text-decoration: none;
}
.ace_line a {
pointer-events: auto;
color: inherit;
}
}
} }
span.idiff { span.idiff {

View file

@ -29,12 +29,3 @@
color: $link; color: $link;
} }
} }
// Links to URLs, emails, or dependencies
.code .line a {
color: inherit;
&:hover {
text-decoration: underline;
}
}

View file

@ -193,6 +193,11 @@ $dark-il: #de935f;
color: $dark-highlight-color !important; color: $dark-highlight-color !important;
} }
// Links to URLs, emails, or dependencies
.line a {
color: $dark-na;
}
.hll { background-color: $dark-hll-bg; } .hll { background-color: $dark-hll-bg; }
.c { color: $dark-c; } /* Comment */ .c { color: $dark-c; } /* Comment */
.err { color: $dark-err; } /* Error */ .err { color: $dark-err; } /* Error */

View file

@ -193,6 +193,11 @@ $monokai-gi: #a6e22e;
color: $black !important; color: $black !important;
} }
// Links to URLs, emails, or dependencies
.line a {
color: $monokai-k;
}
.hll { background-color: $monokai-hll; } .hll { background-color: $monokai-hll; }
.c { color: $monokai-c; } /* Comment */ .c { color: $monokai-c; } /* Comment */
.err { color: $monokai-err-color; background-color: $monokai-err-bg; } /* Error */ .err { color: $monokai-err-color; background-color: $monokai-err-bg; } /* Error */

View file

@ -143,6 +143,12 @@
background-color: $white-normal; background-color: $white-normal;
} }
// Links to URLs, emails, or dependencies
.line a {
color: $gl-text-color;
text-decoration: underline;
}
.hll { background-color: $white-light; } .hll { background-color: $white-light; }
.gd { .gd {

View file

@ -196,6 +196,11 @@ $solarized-dark-il: #2aa198;
background-color: $solarized-dark-highlight !important; background-color: $solarized-dark-highlight !important;
} }
// Links to URLs, emails, or dependencies
.line a {
color: $solarized-dark-kd;
}
/* Solarized Dark /* Solarized Dark
For use with Jekyll and Pygments For use with Jekyll and Pygments

View file

@ -204,6 +204,11 @@ $solarized-light-il: #2aa198;
background-color: $solarized-light-highlight !important; background-color: $solarized-light-highlight !important;
} }
// Links to URLs, emails, or dependencies
.line a {
color: $solarized-light-kd;
}
/* Solarized Light /* Solarized Light
For use with Jekyll and Pygments For use with Jekyll and Pygments

View file

@ -209,6 +209,11 @@ span.highlight_word {
background-color: $white-highlight !important; background-color: $white-highlight !important;
} }
// Links to URLs, emails, or dependencies
.line a {
color: $white-nb;
}
.hll { background-color: $white-hll-bg; } .hll { background-color: $white-hll-bg; }
.c { color: $white-c; .c { color: $white-c;

View file

@ -20,11 +20,11 @@ class ApplicationController < ActionController::Base
before_action :authenticate_user!, except: [:route_not_found] before_action :authenticate_user!, except: [:route_not_found]
before_action :enforce_terms!, if: :should_enforce_terms? before_action :enforce_terms!, if: :should_enforce_terms?
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
before_action :check_password_expiration, if: :html_request? before_action :check_password_expiration
before_action :ldap_security_check before_action :ldap_security_check
before_action :sentry_context before_action :sentry_context
before_action :default_headers before_action :default_headers
before_action :add_gon_variables, if: :html_request? before_action :add_gon_variables, unless: [:peek_request?, :json_request?]
before_action :configure_permitted_parameters, if: :devise_controller? before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller? before_action :require_email, unless: :devise_controller?
before_action :active_user_check, unless: :devise_controller? before_action :active_user_check, unless: :devise_controller?
@ -455,8 +455,8 @@ class ApplicationController < ActionController::Base
response.headers['Page-Title'] = URI.escape(page_title('GitLab')) response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
end end
def html_request? def peek_request?
request.format.html? request.path.start_with?('/-/peek')
end end
def json_request? def json_request?
@ -466,7 +466,7 @@ class ApplicationController < ActionController::Base
def should_enforce_terms? def should_enforce_terms?
return false unless Gitlab::CurrentSettings.current_application_settings.enforce_terms return false unless Gitlab::CurrentSettings.current_application_settings.enforce_terms
html_request? && !devise_controller? !(peek_request? || devise_controller?)
end end
def set_usage_stats_consent_flag def set_usage_stats_consent_flag

View file

@ -4,18 +4,15 @@ module ConfirmEmailWarning
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
before_action :set_confirm_warning, if: :show_confirm_warning? before_action :set_confirm_warning, if: -> { Feature.enabled?(:soft_email_confirmation) }
end end
protected protected
def show_confirm_warning?
html_request? && request.get? && Feature.enabled?(:soft_email_confirmation)
end
def set_confirm_warning def set_confirm_warning
return unless current_user return unless current_user
return if current_user.confirmed? return if current_user.confirmed?
return if peek_request? || json_request? || !request.get?
email = current_user.unconfirmed_email || current_user.email email = current_user.unconfirmed_email || current_user.email

View file

@ -1,16 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
module UploadsActions module UploadsActions
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include SendFileUpload include SendFileUpload
UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze
included do
prepend_before_action :set_request_format_from_path_extension
end
def create def create
uploader = UploadService.new(model, params[:file], uploader_class).execute uploader = UploadService.new(model, params[:file], uploader_class).execute
@ -69,18 +64,6 @@ module UploadsActions
private private
# From ActionDispatch::Http::MimeNegotiation. We have an initializer that
# monkey-patches this method out (so that repository paths don't guess a
# format based on extension), but we do want this behaviour when serving
# uploads.
def set_request_format_from_path_extension
path = request.headers['action_dispatch.original_path'] || request.headers['PATH_INFO']
if match = path&.match(/\.(\w+)\z/)
request.format = match.captures.first
end
end
def uploader_class def uploader_class
raise NotImplementedError raise NotImplementedError
end end

View file

@ -20,7 +20,7 @@ class UploadsController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
before_action :upload_mount_satisfied? before_action :upload_mount_satisfied?
before_action :model before_action :find_model
before_action :authorize_access!, only: [:show] before_action :authorize_access!, only: [:show]
before_action :authorize_create_access!, only: [:create, :authorize] before_action :authorize_create_access!, only: [:create, :authorize]
before_action :verify_workhorse_api!, only: [:authorize] before_action :verify_workhorse_api!, only: [:authorize]

View file

@ -80,7 +80,7 @@ class GroupDescendantsFinder
if current_user if current_user
authorized_groups = GroupsFinder.new(current_user, authorized_groups = GroupsFinder.new(current_user,
all_available: false) all_available: false)
.execute.as('authorized') .execute.arel.as('authorized')
authorized_to_user = groups_table.project(1).from(authorized_groups) authorized_to_user = groups_table.project(1).from(authorized_groups)
.where(authorized_groups[:id].eq(groups_table[:id])) .where(authorized_groups[:id].eq(groups_table[:id]))
.exists .exists

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Mutations
module Todos
class Base < ::Mutations::BaseMutation
private
def find_object(id:)
GitlabSchema.object_from_id(id)
end
def to_global_id(id)
::URI::GID.build(app: GlobalID.app, model_name: Todo.name, model_id: id, params: nil).to_s
end
end
end
end

View file

@ -0,0 +1,38 @@
# frozen_string_literal: true
module Mutations
module Todos
class MarkDone < ::Mutations::Todos::Base
graphql_name 'TodoMarkDone'
authorize :update_todo
argument :id,
GraphQL::ID_TYPE,
required: true,
description: 'The global id of the todo to mark as done'
field :todo, Types::TodoType,
null: false,
description: 'The requested todo'
# rubocop: disable CodeReuse/ActiveRecord
def resolve(id:)
todo = authorized_find!(id: id)
mark_done(Todo.where(id: todo.id)) unless todo.done?
{
todo: todo.reset,
errors: errors_on_object(todo)
}
end
# rubocop: enable CodeReuse/ActiveRecord
private
def mark_done(todo)
TodoService.new.mark_todos_as_done(todo, current_user)
end
end
end
end

View file

@ -16,6 +16,7 @@ module Types
mount_mutation Mutations::Notes::Create::ImageDiffNote, calls_gitaly: true mount_mutation Mutations::Notes::Create::ImageDiffNote, calls_gitaly: true
mount_mutation Mutations::Notes::Update mount_mutation Mutations::Notes::Update
mount_mutation Mutations::Notes::Destroy mount_mutation Mutations::Notes::Destroy
mount_mutation Mutations::Todos::MarkDone
end end
end end

View file

@ -405,7 +405,7 @@ module Ci
.where('stage=sg.stage').failed_but_allowed.to_sql .where('stage=sg.stage').failed_but_allowed.to_sql
stages_with_statuses = CommitStatus.from(stages_query, :sg) stages_with_statuses = CommitStatus.from(stages_query, :sg)
.pluck('sg.stage', status_sql, "(#{warnings_sql})") .pluck('sg.stage', Arel.sql(status_sql), Arel.sql("(#{warnings_sql})"))
stages_with_statuses.map do |stage| stages_with_statuses.map do |stage|
Ci::LegacyStage.new(self, Hash[%i[name status warnings].zip(stage)]) Ci::LegacyStage.new(self, Hash[%i[name status warnings].zip(stage)])

View file

@ -118,8 +118,8 @@ module Issuable
# rubocop:enable GitlabSecurity/SqlInjection # rubocop:enable GitlabSecurity/SqlInjection
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") } scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC') } scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
scope :order_milestone_due_asc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC') } scope :order_milestone_due_asc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC')) }
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :any_label, -> { joins(:label_links).group(:id) } scope :any_label, -> { joins(:label_links).group(:id) }

View file

@ -126,7 +126,7 @@ class Group < Namespace
def visible_to_user_arel(user) def visible_to_user_arel(user)
groups_table = self.arel_table groups_table = self.arel_table
authorized_groups = user.authorized_groups.as('authorized') authorized_groups = user.authorized_groups.arel.as('authorized')
groups_table.project(1) groups_table.project(1)
.from(authorized_groups) .from(authorized_groups)

View file

@ -796,6 +796,8 @@ class MergeRequest < ApplicationRecord
end end
def check_mergeability def check_mergeability
return if Feature.enabled?(:merge_requests_conditional_mergeability_check, default_enabled: true) && !recheck_merge_status?
MergeRequests::MergeabilityCheckService.new(self).execute(retry_lease: false) MergeRequests::MergeabilityCheckService.new(self).execute(retry_lease: false)
end end
# rubocop: enable CodeReuse/ServiceClass # rubocop: enable CodeReuse/ServiceClass

View file

@ -1918,7 +1918,7 @@ class Project < ApplicationRecord
end end
def default_environment def default_environment
production_first = "(CASE WHEN name = 'production' THEN 0 ELSE 1 END), id ASC" production_first = Arel.sql("(CASE WHEN name = 'production' THEN 0 ELSE 1 END), id ASC")
environments environments
.with_state(:available) .with_state(:available)

View file

@ -160,6 +160,10 @@ class Todo < ApplicationRecord
action == ASSIGNED action == ASSIGNED
end end
def done?
state == 'done'
end
def action_name def action_name
ACTION_NAMES[action] ACTION_NAMES[action]
end end

View file

@ -7,4 +7,5 @@ class TodoPolicy < BasePolicy
end end
rule { own_todo }.enable :read_todo rule { own_todo }.enable :read_todo
rule { own_todo }.enable :update_todo
end end

View file

@ -149,7 +149,7 @@ module Ci
# this returns builds that are ordered by number of running builds # this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all # we prefer projects that don't use shared runners at all
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id") joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id")
.order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') .order(Arel.sql('COALESCE(project_builds.running_builds, 0) ASC'), 'ci_builds.id ASC')
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord

View file

@ -88,7 +88,7 @@ class CohortsService
User User
.where('created_at > ?', MONTHS_INCLUDED.months.ago.end_of_month) .where('created_at > ?', MONTHS_INCLUDED.months.ago.end_of_month)
.group(created_at_month, last_activity_on_month) .group(created_at_month, last_activity_on_month)
.reorder("#{created_at_month} ASC", "#{last_activity_on_month} ASC") .reorder(Arel.sql("#{created_at_month} ASC, #{last_activity_on_month} ASC"))
.count .count
end end
end end

View file

@ -10,7 +10,7 @@
%a.diff-line-num{ href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i } %a.diff-line-num{ href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i }
= link_icon = link_icon
= i = i
.blob-content.js-blob-content{ data: { blob_id: blob.id } } .blob-content{ data: { blob_id: blob.id } }
%pre.code.highlight %pre.code.highlight
%code %code
= blob.present.highlight = blob.present.highlight

View file

@ -0,0 +1,5 @@
---
title: Fix closed board list loading issue
merge_request:
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Mark todo done by GraphQL API
merge_request: 18581
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Run check_mergeability only if merge status requires it
merge_request: 19364
author:
type: performance

View file

@ -1,5 +0,0 @@
---
title: Make URLs in blob viewer and blob editor into clickable links
merge_request: 18305
author:
type: added

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddCachedMarkdownVersionToVulnerabilities < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
add_column :vulnerabilities, :cached_markdown_version, :integer
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class ChangeVulnerabilitiesTitleHtmlToNullable < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
change_column_null :vulnerabilities, :title_html, true
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_11_11_115431) do ActiveRecord::Schema.define(version: 2019_11_12_115317) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm" enable_extension "pg_trgm"
@ -3928,7 +3928,7 @@ ActiveRecord::Schema.define(version: 2019_11_11_115431) do
t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime_with_timezone "updated_at", null: false
t.string "title", limit: 255, null: false t.string "title", limit: 255, null: false
t.text "title_html", null: false t.text "title_html"
t.text "description" t.text "description"
t.text "description_html" t.text "description_html"
t.bigint "start_date_sourcing_milestone_id" t.bigint "start_date_sourcing_milestone_id"
@ -3941,6 +3941,7 @@ ActiveRecord::Schema.define(version: 2019_11_11_115431) do
t.integer "confidence", limit: 2, null: false t.integer "confidence", limit: 2, null: false
t.boolean "confidence_overridden", default: false t.boolean "confidence_overridden", default: false
t.integer "report_type", limit: 2, null: false t.integer "report_type", limit: 2, null: false
t.integer "cached_markdown_version"
t.index ["author_id"], name: "index_vulnerabilities_on_author_id" t.index ["author_id"], name: "index_vulnerabilities_on_author_id"
t.index ["closed_by_id"], name: "index_vulnerabilities_on_closed_by_id" t.index ["closed_by_id"], name: "index_vulnerabilities_on_closed_by_id"
t.index ["due_date_sourcing_milestone_id"], name: "index_vulnerabilities_on_due_date_sourcing_milestone_id" t.index ["due_date_sourcing_milestone_id"], name: "index_vulnerabilities_on_due_date_sourcing_milestone_id"

View file

@ -3413,6 +3413,7 @@ type Mutation {
mergeRequestSetMilestone(input: MergeRequestSetMilestoneInput!): MergeRequestSetMilestonePayload mergeRequestSetMilestone(input: MergeRequestSetMilestoneInput!): MergeRequestSetMilestonePayload
mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload
todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload
toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload
updateEpic(input: UpdateEpicInput!): UpdateEpicPayload updateEpic(input: UpdateEpicInput!): UpdateEpicPayload
updateNote(input: UpdateNoteInput!): UpdateNotePayload updateNote(input: UpdateNoteInput!): UpdateNotePayload
@ -4826,6 +4827,41 @@ type TodoEdge {
node: Todo node: Todo
} }
"""
Autogenerated input type of TodoMarkDone
"""
input TodoMarkDoneInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The global id of the todo to mark as done
"""
id: ID!
}
"""
Autogenerated return type of TodoMarkDone
"""
type TodoMarkDonePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The requested todo
"""
todo: Todo!
}
enum TodoStateEnum { enum TodoStateEnum {
done done
pending pending

View file

@ -14557,6 +14557,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "todoMarkDone",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "TodoMarkDoneInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "TodoMarkDonePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "toggleAwardEmoji", "name": "toggleAwardEmoji",
"description": null, "description": null,
@ -16230,6 +16257,112 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "TodoMarkDonePayload",
"description": "Autogenerated return type of TodoMarkDone",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Reasons why the mutation failed.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "todo",
"description": "The requested todo",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Todo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "TodoMarkDoneInput",
"description": "Autogenerated input type of TodoMarkDone",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "The global id of the todo to mark as done",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "DesignManagementUploadPayload", "name": "DesignManagementUploadPayload",

View file

@ -756,6 +756,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `state` | TodoStateEnum! | State of the todo | | `state` | TodoStateEnum! | State of the todo |
| `createdAt` | Time! | Timestamp this todo was created | | `createdAt` | Time! | Timestamp this todo was created |
### TodoMarkDonePayload
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `todo` | Todo! | The requested todo |
### ToggleAwardEmojiPayload ### ToggleAwardEmojiPayload
| Name | Type | Description | | Name | Type | Description |

View file

@ -176,7 +176,7 @@ module Gitlab
self.table_name = 'projects' self.table_name = 'projects'
def self.find_by_full_path(path) def self.find_by_full_path(path)
order_sql = "(CASE WHEN routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)" order_sql = Arel.sql("(CASE WHEN routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)")
where_full_path_in(path).reorder(order_sql).take where_full_path_in(path).reorder(order_sql).take
end end

View file

@ -28,7 +28,7 @@ dast_environment_deploy:
variables: variables:
- $CI_DEFAULT_BRANCH != $CI_COMMIT_REF_NAME - $CI_DEFAULT_BRANCH != $CI_COMMIT_REF_NAME
- $DAST_DISABLED || $DAST_DISABLED_FOR_DEFAULT_BRANCH - $DAST_DISABLED || $DAST_DISABLED_FOR_DEFAULT_BRANCH
- $DAST_WEBSITE # we don't need to create a review app if a URL is already given - $DAST_WEBSITE # we don't need to create a review app if a URL is already given
stop_dast_environment: stop_dast_environment:
extends: .auto-deploy extends: .auto-deploy

View file

@ -955,7 +955,7 @@ into similar problems in the future (e.g. when new tables are created).
table_name = model_class.quoted_table_name table_name = model_class.quoted_table_name
model_class.each_batch(of: batch_size) do |relation| model_class.each_batch(of: batch_size) do |relation|
start_id, end_id = relation.pluck("MIN(#{table_name}.id), MAX(#{table_name}.id)").first start_id, end_id = relation.pluck("MIN(#{table_name}.id)", "MAX(#{table_name}.id)").first
if jobs.length >= BACKGROUND_MIGRATION_JOB_BUFFER_SIZE if jobs.length >= BACKGROUND_MIGRATION_JOB_BUFFER_SIZE
# Note: This code path generally only helps with many millions of rows # Note: This code path generally only helps with many millions of rows

View file

@ -20349,6 +20349,9 @@ msgstr ""
msgid "failed to dismiss associated finding(id=%{finding_id}): %{message}" msgid "failed to dismiss associated finding(id=%{finding_id}): %{message}"
msgstr "" msgstr ""
msgid "finding is not found or is already attached to a vulnerability"
msgstr ""
msgid "for %{link_to_merge_request} with %{link_to_merge_request_source_branch}" msgid "for %{link_to_merge_request} with %{link_to_merge_request_source_branch}"
msgstr "" msgstr ""

View file

@ -90,6 +90,14 @@ describe ApplicationController do
let(:format) { :html } let(:format) { :html }
it_behaves_like 'setting gon variables' it_behaves_like 'setting gon variables'
context 'for peek requests' do
before do
request.path = '/-/peek'
end
it_behaves_like 'not setting gon variables'
end
end end
context 'with json format' do context 'with json format' do
@ -97,12 +105,6 @@ describe ApplicationController do
it_behaves_like 'not setting gon variables' it_behaves_like 'not setting gon variables'
end end
context 'with atom format' do
let(:format) { :atom }
it_behaves_like 'not setting gon variables'
end
end end
describe 'session expiration' do describe 'session expiration' do

View file

@ -228,10 +228,10 @@ describe UploadsController do
user.block user.block
end end
it "responds with status 401" do it "redirects to the sign in page" do
get :show, params: { model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png" } get :show, params: { model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png" }
expect(response).to have_gitlab_http_status(401) expect(response).to redirect_to(new_user_session_path)
end end
end end
@ -320,10 +320,10 @@ describe UploadsController do
end end
context "when not signed in" do context "when not signed in" do
it "responds with status 401" do it "redirects to the sign in page" do
get :show, params: { model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" } get :show, params: { model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" }
expect(response).to have_gitlab_http_status(401) expect(response).to redirect_to(new_user_session_path)
end end
end end
@ -343,10 +343,10 @@ describe UploadsController do
project.add_maintainer(user) project.add_maintainer(user)
end end
it "responds with status 401" do it "redirects to the sign in page" do
get :show, params: { model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" } get :show, params: { model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" }
expect(response).to have_gitlab_http_status(401) expect(response).to redirect_to(new_user_session_path)
end end
end end
@ -439,10 +439,10 @@ describe UploadsController do
user.block user.block
end end
it "responds with status 401" do it "redirects to the sign in page" do
get :show, params: { model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png" } get :show, params: { model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png" }
expect(response).to have_gitlab_http_status(401) expect(response).to redirect_to(new_user_session_path)
end end
end end
@ -526,10 +526,10 @@ describe UploadsController do
end end
context "when not signed in" do context "when not signed in" do
it "responds with status 401" do it "redirects to the sign in page" do
get :show, params: { model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" } get :show, params: { model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" }
expect(response).to have_gitlab_http_status(401) expect(response).to redirect_to(new_user_session_path)
end end
end end
@ -549,10 +549,10 @@ describe UploadsController do
project.add_maintainer(user) project.add_maintainer(user)
end end
it "responds with status 401" do it "redirects to the sign in page" do
get :show, params: { model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" } get :show, params: { model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" }
expect(response).to have_gitlab_http_status(401) expect(response).to redirect_to(new_user_session_path)
end end
end end

View file

@ -62,13 +62,6 @@ describe 'Editing file blob', :js do
expect(page).to have_content 'NextFeature' expect(page).to have_content 'NextFeature'
end end
it 'renders a URL in the content of file as a link' do
project.repository.create_file(user, 'file.yml', '# go to https://gitlab.com', message: 'testing', branch_name: branch)
visit project_edit_blob_path(project, tree_join(branch, 'file.yml'))
expect(page).to have_selector('.ace_content .ace_line a')
end
context 'from blob file path' do context 'from blob file path' do
before do before do
visit project_blob_path(project, tree_join(branch, file_path)) visit project_blob_path(project, tree_join(branch, file_path))

View file

@ -0,0 +1,66 @@
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::Todos::MarkDone do
let_it_be(:current_user) { create(:user) }
let_it_be(:author) { create(:user) }
let_it_be(:other_user) { create(:user) }
let_it_be(:todo1) { create(:todo, user: current_user, author: author, state: :pending) }
let_it_be(:todo2) { create(:todo, user: current_user, author: author, state: :done) }
let_it_be(:other_user_todo) { create(:todo, user: other_user, author: author, state: :pending) }
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }) }
describe '#resolve' do
it 'marks a single todo as done' do
result = mark_done_mutation(todo1)
expect(todo1.reload.state).to eq('done')
expect(todo2.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
todo = result[:todo]
expect(todo.id).to eq(todo1.id)
expect(todo.state).to eq('done')
end
it 'handles a todo which is already done as expected' do
result = mark_done_mutation(todo2)
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
todo = result[:todo]
expect(todo.id).to eq(todo2.id)
expect(todo.state).to eq('done')
end
it 'ignores requests for todos which do not belong to the current user' do
expect { mark_done_mutation(other_user_todo) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
end
it 'ignores invalid GIDs' do
expect { mutation.resolve(id: 'invalid_gid') }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
end
end
def mark_done_mutation(todo)
mutation.resolve(id: global_id_of(todo))
end
def global_id_of(todo)
todo.to_global_id.to_s
end
end

View file

@ -11,13 +11,6 @@ describe('Blob viewer', () => {
preloadFixtures('snippets/show.html'); preloadFixtures('snippets/show.html');
const asyncClick = () =>
new Promise(resolve => {
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
setTimeout(resolve);
});
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
@ -73,12 +66,19 @@ describe('Blob viewer', () => {
}); });
it('doesnt reload file if already loaded', done => { it('doesnt reload file if already loaded', done => {
const asyncClick = () =>
new Promise(resolve => {
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
setTimeout(resolve);
});
asyncClick() asyncClick()
.then(() => asyncClick()) .then(() => asyncClick())
.then(() => { .then(() => {
expect(document.querySelector('.blob-viewer[data-type="simple"]').dataset.loaded).toBe( expect(
'true', document.querySelector('.blob-viewer[data-type="simple"]').getAttribute('data-loaded'),
); ).toBe('true');
done(); done();
}) })
@ -100,7 +100,9 @@ describe('Blob viewer', () => {
}); });
it('has tooltip when disabled', () => { it('has tooltip when disabled', () => {
expect(copyButton.dataset.title).toBe('Switch to the source to copy the file contents'); expect(copyButton.getAttribute('data-original-title')).toBe(
'Switch to the source to copy the file contents',
);
}); });
it('is blurred when clicked and disabled', () => { it('is blurred when clicked and disabled', () => {
@ -134,7 +136,7 @@ describe('Blob viewer', () => {
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click(); document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
setTimeout(() => { setTimeout(() => {
expect(copyButton.dataset.title).toBe('Copy file contents'); expect(copyButton.getAttribute('data-original-title')).toBe('Copy file contents');
done(); done();
}); });
@ -175,27 +177,4 @@ describe('Blob viewer', () => {
expect(axios.get.calls.count()).toBe(1); expect(axios.get.calls.count()).toBe(1);
}); });
}); });
describe('a URL inside the blob content', () => {
beforeEach(() => {
mock.onGet('http://test.host/snippets/1.json?viewer=simple').reply(200, {
html:
'<div class="js-blob-content"><pre class="code"><code><span class="line" lang="yaml"><span class="c1">To install gitlab-shell you also need a Go compiler version 1.8 or newer. https://golang.org/dl/</span></span></code></pre></div>',
});
});
it('is rendered as a link in simple view', done => {
asyncClick()
.then(() => {
expect(document.querySelector('.blob-viewer[data-type="simple"]').innerHTML).toContain(
'<a href="https://golang.org/dl/">https://golang.org/dl/</a>',
);
done();
})
.catch(() => {
fail();
done();
});
});
});
}); });

View file

@ -15,7 +15,12 @@ import boardsStore from '~/boards/stores/boards_store';
window.Sortable = Sortable; window.Sortable = Sortable;
export default function createComponent({ done, listIssueProps = {}, componentProps = {} }) { export default function createComponent({
done,
listIssueProps = {},
componentProps = {},
listProps = {},
}) {
const el = document.createElement('div'); const el = document.createElement('div');
document.body.appendChild(el); document.body.appendChild(el);
@ -25,7 +30,7 @@ export default function createComponent({ done, listIssueProps = {}, componentPr
boardsStore.create(); boardsStore.create();
const BoardListComp = Vue.extend(BoardList); const BoardListComp = Vue.extend(BoardList);
const list = new List(listObj); const list = new List({ ...listObj, ...listProps });
const issue = new ListIssue({ const issue = new ListIssue({
title: 'Testing', title: 'Testing',
id: 1, id: 1,
@ -35,7 +40,9 @@ export default function createComponent({ done, listIssueProps = {}, componentPr
assignees: [], assignees: [],
...listIssueProps, ...listIssueProps,
}); });
list.issuesSize = 1; if (!Object.prototype.hasOwnProperty.call(listProps, 'issuesSize')) {
list.issuesSize = 1;
}
list.issues.push(issue); list.issues.push(issue);
const component = new BoardListComp({ const component = new BoardListComp({

View file

@ -1,156 +1,210 @@
/* global List */
import Vue from 'vue'; import Vue from 'vue';
import eventHub from '~/boards/eventhub'; import eventHub from '~/boards/eventhub';
import createComponent from './board_list_common_spec'; import createComponent from './board_list_common_spec';
import waitForPromises from '../helpers/wait_for_promises';
import '~/boards/models/list';
describe('Board list component', () => { describe('Board list component', () => {
let mock; let mock;
let component; let component;
let getIssues;
beforeEach(done => { function generateIssues(compWrapper) {
({ mock, component } = createComponent({ done }));
});
afterEach(() => {
mock.restore();
});
it('renders component', () => {
expect(component.$el.classList.contains('board-list-component')).toBe(true);
});
it('renders loading icon', done => {
component.loading = true;
Vue.nextTick(() => {
expect(component.$el.querySelector('.board-list-loading')).not.toBeNull();
done();
});
});
it('renders issues', () => {
expect(component.$el.querySelectorAll('.board-card').length).toBe(1);
});
it('sets data attribute with issue id', () => {
expect(component.$el.querySelector('.board-card').getAttribute('data-issue-id')).toBe('1');
});
it('shows new issue form', done => {
component.toggleForm();
Vue.nextTick(() => {
expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
done();
});
});
it('shows new issue form after eventhub event', done => {
eventHub.$emit(`hide-issue-form-${component.list.id}`);
Vue.nextTick(() => {
expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
done();
});
});
it('does not show new issue form for closed list', done => {
component.list.type = 'closed';
component.toggleForm();
Vue.nextTick(() => {
expect(component.$el.querySelector('.board-new-issue-form')).toBeNull();
done();
});
});
it('shows count list item', done => {
component.showCount = true;
Vue.nextTick(() => {
expect(component.$el.querySelector('.board-list-count')).not.toBeNull();
expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
'Showing all issues',
);
done();
});
});
it('sets data attribute with invalid id', done => {
component.showCount = true;
Vue.nextTick(() => {
expect(component.$el.querySelector('.board-list-count').getAttribute('data-issue-id')).toBe(
'-1',
);
done();
});
});
it('shows how many more issues to load', done => {
component.showCount = true;
component.list.issuesSize = 20;
Vue.nextTick(() => {
expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
'Showing 1 of 20 issues',
);
done();
});
});
it('loads more issues after scrolling', done => {
spyOn(component.list, 'nextPage');
component.$refs.list.style.height = '100px';
component.$refs.list.style.overflow = 'scroll';
for (let i = 1; i < 20; i += 1) { for (let i = 1; i < 20; i += 1) {
const issue = Object.assign({}, component.list.issues[0]); const issue = Object.assign({}, compWrapper.list.issues[0]);
issue.id += i; issue.id += i;
component.list.issues.push(issue); compWrapper.list.issues.push(issue);
} }
}
Vue.nextTick(() => { describe('When Expanded', () => {
component.$refs.list.scrollTop = 20000; beforeEach(done => {
getIssues = spyOn(List.prototype, 'getIssues').and.returnValue(new Promise(() => {}));
({ mock, component } = createComponent({ done }));
});
setTimeout(() => { afterEach(() => {
expect(component.list.nextPage).toHaveBeenCalled(); mock.restore();
component.$destroy();
});
it('loads first page of issues', done => {
waitForPromises()
.then(() => {
expect(getIssues).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
it('renders component', () => {
expect(component.$el.classList.contains('board-list-component')).toBe(true);
});
it('renders loading icon', done => {
component.loading = true;
Vue.nextTick(() => {
expect(component.$el.querySelector('.board-list-loading')).not.toBeNull();
done();
});
});
it('renders issues', () => {
expect(component.$el.querySelectorAll('.board-card').length).toBe(1);
});
it('sets data attribute with issue id', () => {
expect(component.$el.querySelector('.board-card').getAttribute('data-issue-id')).toBe('1');
});
it('shows new issue form', done => {
component.toggleForm();
Vue.nextTick(() => {
expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
done();
});
});
it('shows new issue form after eventhub event', done => {
eventHub.$emit(`hide-issue-form-${component.list.id}`);
Vue.nextTick(() => {
expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
done();
});
});
it('does not show new issue form for closed list', done => {
component.list.type = 'closed';
component.toggleForm();
Vue.nextTick(() => {
expect(component.$el.querySelector('.board-new-issue-form')).toBeNull();
done();
});
});
it('shows count list item', done => {
component.showCount = true;
Vue.nextTick(() => {
expect(component.$el.querySelector('.board-list-count')).not.toBeNull();
expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
'Showing all issues',
);
done();
});
});
it('sets data attribute with invalid id', done => {
component.showCount = true;
Vue.nextTick(() => {
expect(component.$el.querySelector('.board-list-count').getAttribute('data-issue-id')).toBe(
'-1',
);
done();
});
});
it('shows how many more issues to load', done => {
component.showCount = true;
component.list.issuesSize = 20;
Vue.nextTick(() => {
expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
'Showing 1 of 20 issues',
);
done();
});
});
it('loads more issues after scrolling', done => {
spyOn(component.list, 'nextPage');
component.$refs.list.style.height = '100px';
component.$refs.list.style.overflow = 'scroll';
generateIssues(component);
Vue.nextTick(() => {
component.$refs.list.scrollTop = 20000;
waitForPromises()
.then(() => {
expect(component.list.nextPage).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
});
it('does not load issues if already loading', done => {
component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(
new Promise(() => {}),
);
component.onScroll();
component.onScroll();
waitForPromises()
.then(() => {
expect(component.list.nextPage).toHaveBeenCalledTimes(1);
})
.then(done)
.catch(done.fail);
});
it('shows loading more spinner', done => {
component.showCount = true;
component.list.loadingMore = true;
Vue.nextTick(() => {
expect(component.$el.querySelector('.board-list-count .gl-spinner')).not.toBeNull();
done(); done();
}); });
}); });
}); });
it('does not load issues if already loading', () => { describe('When Collapsed', () => {
component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue( beforeEach(done => {
new Promise(() => {}), getIssues = spyOn(List.prototype, 'getIssues').and.returnValue(new Promise(() => {}));
); ({ mock, component } = createComponent({
done,
listProps: { type: 'closed', collapsed: true, issuesSize: 50 },
}));
generateIssues(component);
component.scrollHeight = spyOn(component, 'scrollHeight').and.returnValue(0);
});
component.onScroll(); afterEach(() => {
component.onScroll(); mock.restore();
component.$destroy();
});
expect(component.list.nextPage).toHaveBeenCalledTimes(1); it('does not load all issues', done => {
}); waitForPromises()
.then(() => {
it('shows loading more spinner', done => { // Initial getIssues from list constructor
component.showCount = true; expect(getIssues).toHaveBeenCalledTimes(1);
component.list.loadingMore = true; })
.then(done)
Vue.nextTick(() => { .catch(done.fail);
expect(component.$el.querySelector('.board-list-count .gl-spinner')).not.toBeNull();
done();
}); });
}); });
}); });

View file

@ -7,44 +7,6 @@ describe Redactable do
stub_commonmark_sourcepos_disabled stub_commonmark_sourcepos_disabled
end end
shared_examples 'model with redactable field' do
it 'redacts unsubscribe token' do
model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
model.save!
expect(model[field]).to eq 'some text /sent_notifications/REDACTED/unsubscribe more text'
end
it 'ignores not hexadecimal tokens' do
text = 'some text /sent_notifications/token/unsubscribe more text'
model[field] = text
model.save!
expect(model[field]).to eq text
end
it 'ignores not matching texts' do
text = 'some text /sent_notifications/.*/unsubscribe more text'
model[field] = text
model.save!
expect(model[field]).to eq text
end
it 'redacts the field when saving the model before creating markdown cache' do
model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
model.save!
expected = 'some text /sent_notifications/REDACTED/unsubscribe more text'
expect(model[field]).to eq expected
expect(model["#{field}_html"]).to eq "<p dir=\"auto\">#{expected}</p>"
end
end
context 'when model is an issue' do context 'when model is an issue' do
it_behaves_like 'model with redactable field' do it_behaves_like 'model with redactable field' do
let(:model) { create(:issue) } let(:model) { create(:issue) }

View file

@ -2177,6 +2177,50 @@ describe MergeRequest do
end end
end end
describe '#check_mergeability' do
let(:mergeability_service) { double }
before do
allow(MergeRequests::MergeabilityCheckService).to receive(:new) do
mergeability_service
end
end
context 'if the merge status is unchecked' do
before do
subject.mark_as_unchecked!
end
it 'executes MergeabilityCheckService' do
expect(mergeability_service).to receive(:execute)
subject.check_mergeability
end
end
context 'if the merge status is checked' do
context 'and feature flag is enabled' do
it 'executes MergeabilityCheckService' do
expect(mergeability_service).not_to receive(:execute)
subject.check_mergeability
end
end
context 'and feature flag is disabled' do
before do
stub_feature_flags(merge_requests_conditional_mergeability_check: false)
end
it 'does not execute MergeabilityCheckService' do
expect(mergeability_service).to receive(:execute)
subject.check_mergeability
end
end
end
end
describe '#mergeable_state?' do describe '#mergeable_state?' do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }

View file

@ -150,6 +150,19 @@ describe Todo do
end end
end end
describe '#done?' do
let_it_be(:todo1) { create(:todo, state: :pending) }
let_it_be(:todo2) { create(:todo, state: :done) }
it 'returns true for todos with done state' do
expect(todo2.done?).to be_truthy
end
it 'returns false for todos with state pending' do
expect(todo1.done?).to be_falsey
end
end
describe '#self_assigned?' do describe '#self_assigned?' do
let(:user_1) { build(:user) } let(:user_1) { build(:user) }

View file

@ -0,0 +1,97 @@
# frozen_string_literal: true
require 'spec_helper'
describe 'Marking todos done' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:author) { create(:user) }
let_it_be(:other_user) { create(:user) }
let_it_be(:todo1) { create(:todo, user: current_user, author: author, state: :pending) }
let_it_be(:todo2) { create(:todo, user: current_user, author: author, state: :done) }
let_it_be(:other_user_todo) { create(:todo, user: other_user, author: author, state: :pending) }
let(:input) { { id: todo1.to_global_id.to_s } }
let(:mutation) do
graphql_mutation(:todo_mark_done, input,
<<-QL.strip_heredoc
clientMutationId
errors
todo {
id
state
}
QL
)
end
def mutation_response
graphql_mutation_response(:todo_mark_done)
end
it 'marks a single todo as done' do
post_graphql_mutation(mutation, current_user: current_user)
expect(todo1.reload.state).to eq('done')
expect(todo2.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
todo = mutation_response['todo']
expect(todo['id']).to eq(todo1.to_global_id.to_s)
expect(todo['state']).to eq('done')
end
context 'when todo is already marked done' do
let(:input) { { id: todo2.to_global_id.to_s } }
it 'has the expected response' do
post_graphql_mutation(mutation, current_user: current_user)
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
todo = mutation_response['todo']
expect(todo['id']).to eq(todo2.to_global_id.to_s)
expect(todo['state']).to eq('done')
end
end
context 'when todo does not belong to requesting user' do
let(:input) { { id: other_user_todo.to_global_id.to_s } }
let(:access_error) { 'The resource that you are attempting to access does not exist or you don\'t have permission to perform this action' }
it 'contains the expected error' do
post_graphql_mutation(mutation, current_user: current_user)
errors = json_response['errors']
expect(errors).not_to be_blank
expect(errors.first['message']).to eq(access_error)
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
end
end
context 'when using an invalid gid' do
let(:input) { { id: 'invalid_gid' } }
let(:invalid_gid_error) { 'invalid_gid is not a valid GitLab id.' }
it 'contains the expected error' do
post_graphql_mutation(mutation, current_user: current_user)
errors = json_response['errors']
expect(errors).not_to be_blank
expect(errors.first['message']).to eq(invalid_gid_error)
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
end
end
end

View file

@ -1,36 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
describe 'Loading a user avatar' do
let(:user) { create(:user, :with_avatar) }
context 'when logged in' do
# The exact query count will vary depending on the 2FA settings of the
# instance, group, and user. Removing those extra 2FA queries in this case
# may not be a good idea, so we just set up the ideal case.
before do
stub_application_setting(require_two_factor_authentication: true)
login_as(create(:user, :two_factor))
end
# One each for: current user, avatar user, and upload record
it 'only performs three SQL queries' do
get user.avatar_url # Skip queries on first application load
expect(response).to have_gitlab_http_status(200)
expect { get user.avatar_url }.not_to exceed_query_limit(3)
end
end
context 'when logged out' do
# One each for avatar user and upload record
it 'only performs two SQL queries' do
get user.avatar_url # Skip queries on first application load
expect(response).to have_gitlab_http_status(200)
expect { get user.avatar_url }.not_to exceed_query_limit(2)
end
end
end

View file

@ -0,0 +1,39 @@
# frozen_string_literal: true
shared_examples 'model with redactable field' do
it 'redacts unsubscribe token' do
model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
model.save!
expect(model[field]).to eq 'some text /sent_notifications/REDACTED/unsubscribe more text'
end
it 'ignores not hexadecimal tokens' do
text = 'some text /sent_notifications/token/unsubscribe more text'
model[field] = text
model.save!
expect(model[field]).to eq text
end
it 'ignores not matching texts' do
text = 'some text /sent_notifications/.*/unsubscribe more text'
model[field] = text
model.save!
expect(model[field]).to eq text
end
it 'redacts the field when saving the model before creating markdown cache' do
model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
model.save!
expected = 'some text /sent_notifications/REDACTED/unsubscribe more text'
expect(model[field]).to eq expected
expect(model["#{field}_html"]).to eq "<p dir=\"auto\">#{expected}</p>"
end
end