Commit 69944ffb authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 1b7381e9
// capture anything starting with http:// or https://
// up until a disallowed character or whitespace
export const blobLinkRegex = /https?:\/\/[^"<>\\^`{|}\s]+/g;
export default { blobLinkRegex };
...@@ -4,10 +4,6 @@ import Flash from '../../flash'; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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;
}); });
......
...@@ -4,8 +4,7 @@ import $ from 'jquery'; ...@@ -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 { ...@@ -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 { ...@@ -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');
......
...@@ -84,7 +84,8 @@ export default { ...@@ -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(() => {
......
...@@ -50,8 +50,8 @@ class List { ...@@ -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) {
......
...@@ -258,17 +258,6 @@ ...@@ -258,17 +258,6 @@
} }
} }
} }
.file-editor {
.ace_underline {
text-decoration: none;
}
.ace_line a {
pointer-events: auto;
color: inherit;
}
}
} }
span.idiff { span.idiff {
......
...@@ -29,12 +29,3 @@ ...@@ -29,12 +29,3 @@
color: $link; color: $link;
} }
} }
// Links to URLs, emails, or dependencies
.code .line a {
color: inherit;
&:hover {
text-decoration: underline;
}
}
...@@ -193,6 +193,11 @@ $dark-il: #de935f; ...@@ -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 */
......
...@@ -193,6 +193,11 @@ $monokai-gi: #a6e22e; ...@@ -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 */
......
...@@ -143,6 +143,12 @@ ...@@ -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 {
......
...@@ -196,6 +196,11 @@ $solarized-dark-il: #2aa198; ...@@ -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
......
...@@ -204,6 +204,11 @@ $solarized-light-il: #2aa198; ...@@ -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
......
...@@ -209,6 +209,11 @@ span.highlight_word { ...@@ -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;
......
...@@ -20,11 +20,11 @@ class ApplicationController < ActionController::Base ...@@ -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 ...@@ -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 ...@@ -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
......
...@@ -4,18 +4,15 @@ module ConfirmEmailWarning ...@@ -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
......
# 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 ...@@ -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
......
...@@ -20,7 +20,7 @@ class UploadsController < ApplicationController ...@@ -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]
......
...@@ -80,7 +80,7 @@ class GroupDescendantsFinder ...@@ -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
......
# 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
# 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
...@@ -16,6 +16,7 @@ module Types ...@@ -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
......
...@@ -405,7 +405,7 @@ module Ci ...@@ -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)])
......
...@@ -118,8 +118,8 @@ module Issuable ...@@ -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) }
......
...@@ -126,7 +126,7 @@ class Group < Namespace ...@@ -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)
......
...@@ -796,6 +796,8 @@ class MergeRequest < ApplicationRecord ...@@ -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
......
...@@ -1918,7 +1918,7 @@ class Project < ApplicationRecord ...@@ -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)
......
...@@ -160,6 +160,10 @@ class Todo < ApplicationRecord ...@@ -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
......
...@@ -7,4 +7,5 @@ class TodoPolicy < BasePolicy ...@@ -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
...@@ -149,7 +149,7 @@ module Ci ...@@ -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
......
...@@ -88,7 +88,7 @@ class CohortsService ...@@ -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
......
...@@ -10,7 +10,7 @@ ...@@ -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
---
title: Fix closed board list loading issue
merge_request:
author:
type: fixed
---
title: Mark todo done by GraphQL API
merge_request: 18581
author:
type: added
---
title: Run check_mergeability only if merge status requires it
merge_request: 19364
author:
type: performance
---
title: Make URLs in blob viewer and blob editor into clickable links
merge_request: 18305
author:
type: added
# frozen_string_literal: true
class AddCachedMarkdownVersionToVulnerabilities < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
add_column :vulnerabilities, :cached_markdown_version, :integer
end
end
# frozen_string_literal: true
class ChangeVulnerabilitiesTitleHtmlToNullable < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
change_column_null :vulnerabilities, :title_html, true
end
end
...@@ -10,7 +10,7 @@ ...@@ -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 ...@@ -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 ...@@ -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"
......
...@@ -3413,6 +3413,7 @@ type Mutation { ...@@ -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 { ...@@ -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
......
...@@ -14557,6 +14557,33 @@ ...@@ -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 @@ ...@@ -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",
......
...@@ -756,6 +756,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -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 |
......
...@@ -176,7 +176,7 @@ module Gitlab ...@@ -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
......
...@@ -28,7 +28,7 @@ dast_environment_deploy: ...@@ -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
......
...@@ -955,7 +955,7 @@ into similar problems in the future (e.g. when new tables are created). ...@@ -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
......
...@@ -20349,6 +20349,9 @@ msgstr "" ...@@ -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 ""
......
...@@ -90,16 +90,18 @@ describe ApplicationController do ...@@ -90,16 +90,18 @@ describe ApplicationController do
let(:format) { :html } let(:format) { :html }
it_behaves_like 'setting gon variables' it_behaves_like 'setting gon variables'
end
context 'with json format' do context 'for peek requests' do
let(:format) { :json } before do
request.path = '/-/peek'
end
it_behaves_like 'not setting gon variables' it_behaves_like 'not setting gon variables'
end
end end
context 'with atom format' do context 'with json format' do
let(:format) { :atom } let(:format) { :json }
it_behaves_like 'not setting gon variables' it_behaves_like 'not setting gon variables'
end end
......
...@@ -228,10 +228,10 @@ describe UploadsController do ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
......
...@@ -62,13 +62,6 @@ describe 'Editing file blob', :js do ...@@ -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))
......
# 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
...@@ -11,13 +11,6 @@ describe('Blob viewer', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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();
});
});
});
}); });
...@@ -15,7 +15,12 @@ import boardsStore from '~/boards/stores/boards_store'; ...@@ -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 ...@@ -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 ...@@ -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({
......
/* 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;
function generateIssues(compWrapper) {
for (let i = 1; i < 20; i += 1) {
const issue = Object.assign({}, compWrapper.list.issues[0]);
issue.id += i;
compWrapper.list.issues.push(issue);
}
}
beforeEach(done => { describe('When Expanded', () => {
({ mock, component } = createComponent({ done })); beforeEach(done => {
}); getIssues = spyOn(List.prototype, 'getIssues').and.returnValue(new Promise(() => {}));
({ mock, component } = createComponent({ done }));
});
afterEach(() => { afterEach(() => {
mock.restore(); mock.restore();
}); component.$destroy();
});
it('renders component', () => { it('loads first page of issues', done => {
expect(component.$el.classList.contains('board-list-component')).toBe(true); waitForPromises()
}); .then(() => {
expect(getIssues).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
it('renders loading icon', done => { it('renders component', () => {
component.loading = true; expect(component.$el.classList.contains('board-list-component')).toBe(true);
});
it('renders loading icon', done => {
component.loading = true;
Vue.nextTick(() => { Vue.nextTick(() => {
expect(component.$el.querySelector('.board-list-loading')).not.toBeNull(); expect(component.$el.querySelector('.board-list-loading')).not.toBeNull();
done(); done();
});
}); });
});
it('renders issues', () => { it('renders issues', () => {
expect(component.$el.querySelectorAll('.board-card').length).toBe(1); expect(component.$el.querySelectorAll('.board-card').length).toBe(1);
}); });
it('sets data attribute with issue id', () => { it('sets data attribute with issue id', () => {
expect(component.$el.querySelector('.board-card').getAttribute('data-issue-id')).toBe('1'); expect(component.$el.querySelector('.board-card').getAttribute('data-issue-id')).toBe('1');
}); });
it('shows new issue form', done => { it('shows new issue form', done => {
component.toggleForm(); component.toggleForm();
Vue.nextTick(() => { Vue.nextTick(() => {
expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull(); expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
expect(component.$el.querySelector('.is-smaller')).not.toBeNull(); expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
done(); done();
});
}); });
});
it('shows new issue form after eventhub event', done => { it('shows new issue form after eventhub event', done => {
eventHub.$emit(`hide-issue-form-${component.list.id}`); eventHub.$emit(`hide-issue-form-${component.list.id}`);
Vue.nextTick(() => { Vue.nextTick(() => {
expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull(); expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
expect(component.$el.querySelector('.is-smaller')).not.toBeNull(); expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
done(); done();
});
}); });
});
it('does not show new issue form for closed list', done => { it('does not show new issue form for closed list', done => {
component.list.type = 'closed'; component.list.type = 'closed';
component.toggleForm(); component.toggleForm();
Vue.nextTick(() => { Vue.nextTick(() => {
expect(component.$el.querySelector('.board-new-issue-form')).toBeNull(); expect(component.$el.querySelector('.board-new-issue-form')).toBeNull();
done(); done();
});
}); });
});
it('shows count list item', done => { it('shows count list item', done => {
component.showCount = true; component.showCount = true;
Vue.nextTick(() => { Vue.nextTick(() => {
expect(component.$el.querySelector('.board-list-count')).not.toBeNull(); expect(component.$el.querySelector('.board-list-count')).not.toBeNull();
expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe( expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
'Showing all issues', 'Showing all issues',
); );
done(); done();
});
}); });
});
it('sets data attribute with invalid id', done => { it('sets data attribute with invalid id', done => {
component.showCount = true; component.showCount = true;
Vue.nextTick(() => { Vue.nextTick(() => {
expect(component.$el.querySelector('.board-list-count').getAttribute('data-issue-id')).toBe( expect(component.$el.querySelector('.board-list-count').getAttribute('data-issue-id')).toBe(
'-1', '-1',
); );
done(); done();
});
}); });
});
it('shows how many more issues to load', done => { it('shows how many more issues to load', done => {
component.showCount = true; component.showCount = true;
component.list.issuesSize = 20; component.list.issuesSize = 20;
Vue.nextTick(() => { Vue.nextTick(() => {
expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe( expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
'Showing 1 of 20 issues', 'Showing 1 of 20 issues',
); );
done(); 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) { it('loads more issues after scrolling', done => {
const issue = Object.assign({}, component.list.issues[0]); spyOn(component.list, 'nextPage');
issue.id += i; component.$refs.list.style.height = '100px';
component.list.issues.push(issue); 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);
});
});
Vue.nextTick(() => { it('does not load issues if already loading', done => {
component.$refs.list.scrollTop = 20000; component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(
new Promise(() => {}),
);
setTimeout(() => { component.onScroll();
expect(component.list.nextPage).toHaveBeenCalled(); component.onScroll();
done(); waitForPromises()
}); .then(() => {
expect(component.list.nextPage).toHaveBeenCalledTimes(1);
})
.then(done)
.catch(done.fail);
}); });
});
it('does not load issues if already loading', () => { it('shows loading more spinner', done => {
component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue( component.showCount = true;
new Promise(() => {}), component.list.loadingMore = true;
);
component.onScroll(); Vue.nextTick(() => {
component.onScroll(); expect(component.$el.querySelector('.board-list-count .gl-spinner')).not.toBeNull();
expect(component.list.nextPage).toHaveBeenCalledTimes(1); done();
});
});
}); });
it('shows loading more spinner', done => { describe('When Collapsed', () => {
component.showCount = true; beforeEach(done => {
component.list.loadingMore = true; 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);
});
Vue.nextTick(() => { afterEach(() => {
expect(component.$el.querySelector('.board-list-count .gl-spinner')).not.toBeNull(); mock.restore();
component.$destroy();
});
done(); it('does not load all issues', done => {
waitForPromises()
.then(() => {
// Initial getIssues from list constructor
expect(getIssues).toHaveBeenCalledTimes(1);
})
.then(done)
.catch(done.fail);
}); });
}); });
}); });
...@@ -7,44 +7,6 @@ describe Redactable do ...@@ -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) }
......
...@@ -2177,6 +2177,50 @@ describe MergeRequest do ...@@ -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) }
......
...@@ -150,6 +150,19 @@ describe Todo do ...@@ -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) }
......
# 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
# 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
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment