Commit 3c19c971 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 38869-datetime

* master: (112 commits)
  small change to make less conflict with EE version
  Add cop for use of remove_column
  Resolve merge conflicts with dev.gitlab.org/master after security release
  add index for doc/administration/operations/
  Remove RubySampler#sample_objects for performance as well
  Bugfix: User can't change the access level of an access requester
  Add spec for removing issues.assignee_id
  updated imports
  Keep track of storage check timings
  Remove a header level in the new 'Automatic CE->EE merge' doc
  Improve down step of removing issues.assignee_id column
  Fix specs after removing assignee_id field
  Remove issues.assignee_id column
  Resolve conflicts in app/models/user.rb
  Fix image view mode
  Do not raise when downstream pipeline is created
  Remove the need for destroy and add a comment in the spec
  Use build instead of create in importer spec
  Simplify normalizing of paths
  Remove allocation tracking code from InfluxDB sampler for performance
  ...
parents a2479aaa 4ccbd556
...@@ -2,6 +2,17 @@ ...@@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.2.4 (2017-12-07)
### Security (5 changes)
- Fix e-mail address disclosure through member search fields
- Prevent creating issues through API when user does not have permissions
- Prevent an information disclosure in the Groups API
- Fix user without access to private Wiki being able to see it on the project page
- Fix Cross-Site Scripting (XSS) vulnerability while editing a comment
## 10.2.3 (2017-11-30) ## 10.2.3 (2017-11-30)
### Fixed (7 changes) ### Fixed (7 changes)
...@@ -237,6 +248,17 @@ entry. ...@@ -237,6 +248,17 @@ entry.
- Add Gitaly metrics to the performance bar. - Add Gitaly metrics to the performance bar.
## 10.1.5 (2017-12-07)
### Security (5 changes)
- Fix e-mail address disclosure through member search fields
- Prevent creating issues through API when user does not have permissions
- Prevent an information disclosure in the Groups API
- Fix user without access to private Wiki being able to see it on the project page
- Fix Cross-Site Scripting (XSS) vulnerability while editing a comment
## 10.1.4 (2017-11-14) ## 10.1.4 (2017-11-14)
### Fixed (4 changes) ### Fixed (4 changes)
...@@ -485,6 +507,17 @@ entry. ...@@ -485,6 +507,17 @@ entry.
- creation of keys moved to services. !13331 (haseebeqx) - creation of keys moved to services. !13331 (haseebeqx)
- Add username as GL_USERNAME in hooks. - Add username as GL_USERNAME in hooks.
## 10.0.7 (2017-12-07)
### Security (5 changes)
- Fix e-mail address disclosure through member search fields
- Prevent creating issues through API when user does not have permissions
- Prevent an information disclosure in the Groups API
- Fix user without access to private Wiki being able to see it on the project page
- Fix Cross-Site Scripting (XSS) vulnerability while editing a comment
## 10.0.5 (2017-11-03) ## 10.0.5 (2017-11-03)
- [FIXED] Fix incorrect X-axis labels in Prometheus graphs. !14258 - [FIXED] Fix incorrect X-axis labels in Prometheus graphs. !14258
......
...@@ -598,6 +598,7 @@ merge request: ...@@ -598,6 +598,7 @@ merge request:
present time and never use past tense (has been/was). For example instead present time and never use past tense (has been/was). For example instead
of _prohibited this user from being saved due to the following errors:_ the of _prohibited this user from being saved due to the following errors:_ the
text should be _sorry, we could not create your account because:_ text should be _sorry, we could not create your account because:_
1. Code should be written in [US English][us-english]
This is also the style used by linting tools such as This is also the style used by linting tools such as
[RuboCop](https://github.com/bbatsov/rubocop), [RuboCop](https://github.com/bbatsov/rubocop),
...@@ -663,6 +664,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor ...@@ -663,6 +664,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues [GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html [polling-etag]: https://docs.gitlab.com/ce/development/polling.html
[testing]: doc/development/testing_guide/index.md [testing]: doc/development/testing_guide/index.md
[us-english]: https://en.wikipedia.org/wiki/American_English
[^1]: Please note that specs other than JavaScript specs are considered backend [^1]: Please note that specs other than JavaScript specs are considered backend
code. code.
...@@ -283,7 +283,7 @@ group :metrics do ...@@ -283,7 +283,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false gem 'influxdb', '~> 0.2', require: false
# Prometheus # Prometheus
gem 'prometheus-client-mmap', '~> 0.7.0.beta39' gem 'prometheus-client-mmap', '~> 0.7.0.beta43'
gem 'raindrops', '~> 0.18' gem 'raindrops', '~> 0.18'
end end
...@@ -411,3 +411,6 @@ gem 'flipper-active_record', '~> 0.10.2' ...@@ -411,3 +411,6 @@ gem 'flipper-active_record', '~> 0.10.2'
# Structured logging # Structured logging
gem 'lograge', '~> 0.5' gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.7' gem 'grape_logging', '~> 1.7'
# Asset synchronization
gem 'asset_sync', '~> 2.2.0'
...@@ -58,6 +58,11 @@ GEM ...@@ -58,6 +58,11 @@ GEM
asciidoctor (1.5.3) asciidoctor (1.5.3)
asciidoctor-plantuml (0.0.7) asciidoctor-plantuml (0.0.7)
asciidoctor (~> 1.5) asciidoctor (~> 1.5)
asset_sync (2.2.0)
activemodel (>= 4.1.0)
fog-core
mime-types (>= 2.99)
unf
ast (2.3.0) ast (2.3.0)
atomic (1.1.99) atomic (1.1.99)
attr_encrypted (3.0.3) attr_encrypted (3.0.3)
...@@ -486,7 +491,6 @@ GEM ...@@ -486,7 +491,6 @@ GEM
mini_mime (0.1.4) mini_mime (0.1.4)
mini_portile2 (2.3.0) mini_portile2 (2.3.0)
minitest (5.7.0) minitest (5.7.0)
mmap2 (2.2.9)
mousetrap-rails (1.4.6) mousetrap-rails (1.4.6)
multi_json (1.12.2) multi_json (1.12.2)
multi_xml (0.6.0) multi_xml (0.6.0)
...@@ -623,8 +627,7 @@ GEM ...@@ -623,8 +627,7 @@ GEM
parser parser
unparser unparser
procto (0.0.3) procto (0.0.3)
prometheus-client-mmap (0.7.0.beta39) prometheus-client-mmap (0.7.0.beta43)
mmap2 (~> 2.2, >= 2.2.9)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
...@@ -977,6 +980,7 @@ DEPENDENCIES ...@@ -977,6 +980,7 @@ DEPENDENCIES
asana (~> 0.6.0) asana (~> 0.6.0)
asciidoctor (~> 1.5.2) asciidoctor (~> 1.5.2)
asciidoctor-plantuml (= 0.0.7) asciidoctor-plantuml (= 0.0.7)
asset_sync (~> 2.2.0)
attr_encrypted (~> 3.0.0) attr_encrypted (~> 3.0.0)
awesome_print (~> 1.2.0) awesome_print (~> 1.2.0)
babosa (~> 1.0.2) babosa (~> 1.0.2)
...@@ -1109,7 +1113,7 @@ DEPENDENCIES ...@@ -1109,7 +1113,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3) peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2) pg (~> 0.18.2)
premailer-rails (~> 1.9.7) premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta39) prometheus-client-mmap (~> 0.7.0.beta43)
pry-byebug (~> 3.4.1) pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1) rack-attack (~> 4.4.1)
......
...@@ -130,7 +130,8 @@ freeze date (the 7th) should have a corresponding Enterprise Edition merge ...@@ -130,7 +130,8 @@ freeze date (the 7th) should have a corresponding Enterprise Edition merge
request, even if there are no conflicts. This is to reduce the size of the request, even if there are no conflicts. This is to reduce the size of the
subsequent EE merge, as we often merge a lot to CE on the release date. For more subsequent EE merge, as we often merge a lot to CE on the release date. For more
information, see information, see
[limit conflicts with EE when developing on CE][limit_ee_conflicts]. [Automatic CE->EE merge][automatic_ce_ee_merge] and
[Guidelines for implementing Enterprise Edition features][ee_features].
### After the 7th ### After the 7th
...@@ -281,4 +282,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http ...@@ -281,4 +282,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http
["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements ["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements
[Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review [Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review
[done]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done [done]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done
[limit_ee_conflicts]: https://docs.gitlab.com/ce/development/limit_ee_conflicts.html [automatic_ce_ee_merge]: https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
[ee_features]: https://docs.gitlab.com/ce/development/ee_features.html
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */
import { refreshCurrentPage } from './lib/utils/url_utility';
window.Admin = (function() { window.Admin = (function() {
function Admin() { function Admin() {
...@@ -40,10 +41,10 @@ window.Admin = (function() { ...@@ -40,10 +41,10 @@ window.Admin = (function() {
return $('.change-owner-link').show(); return $('.change-owner-link').show();
}); });
$('li.project_member').bind('ajax:success', function() { $('li.project_member').bind('ajax:success', function() {
return gl.utils.refreshCurrentPage(); return refreshCurrentPage();
}); });
$('li.group_member').bind('ajax:success', function() { $('li.group_member').bind('ajax:success', function() {
return gl.utils.refreshCurrentPage(); return refreshCurrentPage();
}); });
showBlacklistType = function() { showBlacklistType = function() {
if ($("input[name='blacklist_type']:checked").val() === 'file') { if ($("input[name='blacklist_type']:checked").val() === 'file') {
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
// %button.js-toggle-button // %button.js-toggle-button
// %div.js-toggle-content // %div.js-toggle-content
// //
import { getLocationHash } from '../lib/utils/url_utility';
$(() => { $(() => {
function toggleContainer(container, toggleState) { function toggleContainer(container, toggleState) {
...@@ -32,7 +33,7 @@ $(() => { ...@@ -32,7 +33,7 @@ $(() => {
// If we're accessing a permalink, ensure it is not inside a // If we're accessing a permalink, ensure it is not inside a
// closed js-toggle-container! // closed js-toggle-container!
const hash = window.gl.utils.getLocationHash(); const hash = getLocationHash();
const anchor = hash && document.getElementById(hash); const anchor = hash && document.getElementById(hash);
const container = anchor && $(anchor).closest('.js-toggle-container'); const container = anchor && $(anchor).closest('.js-toggle-container');
......
/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */ /* eslint-disable func-names, object-shorthand, prefer-arrow-callback */
import Dropzone from 'dropzone'; import Dropzone from 'dropzone';
import '../lib/utils/url_utility'; import { visitUrl } from '../lib/utils/url_utility';
import { HIDDEN_CLASS } from '../lib/utils/constants'; import { HIDDEN_CLASS } from '../lib/utils/constants';
import csrf from '../lib/utils/csrf'; import csrf from '../lib/utils/csrf';
...@@ -49,7 +49,7 @@ export default class BlobFileDropzone { ...@@ -49,7 +49,7 @@ export default class BlobFileDropzone {
}); });
this.on('success', function (header, response) { this.on('success', function (header, response) {
$('#modal-upload-blob').modal('hide'); $('#modal-upload-blob').modal('hide');
window.gl.utils.visitUrl(response.filePath); visitUrl(response.filePath);
}); });
this.on('maxfilesexceeded', function (file) { this.on('maxfilesexceeded', function (file) {
dropzoneMessage.addClass(HIDDEN_CLASS); dropzoneMessage.addClass(HIDDEN_CLASS);
......
import { getLocationHash } from '../lib/utils/url_utility';
const lineNumberRe = /^L[0-9]+/; const lineNumberRe = /^L[0-9]+/;
const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => { const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
const hash = gl.utils.getLocationHash(); const hash = getLocationHash();
if (hash && lineNumberRe.test(hash)) { if (hash && lineNumberRe.test(hash)) {
const hashUrlString = `#${hash}`; const hashUrlString = `#${hash}`;
......
...@@ -121,7 +121,7 @@ export default class ImageFile { ...@@ -121,7 +121,7 @@ export default class ImageFile {
return $('.swipe.view', this.file).each((function(_this) { return $('.swipe.view', this.file).each((function(_this) {
return function(index, view) { return function(index, view) {
var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref; var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
ref = this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
$swipeFrame = $('.swipe-frame', view); $swipeFrame = $('.swipe-frame', view);
$swipeWrap = $('.swipe-wrap', view); $swipeWrap = $('.swipe-wrap', view);
$swipeBar = $('.swipe-bar', view); $swipeBar = $('.swipe-bar', view);
...@@ -158,7 +158,7 @@ export default class ImageFile { ...@@ -158,7 +158,7 @@ export default class ImageFile {
return $('.onion-skin.view', this.file).each((function(_this) { return $('.onion-skin.view', this.file).each((function(_this) {
return function(index, view) { return function(index, view) {
var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false; var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false;
ref = this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
$frame = $('.onion-skin-frame', view); $frame = $('.onion-skin-frame', view);
$frameAdded = $('.frame.added', view); $frameAdded = $('.frame.added', view);
$track = $('.drag-track', view); $track = $('.drag-track', view);
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
import { localTimeAgo } from './lib/utils/datetime_utility'; import { localTimeAgo } from './lib/utils/datetime_utility';
window.Compare = (function() { export default class Compare {
function Compare(opts) { constructor(opts) {
this.opts = opts; this.opts = opts;
this.source_loading = $(".js-source-loading"); this.source_loading = $(".js-source-loading");
this.target_loading = $(".js-target-loading"); this.target_loading = $(".js-target-loading");
...@@ -35,12 +35,12 @@ window.Compare = (function() { ...@@ -35,12 +35,12 @@ window.Compare = (function() {
this.initialState(); this.initialState();
} }
Compare.prototype.initialState = function() { initialState() {
this.getSourceHtml(); this.getSourceHtml();
return this.getTargetHtml(); this.getTargetHtml();
}; }
Compare.prototype.getTargetProject = function() { getTargetProject() {
return $.ajax({ return $.ajax({
url: this.opts.targetProjectUrl, url: this.opts.targetProjectUrl,
data: { data: {
...@@ -53,22 +53,22 @@ window.Compare = (function() { ...@@ -53,22 +53,22 @@ window.Compare = (function() {
return $('.js-target-branch-dropdown .dropdown-content').html(html); return $('.js-target-branch-dropdown .dropdown-content').html(html);
} }
}); });
}; }
Compare.prototype.getSourceHtml = function() { getSourceHtml() {
return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', { return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
ref: $("input[name='merge_request[source_branch]']").val() ref: $("input[name='merge_request[source_branch]']").val()
}); });
}; }
Compare.prototype.getTargetHtml = function() { getTargetHtml() {
return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', { return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
target_project_id: $("input[name='merge_request[target_project_id]']").val(), target_project_id: $("input[name='merge_request[target_project_id]']").val(),
ref: $("input[name='merge_request[target_branch]']").val() ref: $("input[name='merge_request[target_branch]']").val()
}); });
}; }
Compare.prototype.sendAjax = function(url, loading, target, data) { static sendAjax(url, loading, target, data) {
var $target; var $target;
$target = $(target); $target = $(target);
return $.ajax({ return $.ajax({
...@@ -85,7 +85,5 @@ window.Compare = (function() { ...@@ -85,7 +85,5 @@ window.Compare = (function() {
localTimeAgo($('.js-timeago', className)); localTimeAgo($('.js-timeago', className));
} }
}); });
}; }
}
return Compare;
})();
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */
window.CompareAutocomplete = (function() { export default function initCompareAutocomplete() {
function CompareAutocomplete() { $('.js-compare-dropdown').each(function() {
this.initDropdown(); var $dropdown, selected;
} $dropdown = $(this);
selected = $dropdown.data('selected');
CompareAutocomplete.prototype.initDropdown = function() { const $dropdownContainer = $dropdown.closest('.dropdown');
return $('.js-compare-dropdown').each(function() { const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer);
var $dropdown, selected; const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown = $(this); $dropdown.glDropdown({
selected = $dropdown.data('selected'); data: function(term, callback) {
const $dropdownContainer = $dropdown.closest('.dropdown'); return $.ajax({
const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer); url: $dropdown.data('refs-url'),
const $filterInput = $('input[type="search"]', $dropdownContainer); data: {
$dropdown.glDropdown({ ref: $dropdown.data('ref'),
data: function(term, callback) { search: term,
return $.ajax({
url: $dropdown.data('refs-url'),
data: {
ref: $dropdown.data('ref'),
search: term,
}
}).done(function(refs) {
return callback(refs);
});
},
selectable: true,
filterable: true,
filterRemote: true,
fieldName: $dropdown.data('field-name'),
filterInput: 'input[type="search"]',
renderRow: function(ref) {
var link;
if (ref.header != null) {
return $('<li />').addClass('dropdown-header').text(ref.header);
} else {
link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
return $('<li />').append(link);
} }
}, }).done(function(refs) {
id: function(obj, $el) { return callback(refs);
return $el.attr('data-ref'); });
}, },
toggleLabel: function(obj, $el) { selectable: true,
return $el.text().trim(); filterable: true,
} filterRemote: true,
}); fieldName: $dropdown.data('field-name'),
$filterInput.on('keyup', (e) => { filterInput: 'input[type="search"]',
const keyCode = e.keyCode || e.which; renderRow: function(ref) {
if (keyCode !== 13) return; var link;
const text = $filterInput.val(); if (ref.header != null) {
$fieldInput.val(text); return $('<li />').addClass('dropdown-header').text(ref.header);
$('.dropdown-toggle-text', $dropdown).text(text); } else {
$dropdownContainer.removeClass('open'); link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
}); return $('<li />').append(link);
$dropdownContainer.on('click', '.dropdown-content a', (e) => {
$dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) {
$dropdown.tooltip('fixTitle');
} }
}); },
id: function(obj, $el) {
return $el.attr('data-ref');
},
toggleLabel: function(obj, $el) {
return $el.text().trim();
}
});
$filterInput.on('keyup', (e) => {
const keyCode = e.keyCode || e.which;
if (keyCode !== 13) return;
const text = $filterInput.val();
$fieldInput.val(text);
$('.dropdown-toggle-text', $dropdown).text(text);
$dropdownContainer.removeClass('open');
}); });
};
return CompareAutocomplete; $dropdownContainer.on('click', '.dropdown-content a', (e) => {
})(); $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) {
$dropdown.tooltip('fixTitle');
}
});
});
}
...@@ -28,7 +28,7 @@ export default class ContextualSidebar { ...@@ -28,7 +28,7 @@ export default class ContextualSidebar {
this.$closeSidebar.on('click', () => this.toggleSidebarNav(false)); this.$closeSidebar.on('click', () => this.toggleSidebarNav(false));
this.$overlay.on('click', () => this.toggleSidebarNav(false)); this.$overlay.on('click', () => this.toggleSidebarNav(false));
this.$sidebarToggle.on('click', () => { this.$sidebarToggle.on('click', () => {
const value = !this.$sidebar.hasClass('sidebar-icons-only'); const value = !this.$sidebar.hasClass('sidebar-collapsed-desktop');
this.toggleCollapsedSidebar(value); this.toggleCollapsedSidebar(value);
}); });
...@@ -43,16 +43,16 @@ export default class ContextualSidebar { ...@@ -43,16 +43,16 @@ export default class ContextualSidebar {
} }
toggleSidebarNav(show) { toggleSidebarNav(show) {
this.$sidebar.toggleClass('nav-sidebar-expanded', show); this.$sidebar.toggleClass('sidebar-expanded-mobile', show);
this.$overlay.toggleClass('mobile-nav-open', show); this.$overlay.toggleClass('mobile-nav-open', show);
this.$sidebar.removeClass('sidebar-icons-only'); this.$sidebar.removeClass('sidebar-collapsed-desktop');
} }
toggleCollapsedSidebar(collapsed) { toggleCollapsedSidebar(collapsed) {
const breakpoint = bp.getBreakpointSize(); const breakpoint = bp.getBreakpointSize();
if (this.$sidebar.length) { if (this.$sidebar.length) {
this.$sidebar.toggleClass('sidebar-icons-only', collapsed); this.$sidebar.toggleClass('sidebar-collapsed-desktop', collapsed);
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
} }
ContextualSidebar.setCollapsedCookie(collapsed); ContextualSidebar.setCollapsedCookie(collapsed);
......
...@@ -32,7 +32,9 @@ ...@@ -32,7 +32,9 @@
doAction() { doAction() {
this.isLoading = true; this.isLoading = true;
eventHub.$emit(`${this.type}.key`, this.deployKey); eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
this.isLoading = false;
});
}, },
}, },
computed: { computed: {
...@@ -50,6 +52,9 @@ ...@@ -50,6 +52,9 @@
:disabled="isLoading" :disabled="isLoading"
@click="doAction"> @click="doAction">
{{ text }} {{ text }}
<loading-icon v-if="isLoading" /> <loading-icon
v-if="isLoading"
:inline="true"
/>
</button> </button>
</template> </template>
...@@ -47,12 +47,15 @@ ...@@ -47,12 +47,15 @@
.then(() => this.fetchKeys()) .then(() => this.fetchKeys())
.catch(() => new Flash('Error enabling deploy key')); .catch(() => new Flash('Error enabling deploy key'));
}, },
disableKey(deployKey) { disableKey(deployKey, callback) {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (confirm('You are going to remove this deploy key. Are you sure?')) { if (confirm('You are going to remove this deploy key. Are you sure?')) {
this.service.disableKey(deployKey.id) this.service.disableKey(deployKey.id)
.then(() => this.fetchKeys()) .then(() => this.fetchKeys())
.then(callback)
.catch(() => new Flash('Error removing deploy key')); .catch(() => new Flash('Error removing deploy key'));
} else {
callback();
} }
}, },
}, },
......
import './lib/utils/url_utility'; import { getLocationHash } from './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button'; import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff'; import SingleFileDiff from './single_file_diff';
import imageDiffHelper from './image_diff/helpers/index'; import imageDiffHelper from './image_diff/helpers/index';
...@@ -31,7 +31,7 @@ export default class Diff { ...@@ -31,7 +31,7 @@ export default class Diff {
isBound = true; isBound = true;
} }
if (gl.utils.getLocationHash()) { if (getLocationHash()) {
this.highlightSelectedLine(); this.highlightSelectedLine();
} }
...@@ -73,7 +73,7 @@ export default class Diff { ...@@ -73,7 +73,7 @@ export default class Diff {
} }
openAnchoredDiff(cb) { openAnchoredDiff(cb) {
const locationHash = gl.utils.getLocationHash(); const locationHash = getLocationHash();
const anchoredDiff = locationHash && locationHash.split('_')[0]; const anchoredDiff = locationHash && locationHash.split('_')[0];
if (!anchoredDiff) return; if (!anchoredDiff) return;
...@@ -128,7 +128,7 @@ export default class Diff { ...@@ -128,7 +128,7 @@ export default class Diff {
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
highlightSelectedLine() { highlightSelectedLine() {
const hash = gl.utils.getLocationHash(); const hash = getLocationHash();
const $diffFiles = $('.diff-file'); const $diffFiles = $('.diff-file');
$diffFiles.find('.hll').removeClass('hll'); $diffFiles.find('.hll').removeClass('hll');
......
...@@ -16,7 +16,8 @@ import './components/diff_note_avatars'; ...@@ -16,7 +16,8 @@ import './components/diff_note_avatars';
import './components/new_issue_for_discussion'; import './components/new_issue_for_discussion';
$(() => { $(() => {
const projectPath = document.querySelector('.merge-request').dataset.projectPath; const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box');
const projectPath = projectPathHolder.dataset.projectPath;
const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn'; const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
window.gl = window.gl || {}; window.gl = window.gl || {};
......
...@@ -43,7 +43,7 @@ class ResolveServiceClass { ...@@ -43,7 +43,7 @@ class ResolveServiceClass {
discussion.resolveAllNotes(resolvedBy); discussion.resolveAllNotes(resolvedBy);
} }
gl.mrWidget.checkStatus(); if (gl.mrWidget) gl.mrWidget.checkStatus();
discussion.updateHeadline(data); discussion.updateHeadline(data);
}) })
.catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.')); .catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.'));
......
...@@ -22,8 +22,8 @@ import NewCommitForm from './new_commit_form'; ...@@ -22,8 +22,8 @@ import NewCommitForm from './new_commit_form';
import Project from './project'; import Project from './project';
import projectAvatar from './project_avatar'; import projectAvatar from './project_avatar';
/* global MergeRequest */ /* global MergeRequest */
/* global Compare */ import Compare from './compare';
/* global CompareAutocomplete */ import initCompareAutocomplete from './compare_autocomplete';
/* global ProjectFindFile */ /* global ProjectFindFile */
import ProjectNew from './project_new'; import ProjectNew from './project_new';
import projectImport from './project_import'; import projectImport from './project_import';
...@@ -525,13 +525,6 @@ import ProjectVariables from './project_variables'; ...@@ -525,13 +525,6 @@ import ProjectVariables from './project_variables';
case 'projects:settings:ci_cd:show': case 'projects:settings:ci_cd:show':
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
import(/* webpackChunkName: "ci-cd-settings" */ './projects/ci_cd_settings_bundle')
.then(ciCdSettings => ciCdSettings.default())
.catch((err) => {
Flash(s__('ProjectSettings|Problem setting up the CI/CD settings JavaScript'));
throw err;
});
case 'groups:settings:ci_cd:show': case 'groups:settings:ci_cd:show':
new ProjectVariables(); new ProjectVariables();
break; break;
...@@ -629,7 +622,7 @@ import ProjectVariables from './project_variables'; ...@@ -629,7 +622,7 @@ import ProjectVariables from './project_variables';
projectAvatar(); projectAvatar();
switch (path[1]) { switch (path[1]) {
case 'compare': case 'compare':
new CompareAutocomplete(); initCompareAutocomplete();
break; break;
case 'edit': case 'edit':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
......
import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash'; import Flash from '../flash';
import FilteredSearchContainer from './container'; import FilteredSearchContainer from './container';
import RecentSearchesRoot from './recent_searches_root'; import RecentSearchesRoot from './recent_searches_root';
...@@ -566,7 +567,7 @@ class FilteredSearchManager { ...@@ -566,7 +567,7 @@ class FilteredSearchManager {
if (this.updateObject) { if (this.updateObject) {
this.updateObject(parameterizedUrl); this.updateObject(parameterizedUrl);
} else { } else {
gl.utils.visitUrl(parameterizedUrl); visitUrl(parameterizedUrl);
} }
} }
......
...@@ -21,7 +21,7 @@ let headerHeight = 50; ...@@ -21,7 +21,7 @@ let headerHeight = 50;
export const getHeaderHeight = () => headerHeight; export const getHeaderHeight = () => headerHeight;
export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-icons-only'); export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-collapsed-desktop');
export const canShowActiveSubItems = (el) => { export const canShowActiveSubItems = (el) => {
if (el.classList.contains('active') && !isSidebarCollapsed()) { if (el.classList.contains('active') && !isSidebarCollapsed()) {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
/* global fuzzaldrinPlus */ /* global fuzzaldrinPlus */
import _ from 'underscore'; import _ from 'underscore';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { visitUrl } from './lib/utils/url_utility';
import { isObject } from './lib/utils/type_utility'; import { isObject } from './lib/utils/type_utility';
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput; var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput;
...@@ -852,7 +853,7 @@ GitLabDropdown = (function() { ...@@ -852,7 +853,7 @@ GitLabDropdown = (function() {
if ($el.length) { if ($el.length) {
var href = $el.attr('href'); var href = $el.attr('href');
if (href && href !== '#') { if (href && href !== '#') {
gl.utils.visitUrl(href); visitUrl(href);
} else { } else {
$el.trigger('click'); $el.trigger('click');
} }
......
...@@ -5,7 +5,7 @@ import eventHub from '../event_hub'; ...@@ -5,7 +5,7 @@ import eventHub from '../event_hub';
import { getParameterByName } from '../../lib/utils/common_utils'; import { getParameterByName } from '../../lib/utils/common_utils';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import { COMMON_STR } from '../constants'; import { COMMON_STR } from '../constants';
import { mergeUrlParams } from '../../lib/utils/url_utility';
import groupsComponent from './groups.vue'; import groupsComponent from './groups.vue';
export default { export default {
...@@ -93,7 +93,7 @@ export default { ...@@ -93,7 +93,7 @@ export default {
this.isLoading = false; this.isLoading = false;
$.scrollTo(0); $.scrollTo(0);
const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href); const currentPath = mergeUrlParams({ page }, window.location.href);
window.history.replaceState({ window.history.replaceState({
page: currentPath, page: currentPath,
}, document.title, currentPath); }, document.title, currentPath);
......
<script> <script>
import { visitUrl } from '../../lib/utils/url_utility';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import identicon from '../../vue_shared/components/identicon.vue'; import identicon from '../../vue_shared/components/identicon.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -60,7 +61,7 @@ export default { ...@@ -60,7 +61,7 @@ export default {
if (this.hasChildren) { if (this.hasChildren) {
eventHub.$emit('toggleChildren', this.group); eventHub.$emit('toggleChildren', this.group);
} else { } else {
gl.utils.visitUrl(this.group.relativePath); visitUrl(this.group.relativePath);
} }
} }
}, },
......
import { visitUrl } from '../lib/utils/url_utility';
import DropLab from '../droplab/drop_lab'; import DropLab from '../droplab/drop_lab';
import ISetter from '../droplab/plugins/input_setter'; import ISetter from '../droplab/plugins/input_setter';
...@@ -54,9 +55,9 @@ export default class NewGroupChild { ...@@ -54,9 +55,9 @@ export default class NewGroupChild {
onClickNewGroupChildButton(e) { onClickNewGroupChildButton(e) {
if (e.target.dataset.action === NEW_PROJECT) { if (e.target.dataset.action === NEW_PROJECT) {
gl.utils.visitUrl(this.newGroupPath); visitUrl(this.newGroupPath);
} else if (e.target.dataset.action === NEW_SUBGROUP) { } else if (e.target.dataset.action === NEW_SUBGROUP) {
gl.utils.visitUrl(this.subgroupPath); visitUrl(this.subgroupPath);
} }
} }
} }
...@@ -8,18 +8,7 @@ import IssuablesHelper from './helpers/issuables_helper'; ...@@ -8,18 +8,7 @@ import IssuablesHelper from './helpers/issuables_helper';
export default class Issue { export default class Issue {
constructor() { constructor() {
if ($('a.btn-close').length) { if ($('a.btn-close').length) this.initIssueBtnEventListeners();
this.taskList = new TaskList({
dataType: 'issue',
fieldName: 'description',
selector: '.detail-page-description',
onSuccess: (result) => {
document.querySelector('#task_status').innerText = result.task_status;
document.querySelector('#task_status_short').innerText = result.task_status_short;
}
});
this.initIssueBtnEventListeners();
}
Issue.$btnNewBranch = $('#new-branch'); Issue.$btnNewBranch = $('#new-branch');
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap'); Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
......
<script> <script>
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import { visitUrl } from '../../lib/utils/url_utility';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import Service from '../services/index'; import Service from '../services/index';
...@@ -8,7 +9,7 @@ import titleComponent from './title.vue'; ...@@ -8,7 +9,7 @@ import titleComponent from './title.vue';
import descriptionComponent from './description.vue'; import descriptionComponent from './description.vue';
import editedComponent from './edited.vue'; import editedComponent from './edited.vue';
import formComponent from './form.vue'; import formComponent from './form.vue';
import '../../lib/utils/url_utility'; import RecaptchaDialogImplementor from '../../vue_shared/mixins/recaptcha_dialog_implementor';
export default { export default {
props: { props: {
...@@ -149,6 +150,11 @@ export default { ...@@ -149,6 +150,11 @@ export default {
editedComponent, editedComponent,
formComponent, formComponent,
}, },
mixins: [
RecaptchaDialogImplementor,
],
methods: { methods: {
openForm() { openForm() {
if (!this.showForm) { if (!this.showForm) {
...@@ -164,12 +170,14 @@ export default { ...@@ -164,12 +170,14 @@ export default {
closeForm() { closeForm() {
this.showForm = false; this.showForm = false;
}, },
updateIssuable() { updateIssuable() {
this.service.updateIssuable(this.store.formState) this.service.updateIssuable(this.store.formState)
.then(res => res.json()) .then(res => res.json())
.then(data => this.checkForSpam(data))
.then((data) => { .then((data) => {
if (location.pathname !== data.web_url) { if (location.pathname !== data.web_url) {
gl.utils.visitUrl(data.web_url); visitUrl(data.web_url);
} }
return this.service.getData(); return this.service.getData();
...@@ -179,11 +187,24 @@ export default { ...@@ -179,11 +187,24 @@ export default {
this.store.updateState(data); this.store.updateState(data);
eventHub.$emit('close.form'); eventHub.$emit('close.form');
}) })
.catch(() => { .catch((error) => {
eventHub.$emit('close.form'); if (error && error.name === 'SpamError') {
window.Flash(`Error updating ${this.issuableType}`); this.openRecaptcha();
} else {
eventHub.$emit('close.form');
window.Flash(`Error updating ${this.issuableType}`);
}
}); });
}, },
closeRecaptchaDialog() {
this.store.setFormState({
updateLoading: false,
});
this.closeRecaptcha();
},
deleteIssuable() { deleteIssuable() {
this.service.deleteIssuable() this.service.deleteIssuable()
.then(res => res.json()) .then(res => res.json())
...@@ -191,7 +212,7 @@ export default { ...@@ -191,7 +212,7 @@ export default {
// Stop the poll so we don't get 404's with the issuable not existing // Stop the poll so we don't get 404's with the issuable not existing
this.poll.stop(); this.poll.stop();
gl.utils.visitUrl(data.web_url); visitUrl(data.web_url);
}) })
.catch(() => { .catch(() => {
eventHub.$emit('close.form'); eventHub.$emit('close.form');
...@@ -237,9 +258,9 @@ export default { ...@@ -237,9 +258,9 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<div v-if="canUpdate && showForm">
<form-component <form-component
v-if="canUpdate && showForm"
:form-state="formState" :form-state="formState"
:can-destroy="canDestroy" :can-destroy="canDestroy"
:issuable-templates="issuableTemplates" :issuable-templates="issuableTemplates"
...@@ -251,30 +272,37 @@ export default { ...@@ -251,30 +272,37 @@ export default {
:can-attach-file="canAttachFile" :can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete" :enable-autocomplete="enableAutocomplete"
/> />
<div v-else>
<title-component <recaptcha-dialog
:issuable-ref="issuableRef" v-show="showRecaptcha"
:can-update="canUpdate" :html="recaptchaHTML"
:title-html="state.titleHtml" @close="closeRecaptchaDialog"
:title-text="state.titleText" />
:show-inline-edit-button="showInlineEditButton" </div>
/> <div v-else>
<description-component <title-component
v-if="state.descriptionHtml" :issuable-ref="issuableRef"
:can-update="canUpdate" :can-update="canUpdate"
:description-html="state.descriptionHtml" :title-html="state.titleHtml"
:description-text="state.descriptionText" :title-text="state.titleText"
:updated-at="state.updatedAt" :show-inline-edit-button="showInlineEditButton"
:task-status="state.taskStatus" />
:issuable-type="issuableType" <description-component
:update-url="updateEndpoint" v-if="state.descriptionHtml"
/> :can-update="canUpdate"
<edited-component :description-html="state.descriptionHtml"
v-if="hasUpdated" :description-text="state.descriptionText"
:updated-at="state.updatedAt" :updated-at="state.updatedAt"
:updated-by-name="state.updatedByName" :task-status="state.taskStatus"
:updated-by-path="state.updatedByPath" :issuable-type="issuableType"
/> :update-url="updateEndpoint"
</div> />
<edited-component
v-if="hasUpdated"
:updated-at="state.updatedAt"
:updated-by-name="state.updatedByName"
:updated-by-path="state.updatedByPath"
/>
</div> </div>
</div>
</template> </template>
<script> <script>
import animateMixin from '../mixins/animate'; import animateMixin from '../mixins/animate';
import TaskList from '../../task_list'; import TaskList from '../../task_list';
import RecaptchaDialogImplementor from '../../vue_shared/mixins/recaptcha_dialog_implementor';
export default { export default {
mixins: [animateMixin], mixins: [
animateMixin,
RecaptchaDialogImplementor,
],
props: { props: {
canUpdate: { canUpdate: {
type: Boolean, type: Boolean,
...@@ -51,6 +56,7 @@ ...@@ -51,6 +56,7 @@
this.updateTaskStatusText(); this.updateTaskStatusText();
}, },
}, },
methods: { methods: {
renderGFM() { renderGFM() {
$(this.$refs['gfm-content']).renderGFM(); $(this.$refs['gfm-content']).renderGFM();
...@@ -61,9 +67,19 @@ ...@@ -61,9 +67,19 @@
dataType: this.issuableType, dataType: this.issuableType,
fieldName: 'description', fieldName: 'description',
selector: '.detail-page-description', selector: '.detail-page-description',
onSuccess: this.taskListUpdateSuccess.bind(this),
}); });
} }
}, },
taskListUpdateSuccess(data) {
try {
this.checkForSpam(data);
} catch (error) {
if (error && error.name === 'SpamError') this.openRecaptcha();
}
},
updateTaskStatusText() { updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/); const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta'); const $issuableHeader = $('.issuable-meta');
...@@ -109,5 +125,11 @@ ...@@ -109,5 +125,11 @@
:data-update-url="updateUrl" :data-update-url="updateUrl"
> >
</textarea> </textarea>
<recaptcha-dialog
v-show="showRecaptcha"
:html="recaptchaHTML"
@close="closeRecaptcha"
/>
</div> </div>
</template> </template>
...@@ -5,7 +5,7 @@ import '../vue_shared/vue_resource_interceptor'; ...@@ -5,7 +5,7 @@ import '../vue_shared/vue_resource_interceptor';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const initialDataEl = document.getElementById('js-issuable-app-initial-data'); const initialDataEl = document.getElementById('js-issuable-app-initial-data');
const initialData = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"')); const props = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"'));
$('.issuable-edit').on('click', (e) => { $('.issuable-edit').on('click', (e) => {
e.preventDefault(); e.preventDefault();
...@@ -18,32 +18,9 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -18,32 +18,9 @@ document.addEventListener('DOMContentLoaded', () => {
components: { components: {
issuableApp, issuableApp,
}, },
data() {
return {
...initialData,
};
},
render(createElement) { render(createElement) {
return createElement('issuable-app', { return createElement('issuable-app', {
props: { props,
canUpdate: this.canUpdate,
canDestroy: this.canDestroy,
endpoint: this.endpoint,
issuableRef: this.issuableRef,
initialTitleHtml: this.initialTitleHtml,
initialTitleText: this.initialTitleText,
initialDescriptionHtml: this.initialDescriptionHtml,
initialDescriptionText: this.initialDescriptionText,
issuableTemplates: this.issuableTemplates,
markdownPreviewPath: this.markdownPreviewPath,
markdownDocsPath: this.markdownDocsPath,
projectPath: this.projectPath,
projectNamespace: this.projectNamespace,
updatedAt: this.updatedAt,
updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath,
initialTaskStatus: this.initialTaskStatus,
},
}); });
}, },
}); });
......
import _ from 'underscore'; import _ from 'underscore';
import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints'; import bp from './breakpoints';
import { bytesToKiB } from './lib/utils/number_utils'; import { bytesToKiB } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils'; import { setCiStatusFavicon } from './lib/utils/common_utils';
...@@ -10,7 +11,7 @@ export default class Job { ...@@ -10,7 +11,7 @@ export default class Job {
this.state = null; this.state = null;
this.options = options || $('.js-build-options').data(); this.options = options || $('.js-build-options').data();
this.pageUrl = this.options.pageUrl; this.pagePath = this.options.pagePath;
this.buildStatus = this.options.buildStatus; this.buildStatus = this.options.buildStatus;
this.state = this.options.logState; this.state = this.options.logState;
this.buildStage = this.options.buildStage; this.buildStage = this.options.buildStage;
...@@ -168,11 +169,11 @@ export default class Job { ...@@ -168,11 +169,11 @@ export default class Job {
getBuildTrace() { getBuildTrace() {
return $.ajax({ return $.ajax({
url: `${this.pageUrl}/trace.json`, url: `${this.pagePath}/trace.json`,
data: { state: this.state }, data: { state: this.state },
}) })
.done((log) => { .done((log) => {
setCiStatusFavicon(`${this.pageUrl}/status.json`); setCiStatusFavicon(`${this.pagePath}/status.json`);
if (log.state) { if (log.state) {
this.state = log.state; this.state = log.state;
...@@ -210,7 +211,7 @@ export default class Job { ...@@ -210,7 +211,7 @@ export default class Job {
} }
if (log.status !== this.buildStatus) { if (log.status !== this.buildStatus) {
gl.utils.visitUrl(this.pageUrl); visitUrl(this.pagePath);
} }
}) })
.fail(() => { .fail(() => {
......
import { getLocationHash } from './url_utility';
export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index]; export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
...@@ -65,7 +66,7 @@ export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventNa ...@@ -65,7 +66,7 @@ export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventNa
// automatically adjust scroll position for hash urls taking the height of the navbar into account // automatically adjust scroll position for hash urls taking the height of the navbar into account
// https://github.com/twitter/bootstrap/issues/1768 // https://github.com/twitter/bootstrap/issues/1768
export const handleLocationHash = () => { export const handleLocationHash = () => {
let hash = window.gl.utils.getLocationHash(); let hash = getLocationHash();
if (!hash) return; if (!hash) return;
// This is required to handle non-unicode characters in hash // This is required to handle non-unicode characters in hash
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
var base;
var w = window;
if (w.gl == null) {
w.gl = {};
}
if ((base = w.gl).utils == null) {
base.utils = {};
}
// Returns an array containing the value(s) of the // Returns an array containing the value(s) of the
// of the key passed as an argument // of the key passed as an argument
w.gl.utils.getParameterValues = function(sParam) { export function getParameterValues(sParam) {
var i, sPageURL, sParameterName, sURLVariables, values; const sPageURL = decodeURIComponent(window.location.search.substring(1));
sPageURL = decodeURIComponent(window.location.search.substring(1));
sURLVariables = sPageURL.split('&'); return sPageURL.split('&').reduce((acc, urlParam) => {
sParameterName = void 0; const sParameterName = urlParam.split('=');
values = [];
i = 0;
while (i < sURLVariables.length) {
sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] === sParam) { if (sParameterName[0] === sParam) {
values.push(sParameterName[1].replace(/\+/g, ' ')); acc.push(sParameterName[1].replace(/\+/g, ' '));
} }
i += 1;
} return acc;
return values; }, []);
}; }
// @param {Object} params - url keys and value to merge // @param {Object} params - url keys and value to merge
// @param {String} url // @param {String} url
w.gl.utils.mergeUrlParams = function(params, url) { export function mergeUrlParams(params, url) {
var lastChar, newUrl, paramName, paramValue, pattern; let newUrl = Object.keys(params).reduce((acc, paramName) => {
newUrl = decodeURIComponent(url); const paramValue = params[paramName];
for (paramName in params) { const pattern = new RegExp(`\\b(${paramName}=).*?(&|$)`);
paramValue = params[paramName];
pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)"); if (paramValue === null) {
if (paramValue == null) { return acc.replace(pattern, '');
newUrl = newUrl.replace(pattern, '');
} else if (url.search(pattern) !== -1) { } else if (url.search(pattern) !== -1) {
newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2"); return acc.replace(pattern, `$1${paramValue}$2`);
} else {
newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
} }
}
return `${acc}${acc.indexOf('?') > 0 ? '&' : '?'}${paramName}=${paramValue}`;
}, decodeURIComponent(url));
// Remove a trailing ampersand // Remove a trailing ampersand
lastChar = newUrl[newUrl.length - 1]; const lastChar = newUrl[newUrl.length - 1];
if (lastChar === '&') { if (lastChar === '&') {
newUrl = newUrl.slice(0, -1); newUrl = newUrl.slice(0, -1);
} }
return newUrl; return newUrl;
}; }
// removes parameter query string from url. returns the modified url
w.gl.utils.removeParamQueryString = function(url, param) { export function removeParamQueryString(url, param) {
var urlVariables, variables; const decodedUrl = decodeURIComponent(url);
url = decodeURIComponent(url); const urlVariables = decodedUrl.split('&');
urlVariables = url.split('&');
return ((function() { return urlVariables.filter(variable => variable.indexOf(param) === -1).join('&');
var j, len, results; }
results = [];
for (j = 0, len = urlVariables.length; j < len; j += 1) { export function removeParams(params) {
variables = urlVariables[j];
if (variables.indexOf(param) === -1) {
results.push(variables);
}
}
return results;
})()).join('&');
};
w.gl.utils.removeParams = (params) => {
const url = document.createElement('a'); const url = document.createElement('a');
url.href = window.location.href; url.href = window.location.href;
params.forEach((param) => { params.forEach((param) => {
url.search = w.gl.utils.removeParamQueryString(url.search, param); url.search = removeParamQueryString(url.search, param);
}); });
return url.href; return url.href;
}; }
w.gl.utils.getLocationHash = function(url) {
var hashIndex;
if (typeof url === 'undefined') {
// Note: We can't use window.location.hash here because it's
// not consistent across browsers - Firefox will pre-decode it
url = window.location.href;
}
hashIndex = url.indexOf('#');
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
};
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(window.location.href); export function getLocationHash(url = window.location.href) {
const hashIndex = url.indexOf('#');
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
}
// eslint-disable-next-line import/prefer-default-export
export function visitUrl(url, external = false) { export function visitUrl(url, external = false) {
if (external) { if (external) {
// Simulate `target="blank" rel="noopener noreferrer"` // Simulate `target="blank" rel="noopener noreferrer"`
...@@ -100,12 +76,10 @@ export function visitUrl(url, external = false) { ...@@ -100,12 +76,10 @@ export function visitUrl(url, external = false) {
} }
} }
export function refreshCurrentPage() {
visitUrl(window.location.href);
}
export function redirectTo(url) { export function redirectTo(url) {
return window.location.assign(url); return window.location.assign(url);
} }
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
visitUrl,
};
...@@ -28,8 +28,8 @@ import './commit/image_file'; ...@@ -28,8 +28,8 @@ import './commit/image_file';
// lib/utils // lib/utils
import { handleLocationHash } from './lib/utils/common_utils'; import { handleLocationHash } from './lib/utils/common_utils';
import { localTimeAgo, renderTimeago, getLocationHash } from './lib/utils/datetime_utility'; import { localTimeAgo, renderTimeago } from './lib/utils/datetime_utility';
import './lib/utils/url_utility'; import { getLocationHash, visitUrl } from './lib/utils/url_utility';
// behaviors // behaviors
import './behaviors/'; import './behaviors/';
...@@ -40,9 +40,6 @@ import './admin'; ...@@ -40,9 +40,6 @@ import './admin';
import './aside'; import './aside';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import bp from './breakpoints'; import bp from './breakpoints';
import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal'; import './confirm_danger_modal';
import Flash, { removeFlashClickListener } from './flash'; import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown'; import './gl_dropdown';
...@@ -294,7 +291,7 @@ $(function () { ...@@ -294,7 +291,7 @@ $(function () {
const action = `${this.action}${link.search === '' ? '?' : '&'}`; const action = `${this.action}${link.search === '' ? '?' : '&'}`;
event.preventDefault(); event.preventDefault();
gl.utils.visitUrl(`${action}${$(this).serialize()}`); visitUrl(`${action}${$(this).serialize()}`);
}); });
const flashContainer = document.querySelector('.flash-container'); const flashContainer = document.querySelector('.flash-container');
......
...@@ -11,6 +11,7 @@ import { ...@@ -11,6 +11,7 @@ import {
handleLocationHash, handleLocationHash,
isMetaClick, isMetaClick,
} from './lib/utils/common_utils'; } from './lib/utils/common_utils';
import { getLocationHash } from './lib/utils/url_utility';
import initDiscussionTab from './image_diff/init_discussion_tab'; import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff'; import Diff from './diff';
import { import {
...@@ -320,7 +321,7 @@ import { ...@@ -320,7 +321,7 @@ import {
// Scroll any linked note into view // Scroll any linked note into view
// Similar to `toggler_behavior` in the discussion tab // Similar to `toggler_behavior` in the discussion tab
const hash = gl.utils.getLocationHash(); const hash = getLocationHash();
const anchor = hash && $container.find(`.note[id="${hash}"]`); const anchor = hash && $container.find(`.note[id="${hash}"]`);
if (anchor && anchor.length > 0) { if (anchor && anchor.length > 0) {
const notesContent = anchor.closest('.notes_content'); const notesContent = anchor.closest('.notes_content');
......
...@@ -21,6 +21,8 @@ ...@@ -21,6 +21,8 @@
hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics), hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath, documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath, settingsPath: metricsData.settingsPath,
tagsPath: metricsData.tagsPath,
projectPath: metricsData.projectPath,
metricsEndpoint: metricsData.additionalMetrics, metricsEndpoint: metricsData.additionalMetrics,
deploymentEndpoint: metricsData.deploymentEndpoint, deploymentEndpoint: metricsData.deploymentEndpoint,
emptyGettingStartedSvgPath: metricsData.emptyGettingStartedSvgPath, emptyGettingStartedSvgPath: metricsData.emptyGettingStartedSvgPath,
...@@ -112,6 +114,8 @@ ...@@ -112,6 +114,8 @@
:hover-data="hoverData" :hover-data="hoverData"
:update-aspect-ratio="updateAspectRatio" :update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData" :deployment-data="store.deploymentData"
:project-path="projectPath"
:tags-path="tagsPath"
/> />
</graph-group> </graph-group>
</div> </div>
......
...@@ -30,6 +30,14 @@ ...@@ -30,6 +30,14 @@
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
projectPath: {
type: String,
required: true,
},
tagsPath: {
type: String,
required: true,
},
}, },
mixins: [MonitoringMixin], mixins: [MonitoringMixin],
...@@ -251,6 +259,14 @@ ...@@ -251,6 +259,14 @@
:line-color="path.lineColor" :line-color="path.lineColor"
:area-color="path.areaColor" :area-color="path.areaColor"
/> />
<rect
class="prometheus-graph-overlay"
:width="(graphWidth - 70)"
:height="(graphHeight - 100)"
transform="translate(-5, 20)"
ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)">
</rect>
<graph-deployment <graph-deployment
:show-deploy-info="showDeployInfo" :show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData" :deployment-data="reducedDeploymentData"
...@@ -267,14 +283,6 @@ ...@@ -267,14 +283,6 @@
:graph-height-offset="graphHeightOffset" :graph-height-offset="graphHeightOffset"
:show-flag-content="showFlagContent" :show-flag-content="showFlagContent"
/> />
<rect
class="prometheus-graph-overlay"
:width="(graphWidth - 70)"
:height="(graphHeight - 100)"
transform="translate(-5, 20)"
ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)">
</rect>
</svg> </svg>
</svg> </svg>
</div> </div>
......
<script> <script>
import { dateFormat, timeFormat } from '../../utils/date_time_formatters'; import { dateFormatWithName, timeFormat } from '../../utils/date_time_formatters';
import Icon from '../../../vue_shared/components/icon.vue';
export default { export default {
props: { props: {
...@@ -25,6 +26,10 @@ ...@@ -25,6 +26,10 @@
}, },
}, },
components: {
Icon,
},
computed: { computed: {
calculatedHeight() { calculatedHeight() {
return this.graphHeight - this.graphHeightOffset; return this.graphHeight - this.graphHeightOffset;
...@@ -33,7 +38,7 @@ ...@@ -33,7 +38,7 @@
methods: { methods: {
refText(d) { refText(d) {
return d.tag ? d.ref : d.sha.slice(0, 6); return d.tag ? d.ref : d.sha.slice(0, 8);
}, },
formatTime(deploymentTime) { formatTime(deploymentTime) {
...@@ -41,7 +46,7 @@ ...@@ -41,7 +46,7 @@
}, },
formatDate(deploymentTime) { formatDate(deploymentTime) {
return dateFormat(deploymentTime); return dateFormatWithName(deploymentTime);
}, },
nameDeploymentClass(deployment) { nameDeploymentClass(deployment) {
...@@ -54,11 +59,19 @@ ...@@ -54,11 +59,19 @@
positionFlag(deployment) { positionFlag(deployment) {
let xPosition = 3; let xPosition = 3;
if (deployment.xPos > (this.graphWidth - 200)) { if (deployment.xPos > (this.graphWidth - 225)) {
xPosition = -97; xPosition = -142;
} }
return xPosition; return xPosition;
}, },
svgContainerHeight(tag) {
let svgHeight = 80;
if (!tag) {
svgHeight -= 20;
}
return svgHeight;
},
}, },
}; };
</script> </script>
...@@ -91,35 +104,75 @@ ...@@ -91,35 +104,75 @@
class="js-deploy-info-box" class="js-deploy-info-box"
:x="positionFlag(deployment)" :x="positionFlag(deployment)"
y="0" y="0"
width="92" width="134"
height="60"> :height="svgContainerHeight(deployment.tag)">
<rect <rect
class="rect-text-metric deploy-info-rect rect-metric" class="rect-text-metric deploy-info-rect rect-metric"
x="1" x="1"
y="1" y="1"
rx="2" rx="2"
width="90" width="132"
height="58"> :height="svgContainerHeight(deployment.tag) - 2">
</rect> </rect>
<g
transform="translate(5, 2)">
<text
class="deploy-info-text text-metric-bold">
{{refText(deployment)}}
</text>
</g>
<text
class="deploy-info-text"
y="18"
transform="translate(5, 2)">
{{formatDate(deployment.time)}}
</text>
<text <text
class="deploy-info-text text-metric-bold" class="deploy-info-text text-metric-bold"
y="38"
transform="translate(5, 2)"> transform="translate(5, 2)">
{{formatTime(deployment.time)}} Deployed
</text> </text>
<!--The date info-->
<g transform="translate(5, 20)">
<text class="deploy-info-text">
{{formatDate(deployment.time)}}
</text>
<text
class="deploy-info-text text-metric-bold"
x="62">
{{formatTime(deployment.time)}}
</text>
</g>
<line
class="divider-line"
x1="0"
y1="38"
x2="132"
:y2="38"
stroke="#000">
</line>
<!--Commit information-->
<g transform="translate(5, 40)">
<icon
name="commit"
:width="12"
:height="12"
:y="3">
</icon>
<a :xlink:href="deployment.commitUrl">
<text
class="deploy-info-text deploy-info-text-link"
transform="translate(20, 2)">
{{refText(deployment)}}
</text>
</a>
</g>
<!--Tag information-->
<g
transform="translate(5, 55)"
v-if="deployment.tag">
<icon
name="label"
:width="12"
:height="12"
:y="5">
</icon>
<a :xlink:href="deployment.tagUrl">
<text
class="deploy-info-text deploy-info-text-link"
transform="translate(20, 2)"
y="2">
{{deployment.tag}}
</text>
</a>
</g>
</svg> </svg>
</g> </g>
<svg <svg
......
...@@ -33,7 +33,9 @@ const mixins = { ...@@ -33,7 +33,9 @@ const mixins = {
id: deployment.id, id: deployment.id,
time, time,
sha: deployment.sha, sha: deployment.sha,
commitUrl: `${this.projectPath}/commit/${deployment.sha}`,
tag: deployment.tag, tag: deployment.tag,
tagUrl: `${this.tagsPath}/${deployment.tag}`,
ref: deployment.ref.name, ref: deployment.ref.name,
xPos, xPos,
showDeploymentFlag: false, showDeploymentFlag: false,
......
import d3 from 'd3'; import d3 from 'd3';
export const dateFormat = d3.time.format('%b %-d, %Y'); export const dateFormat = d3.time.format('%b %-d, %Y');
export const dateFormatWithName = d3.time.format('%a, %b %-d');
export const timeFormat = d3.time.format('%-I:%M%p'); export const timeFormat = d3.time.format('%-I:%M%p');
export const bisectDate = d3.bisector(d => d.time).left; export const bisectDate = d3.bisector(d => d.time).left;
......
/* eslint-disable func-names, space-before-function-paren, no-var, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */
import Api from './api'; import Api from './api';
import './lib/utils/url_utility'; import { mergeUrlParams } from './lib/utils/url_utility';
export default class NamespaceSelect { export default class NamespaceSelect {
constructor(opts) { constructor(opts) {
...@@ -50,7 +50,7 @@ export default class NamespaceSelect { ...@@ -50,7 +50,7 @@ export default class NamespaceSelect {
} }
}, },
url(namespace) { url(namespace) {
return gl.utils.mergeUrlParams({ [fieldName]: namespace.id }, window.location.href); return mergeUrlParams({ [fieldName]: namespace.id }, window.location.href);
}, },
}); });
} }
......
...@@ -16,6 +16,7 @@ import Autosize from 'autosize'; ...@@ -16,6 +16,7 @@ import Autosize from 'autosize';
import 'vendor/jquery.caret'; // required by jquery.atwho import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho'; import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache'; import AjaxCache from '~/lib/utils/ajax_cache';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash'; import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle'; import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form'; import GLForm from './gl_form';
...@@ -331,7 +332,7 @@ export default class Notes { ...@@ -331,7 +332,7 @@ export default class Notes {
} }
static updateNoteTargetSelector($note) { static updateNoteTargetSelector($note) {
const hash = gl.utils.getLocationHash(); const hash = getLocationHash();
// Needs to be an explicit true/false for the jQuery `toggleClass(force)` // Needs to be an explicit true/false for the jQuery `toggleClass(force)`
const addTargetClass = Boolean(hash && $note.filter(`#${hash}`).length > 0); const addTargetClass = Boolean(hash && $note.filter(`#${hash}`).length > 0);
$note.toggleClass('target', addTargetClass); $note.toggleClass('target', addTargetClass);
......
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
import Flash from '../../flash'; import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue'; import noteHeader from './note_header.vue';
...@@ -85,7 +86,7 @@ ...@@ -85,7 +86,7 @@
}; };
this.isRequesting = true; this.isRequesting = true;
this.oldContent = this.note.note_html; this.oldContent = this.note.note_html;
this.note.note_html = noteText; this.note.note_html = escape(noteText);
this.updateNote(data) this.updateNote(data)
.then(() => { .then(() => {
......
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility';
import Flash from '../../flash'; import Flash from '../../flash';
import store from '../stores/'; import store from '../stores/';
import * as constants from '../constants'; import * as constants from '../constants';
...@@ -95,7 +96,7 @@ ...@@ -95,7 +96,7 @@
this.poll(); this.poll();
}, },
checkLocationHash() { checkLocationHash() {
const hash = gl.utils.getLocationHash(); const hash = getLocationHash();
const element = document.getElementById(hash); const element = document.getElementById(hash);
if (hash && element) { if (hash && element) {
......
...@@ -8,10 +8,18 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -8,10 +8,18 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
}, },
data() { data() {
const notesDataset = document.getElementById('js-vue-notes').dataset; const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
const currentUserData = parsedUserData ? {
id: parsedUserData.id,
name: parsedUserData.name,
username: parsedUserData.username,
avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
path: parsedUserData.path,
} : {};
return { return {
noteableData: JSON.parse(notesDataset.noteableData), noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData), currentUserData,
notesData: { notesData: {
lastFetchedAt: notesDataset.lastFetchedAt, lastFetchedAt: notesDataset.lastFetchedAt,
discussionsPath: notesDataset.discussionsPath, discussionsPath: notesDataset.discussionsPath,
......
import { getParameterByName } from '~/lib/utils/common_utils'; import { getParameterByName } from '~/lib/utils/common_utils';
import '~/lib/utils/url_utility'; import { removeParams } from './lib/utils/url_utility';
(() => { (() => {
const ENDLESS_SCROLL_BOTTOM_PX = 400; const ENDLESS_SCROLL_BOTTOM_PX = 400;
...@@ -7,7 +7,7 @@ import '~/lib/utils/url_utility'; ...@@ -7,7 +7,7 @@ import '~/lib/utils/url_utility';
const Pager = { const Pager = {
init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) { init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) {
this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']); this.url = $('.content_list').data('href') || removeParams(['limit', 'offset']);
this.limit = limit; this.limit = limit;
this.offset = parseInt(getParameterByName('offset'), 10) || this.limit; this.offset = parseInt(getParameterByName('offset'), 10) || this.limit;
this.disable = disable; this.disable = disable;
......
import 'vendor/peek'; import 'vendor/peek';
import 'vendor/peek.performance_bar'; import 'vendor/peek.performance_bar';
import { getParameterValues } from './lib/utils/url_utility';
export default class PerformanceBar { export default class PerformanceBar {
constructor(opts) { constructor(opts) {
...@@ -39,7 +40,7 @@ export default class PerformanceBar { ...@@ -39,7 +40,7 @@ export default class PerformanceBar {
} }
handleLineProfileLink(e) { handleLineProfileLink(e) {
const lineProfilerParameter = gl.utils.getParameterValues('lineprofiler'); const lineProfilerParameter = getParameterValues('lineprofiler');
const lineProfilerParameterRegex = new RegExp(`lineprofiler=${lineProfilerParameter[0]}`); const lineProfilerParameterRegex = new RegExp(`lineprofiler=${lineProfilerParameter[0]}`);
const shouldToggleModal = lineProfilerParameter.length > 0 && const shouldToggleModal = lineProfilerParameter.length > 0 &&
lineProfilerParameterRegex.test(e.currentTarget.href); lineProfilerParameterRegex.test(e.currentTarget.href);
......
/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { visitUrl } from './lib/utils/url_utility';
import projectSelect from './project_select'; import projectSelect from './project_select';
export default class Project { export default class Project {
...@@ -122,7 +123,7 @@ export default class Project { ...@@ -122,7 +123,7 @@ export default class Project {
var action = $form.attr('action'); var action = $form.attr('action');
var divider = action.indexOf('?') === -1 ? '?' : '&'; var divider = action.indexOf('?') === -1 ? '?' : '&';
if (shouldVisit) { if (shouldVisit) {
gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`); visitUrl(`${action}${divider}${$form.serialize()}`);
} }
} }
}, },
......
function updateAutoDevopsRadios(radioWrappers) {
radioWrappers.forEach((radioWrapper) => {
const radio = radioWrapper.querySelector('.js-auto-devops-enable-radio');
const runPipelineCheckboxWrapper = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox-wrapper');
const runPipelineCheckbox = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox');
if (runPipelineCheckbox) {
runPipelineCheckbox.checked = radio.checked;
runPipelineCheckboxWrapper.classList.toggle('hide', !radio.checked);
}
});
}
export default function initCiCdSettings() {
const radioWrappers = document.querySelectorAll('.js-auto-devops-enable-radio-wrapper');
radioWrappers.forEach(radioWrapper =>
radioWrapper.addEventListener('change', () => updateAutoDevopsRadios(radioWrappers)),
);
}
import '../lib/utils/url_utility'; import { getParameterValues } from '../lib/utils/url_utility';
const bindEvents = () => { const bindEvents = () => {
const path = gl.utils.getParameterValues('path')[0]; const path = getParameterValues('path')[0];
// get the path url and append it in the inputS // get the path url and append it in the inputS
$('.js-path-name').val(path); $('.js-path-name').val(path);
......
import Vue from 'vue'; import Vue from 'vue';
import { visitUrl } from '../../lib/utils/url_utility';
import flash from '../../flash'; import flash from '../../flash';
import service from '../services'; import service from '../services';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const redirectToUrl = (_, url) => gl.utils.visitUrl(url); export const redirectToUrl = (_, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
......
import { visitUrl } from '../../../lib/utils/url_utility';
import { normalizeHeaders } from '../../../lib/utils/common_utils'; import { normalizeHeaders } from '../../../lib/utils/common_utils';
import flash from '../../../flash'; import flash from '../../../flash';
import service from '../../services'; import service from '../../services';
...@@ -73,7 +74,7 @@ export const clickedTreeRow = ({ commit, dispatch }, row) => { ...@@ -73,7 +74,7 @@ export const clickedTreeRow = ({ commit, dispatch }, row) => {
} else if (row.type === 'submodule') { } else if (row.type === 'submodule') {
commit(types.TOGGLE_LOADING, row); commit(types.TOGGLE_LOADING, row);
gl.utils.visitUrl(row.url); visitUrl(row.url);
} else if (row.type === 'blob' && row.opened) { } else if (row.type === 'blob' && row.opened) {
dispatch('setFileActive', row); dispatch('setFileActive', row);
} else { } else {
......
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import { refreshCurrentPage, visitUrl } from './lib/utils/url_utility';
import findAndFollowLink from './shortcuts_dashboard_navigation'; import findAndFollowLink from './shortcuts_dashboard_navigation';
const defaultStopCallback = Mousetrap.stopCallback; const defaultStopCallback = Mousetrap.stopCallback;
...@@ -38,7 +39,7 @@ export default class Shortcuts { ...@@ -38,7 +39,7 @@ export default class Shortcuts {
if (typeof findFileURL !== 'undefined' && findFileURL !== null) { if (typeof findFileURL !== 'undefined' && findFileURL !== null) {
Mousetrap.bind('t', () => { Mousetrap.bind('t', () => {
gl.utils.visitUrl(findFileURL); visitUrl(findFileURL);
}); });
} }
...@@ -62,7 +63,7 @@ export default class Shortcuts { ...@@ -62,7 +63,7 @@ export default class Shortcuts {
} else { } else {
Cookies.set(performanceBarCookieName, 'true', { path: '/' }); Cookies.set(performanceBarCookieName, 'true', { path: '/' });
} }
gl.utils.refreshCurrentPage(); refreshCurrentPage();
} }
static toggleMarkdownPreview(e) { static toggleMarkdownPreview(e) {
......
/* global Mousetrap */ /* global Mousetrap */
import { getLocationHash, visitUrl } from './lib/utils/url_utility';
import Shortcuts from './shortcuts'; import Shortcuts from './shortcuts';
const defaults = { const defaults = {
...@@ -18,9 +18,9 @@ export default class ShortcutsBlob extends Shortcuts { ...@@ -18,9 +18,9 @@ export default class ShortcutsBlob extends Shortcuts {
moveToFilePermalink() { moveToFilePermalink() {
if (this.options.fileBlobPermalinkUrl) { if (this.options.fileBlobPermalinkUrl) {
const hash = gl.utils.getLocationHash(); const hash = getLocationHash();
const hashUrlString = hash ? `#${hash}` : ''; const hashUrlString = hash ? `#${hash}` : '';
gl.utils.visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`); visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`);
} }
} }
} }
import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash'; import Flash from '../flash';
import Service from './services/sidebar_service'; import Service from './services/sidebar_service';
import Store from './stores/sidebar_store'; import Store from './stores/sidebar_store';
...@@ -81,7 +82,7 @@ export default class SidebarMediator { ...@@ -81,7 +82,7 @@ export default class SidebarMediator {
.then(response => response.json()) .then(response => response.json())
.then((data) => { .then((data) => {
if (location.pathname !== data.web_url) { if (location.pathname !== data.web_url) {
gl.utils.visitUrl(data.web_url); visitUrl(data.web_url);
} }
}); });
} }
......
/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */ /* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */
import { visitUrl } from './lib/utils/url_utility';
import UsersSelect from './users_select'; import UsersSelect from './users_select';
import { isMetaClick } from './lib/utils/common_utils'; import { isMetaClick } from './lib/utils/common_utils';
...@@ -150,7 +150,7 @@ export default class Todos { ...@@ -150,7 +150,7 @@ export default class Todos {
window.open(todoLink, windowTarget); window.open(todoLink, windowTarget);
} else { } else {
gl.utils.visitUrl(todoLink); visitUrl(todoLink);
} }
} }
} }
/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, class-methods-use-this */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, class-methods-use-this */
import { visitUrl } from './lib/utils/url_utility';
export default class TreeView { export default class TreeView {
constructor() { constructor() {
...@@ -14,7 +15,7 @@ export default class TreeView { ...@@ -14,7 +15,7 @@ export default class TreeView {
e.preventDefault(); e.preventDefault();
return window.open(path, '_blank'); return window.open(path, '_blank');
} else { } else {
return gl.utils.visitUrl(path); return visitUrl(path);
} }
} }
}); });
...@@ -56,7 +57,7 @@ export default class TreeView { ...@@ -56,7 +57,7 @@ export default class TreeView {
} else if (e.which === 13) { } else if (e.which === 13) {
path = $('.tree-item.selected .tree-item-file-name a').attr('href'); path = $('.tree-item.selected .tree-item-file-name a').attr('href');
if (path) { if (path) {
return gl.utils.visitUrl(path); return visitUrl(path);
} }
} }
}); });
......
import { getTimeago } from '~/lib/utils/datetime_utility'; import { getTimeago } from '~/lib/utils/datetime_utility';
import { visitUrl } from '../../lib/utils/url_utility';
import Flash from '../../flash'; import Flash from '../../flash';
import MemoryUsage from './mr_widget_memory_usage'; import MemoryUsage from './mr_widget_memory_usage';
import StatusIcon from './mr_widget_status_icon'; import StatusIcon from './mr_widget_status_icon';
...@@ -36,7 +37,7 @@ export default { ...@@ -36,7 +37,7 @@ export default {
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then((res) => {
if (res.redirect_url) { if (res.redirect_url) {
gl.utils.visitUrl(res.redirect_url); visitUrl(res.redirect_url);
} }
}) })
.catch(() => { .catch(() => {
......
import Project from '~/project';
import SmartInterval from '~/smart_interval'; import SmartInterval from '~/smart_interval';
import Flash from '../flash'; import Flash from '../flash';
import { import {
...@@ -140,6 +141,7 @@ export default { ...@@ -140,6 +141,7 @@ export default {
const el = document.createElement('div'); const el = document.createElement('div');
el.innerHTML = res.body; el.innerHTML = res.body;
document.body.appendChild(el); document.body.appendChild(el);
Project.initRefSwitcher();
} }
}) })
.catch(() => { .catch(() => {
......
...@@ -36,6 +36,30 @@ ...@@ -36,6 +36,30 @@
required: false, required: false,
default: '', default: '',
}, },
width: {
type: Number,
required: false,
default: null,
},
height: {
type: Number,
required: false,
default: null,
},
y: {
type: Number,
required: false,
default: null,
},
x: {
type: Number,
required: false,
default: null,
},
}, },
computed: { computed: {
...@@ -51,7 +75,11 @@ ...@@ -51,7 +75,11 @@
<template> <template>
<svg <svg
:class="[iconSizeClass, cssClasses]"> :class="[iconSizeClass, cssClasses]"
:width="width"
:height="height"
:x="x"
:y="y">
<use <use
v-bind="{'xlink:href':spriteHref}"/> v-bind="{'xlink:href':spriteHref}"/>
</svg> </svg>
......
...@@ -38,7 +38,8 @@ export default { ...@@ -38,7 +38,8 @@ export default {
}, },
primaryButtonLabel: { primaryButtonLabel: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
submitDisabled: { submitDisabled: {
type: Boolean, type: Boolean,
...@@ -113,8 +114,9 @@ export default { ...@@ -113,8 +114,9 @@ export default {
{{ closeButtonLabel }} {{ closeButtonLabel }}
</button> </button>
<button <button
v-if="primaryButtonLabel"
type="button" type="button"
class="btn pull-right" class="btn pull-right js-primary-button"
:disabled="submitDisabled" :disabled="submitDisabled"
:class="btnKindClass" :class="btnKindClass"
@click="emitSubmit(true)"> @click="emitSubmit(true)">
......
<script>
import PopupDialog from './popup_dialog.vue';
export default {
name: 'recaptcha-dialog',
props: {
html: {
type: String,
required: false,
default: '',
},
},
data() {
return {
script: {},
scriptSrc: 'https://www.google.com/recaptcha/api.js',
};
},
components: {
PopupDialog,
},
methods: {
appendRecaptchaScript() {
this.removeRecaptchaScript();
const script = document.createElement('script');
script.src = this.scriptSrc;
script.classList.add('js-recaptcha-script');
script.async = true;
script.defer = true;
this.script = script;
document.body.appendChild(script);
},
removeRecaptchaScript() {
if (this.script instanceof Element) this.script.remove();
},
close() {
this.removeRecaptchaScript();
this.$emit('close');
},
submit() {
this.$el.querySelector('form').submit();
},
},
watch: {
html() {
this.appendRecaptchaScript();
},
},
mounted() {
window.recaptchaDialogCallback = this.submit.bind(this);
},
};
</script>
<template>
<popup-dialog
kind="warning"
class="recaptcha-dialog js-recaptcha-dialog"
:hide-footer="true"
:title="__('Please solve the reCAPTCHA')"
@toggle="close"
>
<div slot="body">
<p>
{{__('We want to be sure it is you, please confirm you are not a robot.')}}
</p>
<div
ref="recaptcha"
v-html="html"
></div>
</div>
</popup-dialog>
</template>
import RecaptchaDialog from '../components/recaptcha_dialog.vue';
export default {
data() {
return {
showRecaptcha: false,
recaptchaHTML: '',
};
},
components: {
RecaptchaDialog,
},
methods: {
openRecaptcha() {
this.showRecaptcha = true;
},
closeRecaptcha() {
this.showRecaptcha = false;
},
checkForSpam(data) {
if (!data.recaptcha_html) return data;
this.recaptchaHTML = data.recaptcha_html;
const spamError = new Error(data.error_message);
spamError.name = 'SpamError';
spamError.message = 'SpamError';
throw spamError;
},
},
};
.page-with-contextual-sidebar { .page-with-contextual-sidebar {
transition: padding-left $sidebar-transition-duration;
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
padding-left: $contextual-sidebar-collapsed-width; padding-left: $contextual-sidebar-collapsed-width;
} }
...@@ -27,8 +29,10 @@ ...@@ -27,8 +29,10 @@
.context-header { .context-header {
position: relative; position: relative;
margin-right: 2px; margin-right: 2px;
width: $contextual-sidebar-width;
a { a {
transition: padding $sidebar-transition-duration;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -63,10 +67,10 @@ ...@@ -63,10 +67,10 @@
} }
.nav-sidebar { .nav-sidebar {
transition: width $sidebar-transition-duration, left $sidebar-transition-duration;
position: fixed; position: fixed;
z-index: 400; z-index: 400;
width: $contextual-sidebar-width; width: $contextual-sidebar-width;
transition: left $sidebar-transition-duration;
top: $header-height; top: $header-height;
bottom: 0; bottom: 0;
left: 0; left: 0;
...@@ -74,16 +78,15 @@ ...@@ -74,16 +78,15 @@
box-shadow: inset -2px 0 0 $border-color; box-shadow: inset -2px 0 0 $border-color;
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
&:not(.sidebar-icons-only) { &:not(.sidebar-collapsed-desktop) {
@media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
box-shadow: inset -2px 0 0 $border-color, box-shadow: inset -2px 0 0 $border-color,
2px 1px 3px $dropdown-shadow-color; 2px 1px 3px $dropdown-shadow-color;
} }
} }
&.sidebar-icons-only { &.sidebar-collapsed-desktop {
width: auto; width: $contextual-sidebar-collapsed-width;
min-width: $contextual-sidebar-collapsed-width;
.nav-sidebar-inner-scroll { .nav-sidebar-inner-scroll {
overflow-x: hidden; overflow-x: hidden;
...@@ -108,12 +111,11 @@ ...@@ -108,12 +111,11 @@
} }
} }
&.nav-sidebar-expanded { &.sidebar-expanded-mobile {
left: 0; left: 0;
} }
a { a {
transition: none;
text-decoration: none; text-decoration: none;
} }
...@@ -126,9 +128,10 @@ ...@@ -126,9 +128,10 @@
white-space: nowrap; white-space: nowrap;
a { a {
transition: padding $sidebar-transition-duration;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 12px 16px; padding: 12px 15px;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
...@@ -288,7 +291,8 @@ ...@@ -288,7 +291,8 @@
> a { > a {
margin-left: 4px; margin-left: 4px;
padding-left: 12px; // Subtract width of left border on active element
padding-left: 11px;
} }
.badge { .badge {
...@@ -313,6 +317,7 @@ ...@@ -313,6 +317,7 @@
.toggle-sidebar-button, .toggle-sidebar-button,
.close-nav-button { .close-nav-button {
width: $contextual-sidebar-width - 2px; width: $contextual-sidebar-width - 2px;
transition: width $sidebar-transition-duration;
position: fixed; position: fixed;
bottom: 0; bottom: 0;
padding: 16px; padding: 16px;
...@@ -343,20 +348,21 @@ ...@@ -343,20 +348,21 @@
} }
} }
.collapse-text {
white-space: nowrap;
overflow: hidden;
}
.sidebar-icons-only { .sidebar-collapsed-desktop {
.context-header { .context-header {
height: 61px; height: 60px;
width: $contextual-sidebar-collapsed-width;
a { a {
padding: 10px 4px; padding: 10px 4px;
} }
} }
li a {
padding: 12px 15px;
}
.sidebar-top-level-items > li { .sidebar-top-level-items > li {
&.active a { &.active a {
padding-left: 12px; padding-left: 12px;
...@@ -374,8 +380,8 @@ ...@@ -374,8 +380,8 @@
} }
.toggle-sidebar-button { .toggle-sidebar-button {
width: $contextual-sidebar-collapsed-width - 2px;
padding: 16px; padding: 16px;
width: $contextual-sidebar-collapsed-width - 2px;
.collapse-text, .collapse-text,
.icon-angle-double-left { .icon-angle-double-left {
......
...@@ -48,3 +48,10 @@ body.modal-open { ...@@ -48,3 +48,10 @@ body.modal-open {
display: block; display: block;
} }
.recaptcha-dialog .recaptcha-form {
display: inline-block;
.recaptcha {
margin: 0;
}
}
...@@ -5,10 +5,9 @@ $grid-size: 8px; ...@@ -5,10 +5,9 @@ $grid-size: 8px;
$gutter_collapsed_width: 62px; $gutter_collapsed_width: 62px;
$gutter_width: 290px; $gutter_width: 290px;
$gutter_inner_width: 250px; $gutter_inner_width: 250px;
$sidebar-transition-duration: .15s; $sidebar-transition-duration: .3s;
$sidebar-breakpoint: 1024px; $sidebar-breakpoint: 1024px;
$default-transition-duration: .15s; $default-transition-duration: .15s;
$right-sidebar-transition-duration: .3s;
$contextual-sidebar-width: 220px; $contextual-sidebar-width: 220px;
$contextual-sidebar-collapsed-width: 50px; $contextual-sidebar-collapsed-width: 50px;
......
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
position: relative; position: relative;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
transition: width $right-sidebar-transition-duration; transition: width $sidebar-transition-duration;
width: 100%; width: 100%;
&.is-compact { &.is-compact {
...@@ -453,8 +453,8 @@ ...@@ -453,8 +453,8 @@
.right-sidebar.right-sidebar-expanded { .right-sidebar.right-sidebar-expanded {
&.boards-sidebar-slide-enter-active, &.boards-sidebar-slide-enter-active,
&.boards-sidebar-slide-leave-active { &.boards-sidebar-slide-leave-active {
transition: width $right-sidebar-transition-duration, transition: width $sidebar-transition-duration,
padding $right-sidebar-transition-duration; padding $sidebar-transition-duration;
} }
&.boards-sidebar-slide-enter, &.boards-sidebar-slide-enter,
......
...@@ -201,8 +201,9 @@ ...@@ -201,8 +201,9 @@
stroke-width: 1; stroke-width: 1;
} }
.deploy-info-text { .divider-line {
dominant-baseline: text-before-edge; stroke-width: 1;
stroke: $gray-darkest;
} }
.prometheus-state { .prometheus-state {
...@@ -312,6 +313,20 @@ ...@@ -312,6 +313,20 @@
stroke: $gray-darker; stroke: $gray-darker;
} }
.deploy-info-text {
dominant-baseline: text-before-edge;
font-size: 12px;
}
.deploy-info-text-link {
font-family: $monospace_font;
fill: $gl-link-color;
&:hover {
fill: $gl-link-hover-color;
}
}
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
.label-axis-text, .label-axis-text,
.text-metric-usage, .text-metric-usage,
......
...@@ -126,7 +126,7 @@ ...@@ -126,7 +126,7 @@
top: $header-height; top: $header-height;
bottom: 0; bottom: 0;
right: 0; right: 0;
transition: width $right-sidebar-transition-duration; transition: width $sidebar-transition-duration;
background: $gray-light; background: $gray-light;
z-index: 200; z-index: 200;
overflow: hidden; overflow: hidden;
......
...@@ -5,7 +5,7 @@ class Admin::HealthCheckController < Admin::ApplicationController ...@@ -5,7 +5,7 @@ class Admin::HealthCheckController < Admin::ApplicationController
end end
def reset_storage_health def reset_storage_health
Gitlab::Git::Storage::CircuitBreaker.reset_all! Gitlab::Git::Storage::FailureInfo.reset_all!
redirect_to admin_health_check_path, redirect_to admin_health_check_path,
notice: _('Git storage health information has been reset') notice: _('Git storage health information has been reset')
end end
......
...@@ -21,11 +21,11 @@ module IssuableActions ...@@ -21,11 +21,11 @@ module IssuableActions
respond_to do |format| respond_to do |format|
format.html do format.html do
recaptcha_check_with_fallback { render :edit } recaptcha_check_if_spammable { render :edit }
end end
format.json do format.json do
render_entity_json recaptcha_check_if_spammable(false) { render_entity_json }
end end
end end
...@@ -80,6 +80,12 @@ module IssuableActions ...@@ -80,6 +80,12 @@ module IssuableActions
private private
def recaptcha_check_if_spammable(should_redirect = true, &block)
return yield unless @issuable.is_a? Spammable
recaptcha_check_with_fallback(should_redirect, &block)
end
def render_conflict_response def render_conflict_response
respond_to do |format| respond_to do |format|
format.html do format.html do
......
...@@ -23,8 +23,8 @@ module SpammableActions ...@@ -23,8 +23,8 @@ module SpammableActions
@spam_config_loaded = Gitlab::Recaptcha.load_configurations! @spam_config_loaded = Gitlab::Recaptcha.load_configurations!
end end
def recaptcha_check_with_fallback(&fallback) def recaptcha_check_with_fallback(should_redirect = true, &fallback)
if spammable.valid? if should_redirect && spammable.valid?
redirect_to spammable_path redirect_to spammable_path
elsif render_recaptcha? elsif render_recaptcha?
ensure_spam_config_loaded! ensure_spam_config_loaded!
...@@ -33,7 +33,18 @@ module SpammableActions ...@@ -33,7 +33,18 @@ module SpammableActions
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end end
render :verify respond_to do |format|
format.html do
render :verify
end
format.json do
locals = { spammable: spammable, script: false, has_submit: false }
recaptcha_html = render_to_string(partial: 'shared/recaptcha_form', formats: :html, locals: locals)
render json: { recaptcha_html: recaptcha_html }
end
end
else else
yield yield
end end
......
...@@ -22,7 +22,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -22,7 +22,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end end
def update def update
@group_member = @group.group_members.find(params[:id]) @group_member = @group.members_and_requesters.find(params[:id])
return render_403 unless can?(current_user, :update_group_member, @group_member) return render_403 unless can?(current_user, :update_group_member, @group_member)
......
class HealthController < ActionController::Base class HealthController < ActionController::Base
protect_from_forgery with: :exception protect_from_forgery with: :exception, except: :storage_check
include RequiresWhitelistedMonitoringClient include RequiresWhitelistedMonitoringClient
CHECKS = [ CHECKS = [
...@@ -23,6 +23,15 @@ class HealthController < ActionController::Base ...@@ -23,6 +23,15 @@ class HealthController < ActionController::Base
render_check_results(results) render_check_results(results)
end end
def storage_check
results = Gitlab::Git::Storage::Checker.check_all
render json: {
check_interval: Gitlab::CurrentSettings.current_application_settings.circuitbreaker_check_interval,
results: results
}
end
private private
def render_check_results(results) def render_check_results(results)
......
...@@ -8,7 +8,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController ...@@ -8,7 +8,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
@personal_access_token = finder.build(personal_access_token_params) @personal_access_token = finder.build(personal_access_token_params)
if @personal_access_token.save if @personal_access_token.save
flash[:personal_access_token] = @personal_access_token.token PersonalAccessToken.redis_store!(current_user.id, @personal_access_token.token)
redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created." redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
else else
set_index_vars set_index_vars
...@@ -43,5 +43,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController ...@@ -43,5 +43,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
@inactive_personal_access_tokens = finder(state: 'inactive').execute @inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at) @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
@new_personal_access_token = PersonalAccessToken.redis_getdel(current_user.id)
end end
end end
...@@ -134,6 +134,23 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -134,6 +134,23 @@ class Projects::CommitController < Projects::ApplicationController
@grouped_diff_discussions = commit.grouped_diff_discussions @grouped_diff_discussions = commit.grouped_diff_discussions
@discussions = commit.discussions @discussions = commit.discussions
if merge_request_iid = params[:merge_request_iid]
@merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: merge_request_iid)
if @merge_request
@new_diff_note_attrs.merge!(
noteable_type: 'MergeRequest',
noteable_id: @merge_request.id
)
merge_request_commit_notes = @merge_request.notes.where(commit_id: @commit.id).inc_relations_for_view
merge_request_commit_diff_discussions = merge_request_commit_notes.grouped_diff_discussions(@commit.diff_refs)
@grouped_diff_discussions.merge!(merge_request_commit_diff_discussions) do |line_code, left, right|
left + right
end
end
end
@notes = (@grouped_diff_discussions.values.flatten + @discussions).flat_map(&:notes) @notes = (@grouped_diff_discussions.values.flatten + @discussions).flat_map(&:notes)
@notes = prepare_notes_for_rendering(@notes, @commit) @notes = prepare_notes_for_rendering(@notes, @commit)
end end
......
...@@ -28,7 +28,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont ...@@ -28,7 +28,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:task_num, :task_num,
:title, :title,
:discussion_locked, :discussion_locked,
label_ids: [] label_ids: []
] ]
end end
......
...@@ -4,6 +4,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic ...@@ -4,6 +4,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
include RendersNotes include RendersNotes
before_action :apply_diff_view_cookie! before_action :apply_diff_view_cookie!
before_action :commit
before_action :define_diff_vars before_action :define_diff_vars
before_action :define_diff_comment_vars before_action :define_diff_comment_vars
...@@ -20,18 +21,33 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic ...@@ -20,18 +21,33 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
private private
def define_diff_vars def define_diff_vars
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc
@compare = commit || find_merge_request_diff_compare
return render_404 unless @compare
@diffs = @compare.diffs(diff_options)
end
def commit
return nil unless commit_id = params[:commit_id].presence
return nil unless @merge_request.all_commits.exists?(sha: commit_id)
@commit ||= @project.commit(commit_id)
end
def find_merge_request_diff_compare
@merge_request_diff = @merge_request_diff =
if params[:diff_id] if diff_id = params[:diff_id].presence
@merge_request.merge_request_diffs.viewable.find(params[:diff_id]) @merge_request.merge_request_diffs.viewable.find_by(id: diff_id)
else else
@merge_request.merge_request_diff @merge_request.merge_request_diff
end end
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc return unless @merge_request_diff
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present? if @start_sha = params[:start_sha].presence
@start_sha = params[:start_sha]
@start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha } @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
unless @start_version unless @start_version
...@@ -40,20 +56,18 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic ...@@ -40,20 +56,18 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
end end
end end
@compare = if @start_sha
if @start_sha @merge_request_diff.compare_with(@start_sha)
@merge_request_diff.compare_with(@start_sha) else
else @merge_request_diff
@merge_request_diff end
end
@diffs = @compare.diffs(diff_options)
end end
def define_diff_comment_vars def define_diff_comment_vars
@new_diff_note_attrs = { @new_diff_note_attrs = {
noteable_type: 'MergeRequest', noteable_type: 'MergeRequest',
noteable_id: @merge_request.id noteable_id: @merge_request.id,
commit_id: @commit&.id
} }
@diff_notes_disabled = false @diff_notes_disabled = false
......
...@@ -7,11 +7,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -7,11 +7,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include IssuableCollections include IssuableCollections
skip_before_action :merge_request, only: [:index, :bulk_update] skip_before_action :merge_request, only: [:index, :bulk_update]
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort] before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :set_issuables_index, only: [:index] before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues] before_action :authenticate_user!, only: [:assign_related_issues]
def index def index
......
...@@ -29,7 +29,6 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -29,7 +29,6 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
:runners_token, :builds_enabled, :build_allow_git_fetch, :runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_in_minutes, :build_coverage_regex, :public_builds, :build_timeout_in_minutes, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path, :auto_cancel_pending_pipelines, :ci_config_path,
:run_auto_devops_pipeline_implicit, :run_auto_devops_pipeline_explicit,
auto_devops_attributes: [:id, :domain, :enabled] auto_devops_attributes: [:id, :domain, :enabled]
) )
end end
......
...@@ -26,7 +26,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -26,7 +26,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
def update def update
@project_member = @project.project_members.find(params[:id]) @project_member = @project.members_and_requesters.find(params[:id])
return render_403 unless can?(current_user, :update_project_member, @project_member) return render_403 unless can?(current_user, :update_project_member, @project_member)
......
...@@ -272,7 +272,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -272,7 +272,7 @@ class ProjectsController < Projects::ApplicationController
render 'projects/empty' if @project.empty_repo? render 'projects/empty' if @project.empty_repo?
else else
if @project.wiki_enabled? if can?(current_user, :read_wiki, @project)
@project_wiki = @project.wiki @project_wiki = @project.wiki
@wiki_home = @project_wiki.find_page('home', params[:version_id]) @wiki_home = @project_wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user) elsif @project.feature_available?(:issues, current_user)
......
...@@ -124,17 +124,6 @@ module ApplicationSettingsHelper ...@@ -124,17 +124,6 @@ module ApplicationSettingsHelper
_('The number of attempts GitLab will make to access a storage.') _('The number of attempts GitLab will make to access a storage.')
end end
def circuitbreaker_backoff_threshold_help_text
_("The number of failures after which GitLab will start temporarily "\
"disabling access to a storage shard on a host")
end
def circuitbreaker_failure_wait_time_help_text
_("When access to a storage fails. GitLab will prevent access to the "\
"storage for the time specified here. This allows the filesystem to "\
"recover. Repositories on failing shards are temporarly unavailable")
end
def circuitbreaker_failure_reset_time_help_text def circuitbreaker_failure_reset_time_help_text
_("The time in seconds GitLab will keep failure information. When no "\ _("The time in seconds GitLab will keep failure information. When no "\
"failures occur during this time, information about the mount is reset.") "failures occur during this time, information about the mount is reset.")
...@@ -145,6 +134,11 @@ module ApplicationSettingsHelper ...@@ -145,6 +134,11 @@ module ApplicationSettingsHelper
"timeout error will be raised.") "timeout error will be raised.")
end end
def circuitbreaker_check_interval_help_text
_("The time in seconds between storage checks. When a previous check did "\
"complete yet, GitLab will skip a check.")
end
def visible_attributes def visible_attributes
[ [
:admin_notification_email, :admin_notification_email,
...@@ -154,10 +148,9 @@ module ApplicationSettingsHelper ...@@ -154,10 +148,9 @@ module ApplicationSettingsHelper
:akismet_enabled, :akismet_enabled,
:auto_devops_enabled, :auto_devops_enabled,
:circuitbreaker_access_retries, :circuitbreaker_access_retries,
:circuitbreaker_backoff_threshold, :circuitbreaker_check_interval,
:circuitbreaker_failure_count_threshold, :circuitbreaker_failure_count_threshold,
:circuitbreaker_failure_reset_time, :circuitbreaker_failure_reset_time,
:circuitbreaker_failure_wait_time,
:circuitbreaker_storage_timeout, :circuitbreaker_storage_timeout,
:clientside_sentry_dsn, :clientside_sentry_dsn,
:clientside_sentry_enabled, :clientside_sentry_enabled,
......
...@@ -8,22 +8,6 @@ module AutoDevopsHelper ...@@ -8,22 +8,6 @@ module AutoDevopsHelper
!project.ci_service !project.ci_service
end end
def show_run_auto_devops_pipeline_checkbox_for_instance_setting?(project)
return false if project.repository.gitlab_ci_yml
if project&.auto_devops&.enabled.present?
!project.auto_devops.enabled && current_application_settings.auto_devops_enabled?
else
current_application_settings.auto_devops_enabled?
end
end
def show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(project)
return false if project.repository.gitlab_ci_yml
!project.auto_devops_enabled?
end
def auto_devops_warning_message(project) def auto_devops_warning_message(project)
missing_domain = !project.auto_devops&.has_domain? missing_domain = !project.auto_devops&.has_domain?
missing_service = !project.deployment_platform&.active? missing_service = !project.deployment_platform&.active?
......
...@@ -20,8 +20,7 @@ module BuildsHelper ...@@ -20,8 +20,7 @@ module BuildsHelper
def javascript_build_options def javascript_build_options
{ {
page_url: project_job_url(@project, @build), page_path: project_job_path(@project, @build),
build_url: project_job_url(@project, @build, :json),
build_status: @build.status, build_status: @build.status,
build_stage: @build.stage, build_stage: @build.stage,
log_state: '' log_state: ''
......
...@@ -228,4 +228,12 @@ module CommitsHelper ...@@ -228,4 +228,12 @@ module CommitsHelper
[commits, 0] [commits, 0]
end end
end end
def commit_path(project, commit, merge_request: nil)
if merge_request&.persisted?
diffs_project_merge_request_path(project, merge_request, commit_id: commit.id)
else
project_commit_path(project, commit)
end
end
end end
...@@ -101,6 +101,30 @@ module MergeRequestsHelper ...@@ -101,6 +101,30 @@ module MergeRequestsHelper
}.merge(merge_params_ee(merge_request)) }.merge(merge_params_ee(merge_request))
end end
def tab_link_for(merge_request, tab, options = {}, &block)
data_attrs = {
action: tab.to_s,
target: "##{tab}",
toggle: options.fetch(:force_link, false) ? '' : 'tab'
}
url = case tab
when :show
data_attrs[:target] = '#notes'
method(:project_merge_request_path)
when :commits
method(:commits_project_merge_request_path)
when :pipelines
method(:pipelines_project_merge_request_path)
when :diffs
method(:diffs_project_merge_request_path)
else
raise "Cannot create tab #{tab}."
end
link_to(url[merge_request.project, merge_request], data: data_attrs, &block)
end
def merge_params_ee(merge_request) def merge_params_ee(merge_request)
{} {}
end end
......
...@@ -58,7 +58,7 @@ module PreferencesHelper ...@@ -58,7 +58,7 @@ module PreferencesHelper
user_view user_view
elsif user_view == "activity" elsif user_view == "activity"
"activity" "activity"
elsif @project.wiki_enabled? elsif can?(current_user, :read_wiki, @project)
"wiki" "wiki"
elsif @project.feature_available?(:issues, current_user) elsif @project.feature_available?(:issues, current_user)
"projects/issues/issues" "projects/issues/issues"
......
...@@ -18,16 +18,12 @@ module StorageHealthHelper ...@@ -18,16 +18,12 @@ module StorageHealthHelper
current_failures = circuit_breaker.failure_count current_failures = circuit_breaker.failure_count
translation_params = { number_of_failures: current_failures, translation_params = { number_of_failures: current_failures,
maximum_failures: maximum_failures, maximum_failures: maximum_failures }
number_of_seconds: circuit_breaker.failure_wait_time }
if circuit_breaker.circuit_broken? if circuit_breaker.circuit_broken?
s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\ s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\
"retry automatically. Reset storage information when the problem is "\ "retry automatically. Reset storage information when the problem is "\
"resolved.") % translation_params "resolved.") % translation_params
elsif circuit_breaker.backing_off?
_("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\
"block access for %{number_of_seconds} seconds.") % translation_params
else else
_("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\ _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\
"allow access on the next attempt.") % translation_params "allow access on the next attempt.") % translation_params
......
...@@ -153,11 +153,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -153,11 +153,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true, presence: true,
numericality: { greater_than_or_equal_to: 0 } numericality: { greater_than_or_equal_to: 0 }
validates :circuitbreaker_backoff_threshold, validates :circuitbreaker_failure_count_threshold,
:circuitbreaker_failure_count_threshold,
:circuitbreaker_failure_wait_time,
:circuitbreaker_failure_reset_time, :circuitbreaker_failure_reset_time,
:circuitbreaker_storage_timeout, :circuitbreaker_storage_timeout,
:circuitbreaker_check_interval,
presence: true, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
...@@ -165,13 +164,6 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -165,13 +164,6 @@ class ApplicationSetting < ActiveRecord::Base
presence: true, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 1 } numericality: { only_integer: true, greater_than_or_equal_to: 1 }
validates_each :circuitbreaker_backoff_threshold do |record, attr, value|
if value.to_i >= record.circuitbreaker_failure_count_threshold
record.errors.add(attr, _("The circuitbreaker backoff threshold should be "\
"lower than the failure count threshold"))
end
end
validates :gitaly_timeout_default, validates :gitaly_timeout_default,
presence: true, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
......
...@@ -6,6 +6,8 @@ module Ci ...@@ -6,6 +6,8 @@ module Ci
include Presentable include Presentable
include Importable include Importable
MissingDependenciesError = Class.new(StandardError)
belongs_to :runner belongs_to :runner
belongs_to :trigger_request belongs_to :trigger_request
belongs_to :erased_by, class_name: 'User' belongs_to :erased_by, class_name: 'User'
...@@ -139,6 +141,10 @@ module Ci ...@@ -139,6 +141,10 @@ module Ci
Ci::Build.retry(build, build.user) Ci::Build.retry(build, build.user)
end end
end end
before_transition any => [:running] do |build|
build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
end
end end
def detailed_status(current_user) def detailed_status(current_user)
...@@ -478,6 +484,20 @@ module Ci ...@@ -478,6 +484,20 @@ module Ci
options[:dependencies]&.empty? options[:dependencies]&.empty?
end end
def validates_dependencies!
dependencies.each do |dependency|
raise MissingDependenciesError unless dependency.valid_dependency?
end
end
def valid_dependency?
return false unless complete?
return false if artifacts_expired?
return false if erased?
true
end
def hide_secrets(trace) def hide_secrets(trace)
return unless trace return unless trace
......
# coding: utf-8
class Commit class Commit
extend ActiveModel::Naming extend ActiveModel::Naming
extend Gitlab::Cache::RequestCache extend Gitlab::Cache::RequestCache
...@@ -25,7 +26,7 @@ class Commit ...@@ -25,7 +26,7 @@ class Commit
DIFF_HARD_LIMIT_FILES = 1000 DIFF_HARD_LIMIT_FILES = 1000
DIFF_HARD_LIMIT_LINES = 50000 DIFF_HARD_LIMIT_LINES = 50000
MIN_SHA_LENGTH = 7 MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH
COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze
def banzai_render_context(field) def banzai_render_context(field)
......
...@@ -43,7 +43,8 @@ class CommitStatus < ActiveRecord::Base ...@@ -43,7 +43,8 @@ class CommitStatus < ActiveRecord::Base
script_failure: 1, script_failure: 1,
api_failure: 2, api_failure: 2,
stuck_or_timeout_failure: 3, stuck_or_timeout_failure: 3,
runner_system_failure: 4 runner_system_failure: 4,
missing_dependency_failure: 5
} }
## ##
......
...@@ -32,6 +32,10 @@ module DiscussionOnDiff ...@@ -32,6 +32,10 @@ module DiscussionOnDiff
first_note.position.new_path first_note.position.new_path
end end
def on_merge_request_commit?
for_merge_request? && commit_id.present?
end
# Returns an array of at most 16 highlighted lines above a diff note # Returns an array of at most 16 highlighted lines above a diff note
def truncated_diff_lines(highlight: true) def truncated_diff_lines(highlight: true)
lines = highlight ? highlighted_diff_lines : diff_lines lines = highlight ? highlighted_diff_lines : diff_lines
......
...@@ -24,7 +24,11 @@ class DiffDiscussion < Discussion ...@@ -24,7 +24,11 @@ class DiffDiscussion < Discussion
return unless for_merge_request? return unless for_merge_request?
return {} if active? return {} if active?
noteable.version_params_for(position.diff_refs) if on_merge_request_commit?
{ commit_id: commit_id }
else
noteable.version_params_for(position.diff_refs)
end
end end
def reply_attributes def reply_attributes
......
...@@ -17,6 +17,7 @@ class DiffNote < Note ...@@ -17,6 +17,7 @@ class DiffNote < Note
validates :noteable_type, inclusion: { in: NOTEABLE_TYPES } validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
validate :positions_complete validate :positions_complete
validate :verify_supported validate :verify_supported
validate :diff_refs_match_commit, if: :for_commit?
before_validation :set_original_position, on: :create before_validation :set_original_position, on: :create
before_validation :update_position, on: :create, if: :on_text? before_validation :update_position, on: :create, if: :on_text?
...@@ -135,6 +136,12 @@ class DiffNote < Note ...@@ -135,6 +136,12 @@ class DiffNote < Note
errors.add(:position, "is invalid") errors.add(:position, "is invalid")
end end
def diff_refs_match_commit
return if self.original_position.diff_refs == self.commit.diff_refs
errors.add(:commit_id, 'does not match the diff refs')
end
def keep_around_commits def keep_around_commits
project.repository.keep_around(self.original_position.base_sha) project.repository.keep_around(self.original_position.base_sha)
project.repository.keep_around(self.original_position.start_sha) project.repository.keep_around(self.original_position.start_sha)
......
...@@ -11,6 +11,7 @@ class Discussion ...@@ -11,6 +11,7 @@ class Discussion
:author, :author,
:noteable, :noteable,
:commit_id,
:for_commit?, :for_commit?,
:for_merge_request?, :for_merge_request?,
......
...@@ -72,7 +72,7 @@ class Event < ActiveRecord::Base ...@@ -72,7 +72,7 @@ class Event < ActiveRecord::Base
# We're using preload for "push_event_payload" as otherwise the association # We're using preload for "push_event_payload" as otherwise the association
# is not always available (depending on the query being built). # is not always available (depending on the query being built).
includes(:author, :project, project: :namespace) includes(:author, :project, project: :namespace)
.preload(:target, :push_event_payload) .preload(:push_event_payload, target: :author)
end end
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) } scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
......
...@@ -10,6 +10,9 @@ class Issue < ActiveRecord::Base ...@@ -10,6 +10,9 @@ class Issue < ActiveRecord::Base
include RelativePositioning include RelativePositioning
include TimeTrackable include TimeTrackable
include ThrottledTouch include ThrottledTouch
include IgnorableColumn
ignore_column :assignee_id
DueDateStruct = Struct.new(:title, :name).freeze DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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