Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1b7381e998
commit
69944ffb68
59 changed files with 846 additions and 401 deletions
|
@ -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 };
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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 {
|
||||||
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 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-editor {
|
|
||||||
.ace_underline {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ace_line a {
|
|
||||||
pointer-events: auto;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
span.idiff {
|
span.idiff {
|
||||||
|
|
|
@ -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;
|
||||||
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;
|
||||||
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 @@
|
||||||
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;
|
||||||
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;
|
||||||
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 {
|
||||||
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
|
||||||
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
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
|
||||||
|
|
17
app/graphql/mutations/todos/base.rb
Normal file
17
app/graphql/mutations/todos/base.rb
Normal 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
|
38
app/graphql/mutations/todos/mark_done.rb
Normal file
38
app/graphql/mutations/todos/mark_done.rb
Normal 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
|
|
@ -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
|
||||||
.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
|
||||||
# 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
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
# 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
|
||||||
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 @@
|
||||||
%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
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix closed board list loading issue
|
||||||
|
merge_request:
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Mark todo done by GraphQL API
|
||||||
|
merge_request: 18581
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Run check_mergeability only if merge status requires it
|
||||||
|
merge_request: 19364
|
||||||
|
author:
|
||||||
|
type: performance
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
title: Make URLs in blob viewer and blob editor into clickable links
|
|
||||||
merge_request: 18305
|
|
||||||
author:
|
|
||||||
type: added
|
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
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:
|
||||||
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).
|
||||||
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 ""
|
||||||
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,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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
66
spec/graphql/mutations/todos/mark_done_spec.rb
Normal file
66
spec/graphql/mutations/todos/mark_done_spec.rb
Normal 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
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
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
|
||||||
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) }
|
||||||
|
|
||||||
|
|
97
spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
Normal file
97
spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
Normal 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
|
|
@ -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
|
|
|
@ -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
|
Loading…
Reference in a new issue