summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.md39
-rw-r--r--app/assets/javascripts/awards_handler.js10
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji.js206
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js121
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/spread_string.js2
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js33
-rw-r--r--app/assets/javascripts/dispatcher.js3
-rw-r--r--app/assets/javascripts/extensions/string.js4
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js8
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js7
-rw-r--r--app/assets/javascripts/main.js326
-rw-r--r--app/assets/javascripts/merge_request_widget.js5
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js155
-rw-r--r--app/assets/javascripts/notes.js10
-rw-r--r--app/assets/javascripts/pager.js8
-rw-r--r--app/controllers/autocomplete_controller.rb3
-rw-r--r--app/controllers/projects/services_controller.rb3
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/models/ci/stage.rb6
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/project_services/issue_tracker_service.rb11
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/projects/diffs/_line.html.haml4
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml4
-rw-r--r--app/views/shared/issuable/_form.html.haml27
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml34
-rw-r--r--app/views/shared/milestones/_issuables.html.haml2
-rw-r--r--changelogs/unreleased/24166-close-builds-dropdown.yml4
-rw-r--r--changelogs/unreleased/28030-infinite-offset.yml4
-rw-r--r--changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml4
-rw-r--r--changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml4
-rw-r--r--changelogs/unreleased/29209-sign-up-form-name.yml4
-rw-r--r--changelogs/unreleased/adam-prevent-two-issue-trackers.yml4
-rw-r--r--changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml4
-rw-r--r--config/gitlab.yml.example10
-rw-r--r--config/webpack.config.js3
-rw-r--r--doc/administration/pages/source.md20
-rw-r--r--doc/development/frontend.md22
-rw-r--r--doc/development/ux_guide/copy.md9
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/update/8.17-to-9.0.md234
-rw-r--r--lib/api/milestones.rb6
-rw-r--r--lib/tasks/gitlab/db.rake2
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb50
-rw-r--r--spec/features/projects/edit_spec.rb30
-rw-r--r--spec/javascripts/awards_handler_spec.js5
-rw-r--r--spec/javascripts/gl_emoji_spec.js23
-rw-r--r--spec/javascripts/mini_pipeline_graph_dropdown_spec.js35
-rw-r--r--spec/javascripts/pager_spec.js90
-rw-r--r--spec/models/ci/pipeline_spec.rb18
-rw-r--r--spec/models/ci/stage_spec.rb17
-rw-r--r--spec/models/project_services/issue_tracker_service_spec.rb32
-rw-r--r--spec/requests/api/milestones_spec.rb32
53 files changed, 1169 insertions, 538 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3cbc826e6db..1fd29fef4f0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -94,6 +94,10 @@ look for [issues with the label `Accepting Merge Requests` and weight < 5][accep
These issues will be of reasonable size and challenge, for anyone to start
contributing to GitLab.
+## Workflow labels
+
+Labelling issues is described in the [GitLab Inc engineering workflow].
+
## Implement design & UI elements
Please see the [UX Guide for GitLab].
@@ -299,10 +303,13 @@ request is as follows:
1. [Generate a changelog entry with `bin/changelog`][changelog]
1. If you are writing documentation, make sure to follow the
[documentation styleguide][doc-styleguide]
-1. If you have multiple commits please combine them into one commit by
- [squashing them][git-squash]
+1. If you have multiple commits please combine them into a few logically
+ organized commits by [squashing them][git-squash]
1. Push the commit(s) to your fork
1. Submit a merge request (MR) to the `master` branch
+1. Leave the approvals settings as they are:
+ 1. Your merge request needs at least 1 approval
+ 1. You don't have to select any approvers
1. The MR title should describe the change you want to make
1. The MR description should give a motive for your change and the method you
used to achieve it.
@@ -345,13 +352,31 @@ The ['How to get faster PR reviews' document of Kubernetes](https://github.com/k
For examples of feedback on merge requests please look at already
[closed merge requests][closed-merge-requests]. If you would like quick feedback
-on your merge request feel free to mention one of the Merge Marshalls in the
-[core team] or one of the [Merge request coaches](https://about.gitlab.com/team/).
+on your merge request feel free to mention someone from the [core team] or one
+of the [Merge request coaches][team].
Please ensure that your merge request meets the contribution acceptance criteria.
When having your code reviewed and when reviewing merge requests please take the
[code review guidelines](doc/development/code_review.md) into account.
+### Getting your merge request reviewed, approved, and merged
+
+There are a few rules to get your merge request accepted:
+
+1. Your merge request should only be **merged by a [maintainer][team]**.
+ 1. If your merge request includes only backend changes [^1], it must be
+ **approved by a [backend maintainer][team]**.
+ 1. If your merge request includes only frontend changes [^1], it must be
+ **approved by a [frontend maintainer][team]**.
+ 1. If your merge request includes frontend and backend changes [^1], it must
+ be approved by a frontend **and** a backend maintainer.
+1. To lower the amount of merge requests maintainers need to review, you can
+ ask or assign any [reviewers][team] for a first review.
+ 1. If you need some guidance (e.g. it's your first merge request), feel free
+ to ask one of the [Merge request coaches][team].
+ 1. The reviewer will assign the merge request to a maintainer once the
+ reviewer is satisfied with the state of the merge request.
+
### Contribution acceptance criteria
1. The change is as small as possible
@@ -489,6 +514,7 @@ This Code of Conduct is adapted from the [Contributor Covenant][contributor-cove
available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/).
[core team]: https://about.gitlab.com/core-team/
+[team]: https://about.gitlab.com/team/
[getting-help]: https://about.gitlab.com/getting-help/
[codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq
[accepting-mrs-weight]: https://gitlab.com/gitlab-org/gitlab-ce/issues?assignee_id=0&label_name[]=Accepting%20Merge%20Requests&sort=weight_asc
@@ -513,3 +539,8 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
[UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/
[license-finder-doc]: doc/development/licensing.md
+[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
+
+[^1]: Specs other than JavaScript specs are considered backend code. Haml
+ changes are considered backend code if they include Ruby code other than just
+ pure HTML.
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 4667980a960..54836efdf29 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,10 +1,8 @@
/* global Cookies */
-const emojiMap = require('emoji-map');
-const emojiAliases = require('emoji-aliases');
-const glEmoji = require('./behaviors/gl_emoji');
-
-const glEmojiTag = glEmoji.glEmojiTag;
+import emojiMap from 'emojis/digests.json';
+import emojiAliases from 'emojis/aliases.json';
+import { glEmojiTag } from './behaviors/gl_emoji';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const requestAnimationFrame = window.requestAnimationFrame ||
@@ -515,4 +513,4 @@ AwardsHandler.prototype.destroy = function destroy() {
$('.emoji-menu').remove();
};
-module.exports = AwardsHandler;
+export default AwardsHandler;
diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js
index d1d98c3919f..59741cc9b1a 100644
--- a/app/assets/javascripts/behaviors/gl_emoji.js
+++ b/app/assets/javascripts/behaviors/gl_emoji.js
@@ -1,11 +1,13 @@
-const installCustomElements = require('document-register-element');
-const emojiMap = require('emoji-map');
-const emojiAliases = require('emoji-aliases');
-const generatedUnicodeSupportMap = require('./gl_emoji/unicode_support_map');
-const spreadString = require('./gl_emoji/spread_string');
+import installCustomElements from 'document-register-element';
+import emojiMap from 'emojis/digests.json';
+import emojiAliases from 'emojis/aliases.json';
+import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map';
+import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported';
installCustomElements(window);
+const generatedUnicodeSupportMap = getUnicodeSupportMap();
+
function emojiImageTag(name, src) {
return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`;
}
@@ -55,163 +57,49 @@ function glEmojiTag(inputName, options) {
`;
}
-// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
-const flagACodePoint = 127462; // parseInt('1F1E6', 16)
-const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
-function isFlagEmoji(emojiUnicode) {
- const cp = emojiUnicode.codePointAt(0);
- // Length 4 because flags are made of 2 characters which are surrogate pairs
- return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint;
-}
-
-// Chrome <57 renders keycaps oddly
-// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294
-// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png
-function isKeycapEmoji(emojiUnicode) {
- return emojiUnicode.length === 3 && emojiUnicode[2] === '\u20E3';
-}
-
-// Check for a skin tone variation emoji which aren't always supported
-const tone1 = 127995;// parseInt('1F3FB', 16)
-const tone5 = 127999;// parseInt('1F3FF', 16)
-function isSkinToneComboEmoji(emojiUnicode) {
- return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => {
- const cp = char.codePointAt(0);
- return cp >= tone1 && cp <= tone5;
- });
-}
-
-// macOS supports most skin tone emoji's but
-// doesn't support the skin tone versions of horse racing
-const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
-function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
- return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
- isSkinToneComboEmoji(emojiUnicode);
-}
-
-// Check for `family_*`, `kiss_*`, `couple_*`
-// For ex. Windows 8.1 Firefox 51.0.1, doesn't support these
-const zwj = 8205; // parseInt('200D', 16)
-const personStartCodePoint = 128102; // parseInt('1F466', 16)
-const personEndCodePoint = 128105; // parseInt('1F469', 16)
-function isPersonZwjEmoji(emojiUnicode) {
- let hasPersonEmoji = false;
- let hasZwj = false;
- spreadString(emojiUnicode).forEach((character) => {
- const cp = character.codePointAt(0);
- if (cp === zwj) {
- hasZwj = true;
- } else if (cp >= personStartCodePoint && cp <= personEndCodePoint) {
- hasPersonEmoji = true;
+function installGlEmojiElement() {
+ const GlEmojiElementProto = Object.create(HTMLElement.prototype);
+ GlEmojiElementProto.createdCallback = function createdCallback() {
+ const emojiUnicode = this.textContent.trim();
+ const {
+ name,
+ unicodeVersion,
+ fallbackSrc,
+ fallbackSpriteClass,
+ } = this.dataset;
+
+ const isEmojiUnicode = this.childNodes && Array.prototype.every.call(
+ this.childNodes,
+ childNode => childNode.nodeType === 3,
+ );
+ const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
+ const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
+
+ if (
+ isEmojiUnicode &&
+ !isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion)
+ ) {
+ // CSS sprite fallback takes precedence over image fallback
+ if (hasCssSpriteFalback) {
+ // IE 11 doesn't like adding multiple at once :(
+ this.classList.add('emoji-icon');
+ this.classList.add(fallbackSpriteClass);
+ } else if (hasImageFallback) {
+ this.innerHTML = emojiImageTag(name, fallbackSrc);
+ } else {
+ const src = assembleFallbackImageSrc(name);
+ this.innerHTML = emojiImageTag(name, src);
+ }
}
- });
-
- return hasPersonEmoji && hasZwj;
-}
-
-// Helper so we don't have to run `isFlagEmoji` twice
-// in `isEmojiUnicodeSupported` logic
-function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
- const isFlagResult = isFlagEmoji(emojiUnicode);
- return (
- (unicodeSupportMap.flag && isFlagResult) ||
- !isFlagResult
- );
-}
-
-// Helper so we don't have to run `isSkinToneComboEmoji` twice
-// in `isEmojiUnicodeSupported` logic
-function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) {
- const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode);
- return (
- (unicodeSupportMap.skinToneModifier && isSkinToneResult) ||
- !isSkinToneResult
- );
-}
-
-// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice
-// in `isEmojiUnicodeSupported` logic
-function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) {
- const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode);
- return (
- (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) ||
- !isHorseRacingSkinToneResult
- );
-}
-
-// Helper so we don't have to run `isPersonZwjEmoji` twice
-// in `isEmojiUnicodeSupported` logic
-function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) {
- const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode);
- return (
- (unicodeSupportMap.personZwj && isPersonZwjResult) ||
- !isPersonZwjResult
- );
-}
-
-// Takes in a support map and determines whether
-// the given unicode emoji is supported on the platform.
-//
-// Combines all the edge case tests into a one-stop shop method
-function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) {
- const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome &&
- unicodeSupportMap.meta.chromeVersion < 57;
+ };
- // For comments about each scenario, see the comments above each individual respective function
- return unicodeSupportMap[unicodeVersion] &&
- !(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) &&
- checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) &&
- checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) &&
- checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) &&
- checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode);
+ document.registerElement('gl-emoji', {
+ prototype: GlEmojiElementProto,
+ });
}
-const GlEmojiElementProto = Object.create(HTMLElement.prototype);
-GlEmojiElementProto.createdCallback = function createdCallback() {
- const emojiUnicode = this.textContent.trim();
- const {
- name,
- unicodeVersion,
- fallbackSrc,
- fallbackSpriteClass,
- } = this.dataset;
-
- const isEmojiUnicode = this.childNodes && Array.prototype.every.call(
- this.childNodes,
- childNode => childNode.nodeType === 3,
- );
- const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
- const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
-
- if (
- isEmojiUnicode &&
- !isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion)
- ) {
- // CSS sprite fallback takes precedence over image fallback
- if (hasCssSpriteFalback) {
- // IE 11 doesn't like adding multiple at once :(
- this.classList.add('emoji-icon');
- this.classList.add(fallbackSpriteClass);
- } else if (hasImageFallback) {
- this.innerHTML = emojiImageTag(name, fallbackSrc);
- } else {
- const src = assembleFallbackImageSrc(name);
- this.innerHTML = emojiImageTag(name, src);
- }
- }
-};
-
-document.registerElement('gl-emoji', {
- prototype: GlEmojiElementProto,
-});
-
-module.exports = {
- emojiImageTag,
+export {
+ installGlEmojiElement,
glEmojiTag,
- isEmojiUnicodeSupported,
- isFlagEmoji,
- isKeycapEmoji,
- isSkinToneComboEmoji,
- isHorceRacingSkinToneComboEmoji,
- isPersonZwjEmoji,
+ emojiImageTag,
};
diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js
new file mode 100644
index 00000000000..5e3c45f7e92
--- /dev/null
+++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js
@@ -0,0 +1,121 @@
+import spreadString from './spread_string';
+
+// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
+const flagACodePoint = 127462; // parseInt('1F1E6', 16)
+const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
+function isFlagEmoji(emojiUnicode) {
+ const cp = emojiUnicode.codePointAt(0);
+ // Length 4 because flags are made of 2 characters which are surrogate pairs
+ return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint;
+}
+
+// Chrome <57 renders keycaps oddly
+// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294
+// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png
+function isKeycapEmoji(emojiUnicode) {
+ return emojiUnicode.length === 3 && emojiUnicode[2] === '\u20E3';
+}
+
+// Check for a skin tone variation emoji which aren't always supported
+const tone1 = 127995;// parseInt('1F3FB', 16)
+const tone5 = 127999;// parseInt('1F3FF', 16)
+function isSkinToneComboEmoji(emojiUnicode) {
+ return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => {
+ const cp = char.codePointAt(0);
+ return cp >= tone1 && cp <= tone5;
+ });
+}
+
+// macOS supports most skin tone emoji's but
+// doesn't support the skin tone versions of horse racing
+const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
+function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
+ return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
+ isSkinToneComboEmoji(emojiUnicode);
+}
+
+// Check for `family_*`, `kiss_*`, `couple_*`
+// For ex. Windows 8.1 Firefox 51.0.1, doesn't support these
+const zwj = 8205; // parseInt('200D', 16)
+const personStartCodePoint = 128102; // parseInt('1F466', 16)
+const personEndCodePoint = 128105; // parseInt('1F469', 16)
+function isPersonZwjEmoji(emojiUnicode) {
+ let hasPersonEmoji = false;
+ let hasZwj = false;
+ spreadString(emojiUnicode).forEach((character) => {
+ const cp = character.codePointAt(0);
+ if (cp === zwj) {
+ hasZwj = true;
+ } else if (cp >= personStartCodePoint && cp <= personEndCodePoint) {
+ hasPersonEmoji = true;
+ }
+ });
+
+ return hasPersonEmoji && hasZwj;
+}
+
+// Helper so we don't have to run `isFlagEmoji` twice
+// in `isEmojiUnicodeSupported` logic
+function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
+ const isFlagResult = isFlagEmoji(emojiUnicode);
+ return (
+ (unicodeSupportMap.flag && isFlagResult) ||
+ !isFlagResult
+ );
+}
+
+// Helper so we don't have to run `isSkinToneComboEmoji` twice
+// in `isEmojiUnicodeSupported` logic
+function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) {
+ const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode);
+ return (
+ (unicodeSupportMap.skinToneModifier && isSkinToneResult) ||
+ !isSkinToneResult
+ );
+}
+
+// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice
+// in `isEmojiUnicodeSupported` logic
+function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) {
+ const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode);
+ return (
+ (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) ||
+ !isHorseRacingSkinToneResult
+ );
+}
+
+// Helper so we don't have to run `isPersonZwjEmoji` twice
+// in `isEmojiUnicodeSupported` logic
+function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) {
+ const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode);
+ return (
+ (unicodeSupportMap.personZwj && isPersonZwjResult) ||
+ !isPersonZwjResult
+ );
+}
+
+// Takes in a support map and determines whether
+// the given unicode emoji is supported on the platform.
+//
+// Combines all the edge case tests into a one-stop shop method
+function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) {
+ const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome &&
+ unicodeSupportMap.meta.chromeVersion < 57;
+
+ // For comments about each scenario, see the comments above each individual respective function
+ return unicodeSupportMap[unicodeVersion] &&
+ !(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) &&
+ checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) &&
+ checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) &&
+ checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) &&
+ checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode);
+}
+
+export {
+ isEmojiUnicodeSupported,
+ isFlagEmoji,
+ isKeycapEmoji,
+ isSkinToneComboEmoji,
+ isHorceRacingSkinToneComboEmoji,
+ isPersonZwjEmoji,
+};
diff --git a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js
index 2380349c4fa..327764ec6e9 100644
--- a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js
+++ b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js
@@ -47,4 +47,4 @@ function spreadString(str) {
return arr;
}
-module.exports = spreadString;
+export default spreadString;
diff --git a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js
index f31716d4c07..aa522e20c36 100644
--- a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js
+++ b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js
@@ -68,7 +68,7 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche
// See 32px, https://i.imgur.com/htY6Zym.png
// See 16px, https://i.imgur.com/FPPsIF8.png
const fontSize = 16;
-function testUnicodeSupportMap(testMap) {
+function generateUnicodeSupportMap(testMap) {
const testMapKeys = Object.keys(testMap);
const numTestEntries = testMapKeys
.reduce((list, testKey) => list.concat(testMap[testKey]), []).length;
@@ -138,17 +138,24 @@ function testUnicodeSupportMap(testMap) {
return resultMap;
}
-let unicodeSupportMap;
-const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
-try {
- unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
-} catch (err) {
- // swallow
-}
-if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
- unicodeSupportMap = testUnicodeSupportMap(unicodeSupportTestMap);
- window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
- window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
+function getUnicodeSupportMap() {
+ let unicodeSupportMap;
+ const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
+ try {
+ unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
+ } catch (err) {
+ // swallow
+ }
+ if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
+ unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap);
+ window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
+ window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
+ }
+
+ return unicodeSupportMap;
}
-module.exports = unicodeSupportMap;
+export {
+ getUnicodeSupportMap,
+ generateUnicodeSupportMap,
+};
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 017980271b1..7b9b9123c31 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -39,6 +39,7 @@ import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
import GroupsList from './groups_list';
import ProjectsList from './projects_list';
+import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
const ShortcutsBlob = require('./shortcuts_blob');
const UserCallout = require('./user_callout');
@@ -181,7 +182,7 @@ const UserCallout = require('./user_callout');
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:commit:pipelines':
- new gl.MiniPipelineGraph({
+ new MiniPipelineGraph({
container: '.js-pipeline-table',
}).bindEvents();
break;
diff --git a/app/assets/javascripts/extensions/string.js b/app/assets/javascripts/extensions/string.js
index fe23be0bbc1..ae9662444b0 100644
--- a/app/assets/javascripts/extensions/string.js
+++ b/app/assets/javascripts/extensions/string.js
@@ -1,2 +1,2 @@
-require('string.prototype.codepointat');
-require('string.fromcodepoint');
+import 'string.prototype.codepointat';
+import 'string.fromcodepoint';
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 1bc04a5ad96..4f7ce1fa197 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -1,10 +1,8 @@
/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */
-const emojiMap = require('emoji-map');
-const emojiAliases = require('emoji-aliases');
-const glEmoji = require('./behaviors/gl_emoji');
-
-const glEmojiTag = glEmoji.glEmojiTag;
+import emojiMap from 'emojis/digests.json';
+import emojiAliases from 'emojis/aliases.json';
+import { glEmojiTag } from '~/behaviors/gl_emoji';
// Creates the variables for setting up GFM auto-completion
(function() {
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 1bc81d2e4a4..09c4261b318 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -66,6 +66,13 @@
return results;
})()).join('&');
};
+ w.gl.utils.removeParams = (params) => {
+ const url = new URL(window.location.href);
+ params.forEach((param) => {
+ url.search = w.gl.utils.removeParamQueryString(url.search, param);
+ });
+ return url.href;
+ };
w.gl.utils.getLocationHash = function(url) {
var hashIndex;
if (typeof url === 'undefined') {
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 79164edff0e..689a6c3a93a 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import */
+/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */
/* global bp */
/* global Cookies */
/* global Flash */
@@ -13,19 +13,20 @@ import Dropzone from 'dropzone';
import Sortable from 'vendor/Sortable';
// libraries with import side-effects
-require('mousetrap');
-require('mousetrap/plugins/pause/mousetrap-pause');
-require('vendor/fuzzaldrin-plus');
-require('es6-promise').polyfill();
+import 'mousetrap';
+import 'mousetrap/plugins/pause/mousetrap-pause';
+import 'vendor/fuzzaldrin-plus';
+import promisePolyfill from 'es6-promise';
// extensions
-require('./extensions/string');
-require('./extensions/array');
-require('./extensions/custom_event');
-require('./extensions/element');
-require('./extensions/jquery');
-require('./extensions/object');
-require('es6-promise').polyfill();
+import './extensions/string';
+import './extensions/array';
+import './extensions/custom_event';
+import './extensions/element';
+import './extensions/jquery';
+import './extensions/object';
+
+promisePolyfill.polyfill();
// expose common libraries as globals (TODO: remove these)
window.jQuery = jQuery;
@@ -37,174 +38,171 @@ window.Dropzone = Dropzone;
window.Sortable = Sortable;
// shortcuts
-require('./shortcuts');
-require('./shortcuts_navigation');
-require('./shortcuts_dashboard_navigation');
-require('./shortcuts_issuable');
-require('./shortcuts_network');
+import './shortcuts';
+import './shortcuts_blob';
+import './shortcuts_dashboard_navigation';
+import './shortcuts_navigation';
+import './shortcuts_find_file';
+import './shortcuts_issuable';
+import './shortcuts_network';
// behaviors
-require('./behaviors/autosize');
-require('./behaviors/details_behavior');
-require('./behaviors/quick_submit');
-require('./behaviors/requires_input');
-require('./behaviors/toggler_behavior');
-require('./behaviors/bind_in_out');
+import './behaviors/autosize';
+import './behaviors/details_behavior';
+import './behaviors/quick_submit';
+import './behaviors/requires_input';
+import './behaviors/toggler_behavior';
+import './behaviors/bind_in_out';
+import { installGlEmojiElement } from './behaviors/gl_emoji';
+installGlEmojiElement();
// blob
-require('./blob/blob_ci_yaml');
-require('./blob/blob_dockerfile_selector');
-require('./blob/blob_dockerfile_selectors');
-require('./blob/blob_file_dropzone');
-require('./blob/blob_gitignore_selector');
-require('./blob/blob_gitignore_selectors');
-require('./blob/blob_license_selector');
-require('./blob/blob_license_selectors');
-require('./blob/template_selector');
+import './blob/blob_ci_yaml';
+import './blob/blob_dockerfile_selector';
+import './blob/blob_dockerfile_selectors';
+import './blob/blob_file_dropzone';
+import './blob/blob_gitignore_selector';
+import './blob/blob_gitignore_selectors';
+import './blob/blob_license_selector';
+import './blob/blob_license_selectors';
+import './blob/template_selector';
// templates
-require('./templates/issuable_template_selector');
-require('./templates/issuable_template_selectors');
+import './templates/issuable_template_selector';
+import './templates/issuable_template_selectors';
// commit
-require('./commit/file.js');
-require('./commit/image_file.js');
+import './commit/file';
+import './commit/image_file';
// lib/utils
-require('./lib/utils/animate');
-require('./lib/utils/bootstrap_linked_tabs');
-require('./lib/utils/common_utils');
-require('./lib/utils/datetime_utility');
-require('./lib/utils/notify');
-require('./lib/utils/pretty_time');
-require('./lib/utils/text_utility');
-require('./lib/utils/type_utility');
-require('./lib/utils/url_utility');
+import './lib/utils/animate';
+import './lib/utils/bootstrap_linked_tabs';
+import './lib/utils/common_utils';
+import './lib/utils/datetime_utility';
+import './lib/utils/notify';
+import './lib/utils/pretty_time';
+import './lib/utils/text_utility';
+import './lib/utils/type_utility';
+import './lib/utils/url_utility';
// u2f
-require('./u2f/authenticate');
-require('./u2f/error');
-require('./u2f/register');
-require('./u2f/util');
+import './u2f/authenticate';
+import './u2f/error';
+import './u2f/register';
+import './u2f/util';
// droplab
-require('./droplab/droplab');
-require('./droplab/droplab_ajax');
-require('./droplab/droplab_ajax_filter');
-require('./droplab/droplab_filter');
+import './droplab/droplab';
+import './droplab/droplab_ajax';
+import './droplab/droplab_ajax_filter';
+import './droplab/droplab_filter';
// everything else
-require('./abuse_reports');
-require('./activities');
-require('./admin');
-require('./ajax_loading_spinner');
-require('./api');
-require('./aside');
-require('./autosave');
-const AwardsHandler = require('./awards_handler');
-require('./breakpoints');
-require('./broadcast_message');
-require('./build');
-require('./build_artifacts');
-require('./build_variables');
-require('./ci_lint_editor');
-require('./commit');
-require('./commits');
-require('./compare');
-require('./compare_autocomplete');
-require('./confirm_danger_modal');
-require('./copy_as_gfm');
-require('./copy_to_clipboard');
-require('./create_label');
-require('./diff');
-require('./dispatcher');
-require('./dropzone_input');
-require('./due_date_select');
-require('./files_comment_button');
-require('./flash');
-require('./gfm_auto_complete');
-require('./gl_dropdown');
-require('./gl_field_error');
-require('./gl_field_errors');
-require('./gl_form');
-require('./group_avatar');
-require('./group_label_subscription');
-require('./groups_select');
-require('./header');
-require('./importer_status');
-require('./issuable');
-require('./issuable_context');
-require('./issuable_form');
-require('./issue');
-require('./issue_status_select');
-require('./issues_bulk_assignment');
-require('./label_manager');
-require('./labels');
-require('./labels_select');
-require('./layout_nav');
-require('./line_highlighter');
-require('./logo');
-require('./member_expiration_date');
-require('./members');
-require('./merge_request');
-require('./merge_request_tabs');
-require('./merge_request_widget');
-require('./merged_buttons');
-require('./milestone');
-require('./milestone_select');
-require('./mini_pipeline_graph_dropdown');
-require('./namespace_select');
-require('./new_branch_form');
-require('./new_commit_form');
-require('./notes');
-require('./notifications_dropdown');
-require('./notifications_form');
-require('./pager');
-require('./pipelines');
-require('./preview_markdown');
-require('./project');
-require('./project_avatar');
-require('./project_find_file');
-require('./project_fork');
-require('./project_import');
-require('./project_label_subscription');
-require('./project_new');
-require('./project_select');
-require('./project_show');
-require('./project_variables');
-require('./projects_list');
-require('./render_gfm');
-require('./render_math');
-require('./right_sidebar');
-require('./search');
-require('./search_autocomplete');
-require('./shortcuts');
-require('./shortcuts_blob');
-require('./shortcuts_dashboard_navigation');
-require('./shortcuts_find_file');
-require('./shortcuts_issuable');
-require('./shortcuts_navigation');
-require('./shortcuts_network');
-require('./signin_tabs_memoizer');
-require('./single_file_diff');
-require('./smart_interval');
-require('./snippets_list');
-require('./star');
-require('./subbable_resource');
-require('./subscription');
-require('./subscription_select');
-require('./syntax_highlight');
-require('./task_list');
-require('./todos');
-require('./tree');
-require('./user');
-require('./user_tabs');
-require('./username_validator');
-require('./users_select');
-require('./version_check_image');
-require('./visibility_select');
-require('./wikis');
-require('./zen_mode');
+import './abuse_reports';
+import './activities';
+import './admin';
+import './ajax_loading_spinner';
+import './api';
+import './aside';
+import './autosave';
+import AwardsHandler from './awards_handler';
+import './breakpoints';
+import './broadcast_message';
+import './build';
+import './build_artifacts';
+import './build_variables';
+import './ci_lint_editor';
+import './commit';
+import './commits';
+import './compare';
+import './compare_autocomplete';
+import './confirm_danger_modal';
+import './copy_as_gfm';
+import './copy_to_clipboard';
+import './create_label';
+import './diff';
+import './dispatcher';
+import './dropzone_input';
+import './due_date_select';
+import './files_comment_button';
+import './flash';
+import './gfm_auto_complete';
+import './gl_dropdown';
+import './gl_field_error';
+import './gl_field_errors';
+import './gl_form';
+import './group_avatar';
+import './group_label_subscription';
+import './groups_select';
+import './header';
+import './importer_status';
+import './issuable';
+import './issuable_context';
+import './issuable_form';
+import './issue';
+import './issue_status_select';
+import './issues_bulk_assignment';
+import './label_manager';
+import './labels';
+import './labels_select';
+import './layout_nav';
+import './line_highlighter';
+import './logo';
+import './member_expiration_date';
+import './members';
+import './merge_request';
+import './merge_request_tabs';
+import './merge_request_widget';
+import './merged_buttons';
+import './milestone';
+import './milestone_select';
+import './mini_pipeline_graph_dropdown';
+import './namespace_select';
+import './new_branch_form';
+import './new_commit_form';
+import './notes';
+import './notifications_dropdown';
+import './notifications_form';
+import './pager';
+import './pipelines';
+import './preview_markdown';
+import './project';
+import './project_avatar';
+import './project_find_file';
+import './project_fork';
+import './project_import';
+import './project_label_subscription';
+import './project_new';
+import './project_select';
+import './project_show';
+import './project_variables';
+import './projects_list';
+import './render_gfm';
+import './render_math';
+import './right_sidebar';
+import './search';
+import './search_autocomplete';
+import './signin_tabs_memoizer';
+import './single_file_diff';
+import './smart_interval';
+import './snippets_list';
+import './star';
+import './subbable_resource';
+import './subscription';
+import './subscription_select';
+import './syntax_highlight';
+import './task_list';
+import './todos';
+import './tree';
+import './user';
+import './user_tabs';
+import './username_validator';
+import './users_select';
+import './version_check_image';
+import './visibility_select';
+import './wikis';
+import './zen_mode';
(function () {
document.addEventListener('beforeunload', function () {
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
index 5f1bd474a0c..66cc270ab4d 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -3,7 +3,8 @@
/* global notifyPermissions */
/* global merge_request_widget */
-require('./smart_interval');
+import './smart_interval';
+import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
((global) => {
var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
@@ -285,7 +286,7 @@ require('./smart_interval');
};
MergeRequestWidget.prototype.initMiniPipelineGraph = function() {
- new gl.MiniPipelineGraph({
+ new MiniPipelineGraph({
container: '.js-pipeline-inline-mr-widget-graph:visible',
}).bindEvents();
};
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
index 2145e531331..9c58c465001 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
@@ -15,81 +15,96 @@
* <div class="js-builds-dropdown-container dropdown-menu"></div>
* </div>
*/
-(() => {
- class MiniPipelineGraph {
- constructor(opts = {}) {
- this.container = opts.container || '';
- this.dropdownListSelector = '.js-builds-dropdown-container';
- this.getBuildsList = this.getBuildsList.bind(this);
- }
- /**
- * Adds the event listener when the dropdown is opened.
- * All dropdown events are fired at the .dropdown-menu's parent element.
- */
- bindEvents() {
- $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList);
- }
+export default class MiniPipelineGraph {
+ constructor(opts = {}) {
+ this.container = opts.container || '';
+ this.dropdownListSelector = '.js-builds-dropdown-container';
+ this.getBuildsList = this.getBuildsList.bind(this);
+ }
+
+ /**
+ * Adds the event listener when the dropdown is opened.
+ * All dropdown events are fired at the .dropdown-menu's parent element.
+ */
+ bindEvents() {
+ $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList);
+ }
- /**
- * For the clicked stage, renders the given data in the dropdown list.
- *
- * @param {HTMLElement} stageContainer
- * @param {Object} data
- */
- renderBuildsList(stageContainer, data) {
- const dropdownContainer = stageContainer.parentElement.querySelector(
- `${this.dropdownListSelector} .js-builds-dropdown-list`,
- );
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name
+ * the dropdown should not be closed and the link should open in another tab,
+ * so we stop propagation of the click event inside the dropdown.
+ *
+ * Since this component is rendered multiple times per page we need to guarantee we only
+ * target the click event of this component.
+ */
+ stopDropdownClickPropagation() {
+ $(document).on(
+ 'click',
+ `${this.container} .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item`,
+ (e) => {
+ e.stopPropagation();
+ },
+ );
+ }
- dropdownContainer.innerHTML = data;
- }
+ /**
+ * For the clicked stage, renders the given data in the dropdown list.
+ *
+ * @param {HTMLElement} stageContainer
+ * @param {Object} data
+ */
+ renderBuildsList(stageContainer, data) {
+ const dropdownContainer = stageContainer.parentElement.querySelector(
+ `${this.dropdownListSelector} .js-builds-dropdown-list`,
+ );
- /**
- * For the clicked stage, gets the list of builds.
- *
- * All dropdown events have a relatedTarget property,
- * whose value is the toggling anchor element.
- *
- * @param {Object} e bootstrap dropdown event
- * @return {Promise}
- */
- getBuildsList(e) {
- const button = e.relatedTarget;
- const endpoint = button.dataset.stageEndpoint;
+ dropdownContainer.innerHTML = data;
+ }
- return $.ajax({
- dataType: 'json',
- type: 'GET',
- url: endpoint,
- beforeSend: () => {
- this.renderBuildsList(button, '');
- this.toggleLoading(button);
- },
- success: (data) => {
- this.toggleLoading(button);
- this.renderBuildsList(button, data.html);
- },
- error: () => {
- this.toggleLoading(button);
- new Flash('An error occurred while fetching the builds.', 'alert');
- },
- });
- }
+ /**
+ * For the clicked stage, gets the list of builds.
+ *
+ * All dropdown events have a relatedTarget property,
+ * whose value is the toggling anchor element.
+ *
+ * @param {Object} e bootstrap dropdown event
+ * @return {Promise}
+ */
+ getBuildsList(e) {
+ const button = e.relatedTarget;
+ const endpoint = button.dataset.stageEndpoint;
- /**
- * Toggles the visibility of the loading icon.
- *
- * @param {HTMLElement} stageContainer
- * @return {type}
- */
- toggleLoading(stageContainer) {
- stageContainer.parentElement.querySelector(
- `${this.dropdownListSelector} .js-builds-dropdown-loading`,
- ).classList.toggle('hidden');
- }
+ return $.ajax({
+ dataType: 'json',
+ type: 'GET',
+ url: endpoint,
+ beforeSend: () => {
+ this.renderBuildsList(button, '');
+ this.toggleLoading(button);
+ },
+ success: (data) => {
+ this.toggleLoading(button);
+ this.renderBuildsList(button, data.html);
+ this.stopDropdownClickPropagation();
+ },
+ error: () => {
+ this.toggleLoading(button);
+ new Flash('An error occurred while fetching the builds.', 'alert');
+ },
+ });
}
- window.gl = window.gl || {};
- window.gl.MiniPipelineGraph = MiniPipelineGraph;
-})();
+ /**
+ * Toggles the visibility of the loading icon.
+ *
+ * @param {HTMLElement} stageContainer
+ * @return {type}
+ */
+ toggleLoading(stageContainer) {
+ stageContainer.parentElement.querySelector(
+ `${this.dropdownListSelector} .js-builds-dropdown-loading`,
+ ).classList.toggle('hidden');
+ }
+}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index eeab69da941..47cc34e7a20 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,12 +1,14 @@
/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape */
/* global Flash */
/* global Autosave */
+/* global Cookies */
/* global ResolveService */
/* global mrRefreshWidgetUrl */
require('./autosave');
window.autosize = require('vendor/autosize');
window.Dropzone = require('dropzone');
+window.Cookies = require('js-cookie');
require('./dropzone_input');
require('./gfm_auto_complete');
require('vendor/jquery.caret'); // required by jquery.atwho
@@ -42,7 +44,6 @@ require('./task_list');
this.notes_url = notes_url;
this.note_ids = note_ids;
this.last_fetched_at = last_fetched_at;
- this.view = view;
this.noteable_url = document.URL;
this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge"));
this.basePollingInterval = 15000;
@@ -57,6 +58,7 @@ require('./task_list');
selector: '.notes'
});
this.collapseLongCommitList();
+ this.setViewType(view);
// We are in the Merge Requests page so we need another edit form for Changes tab
if (gl.utils.getPagePath(1) === 'merge_requests') {
@@ -65,6 +67,10 @@ require('./task_list');
}
}
+ Notes.prototype.setViewType = function(view) {
+ this.view = Cookies.get('diff_view') || view;
+ };
+
Notes.prototype.addBinding = function() {
// add note to UI after creation
$(document).on("ajax:success", ".js-main-target-form", this.addNote);
@@ -302,7 +308,7 @@ require('./task_list');
};
Notes.prototype.isParallelView = function() {
- return this.view === 'parallel';
+ return Cookies.get('diff_view') === 'parallel';
};
/*
diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js
index e35cf6d295e..5f6bc902cf8 100644
--- a/app/assets/javascripts/pager.js
+++ b/app/assets/javascripts/pager.js
@@ -1,11 +1,15 @@
+require('~/lib/utils/common_utils');
+require('~/lib/utils/url_utility');
+
(() => {
const ENDLESS_SCROLL_BOTTOM_PX = 400;
const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
const Pager = {
init(limit = 0, preload = false, disable = false, callback = $.noop) {
+ this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']);
this.limit = limit;
- this.offset = this.limit;
+ this.offset = parseInt(gl.utils.getParameterByName('offset'), 10) || this.limit;
this.disable = disable;
this.callback = callback;
this.loading = $('.loading').first();
@@ -20,7 +24,7 @@
this.loading.show();
$.ajax({
type: 'GET',
- url: $('.content_list').data('href') || window.location.href,
+ url: this.url,
data: `limit=${this.limit}&offset=${this.offset}`,
dataType: 'json',
error: () => this.loading.hide(),
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index d7a45bacd35..b79ca034c5b 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -18,8 +18,7 @@ class AutocompleteController < ApplicationController
if params[:search].blank?
# Include current user if available to filter by "Me"
if params[:current_user].present? && current_user
- @users = @users.where.not(id: current_user.id)
- @users = [current_user, *@users]
+ @users = [current_user, *@users].uniq
end
if params[:author_id].present?
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 17cb1d5be24..f9d798d0455 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -13,7 +13,8 @@ class Projects::ServicesController < Projects::ApplicationController
end
def update
- if @service.update_attributes(service_params[:service])
+ @service.assign_attributes(service_params[:service])
+ if @service.save(context: :manual_change)
redirect_to(
edit_namespace_project_service_path(@project.namespace, @project, @service.to_param),
notice: 'Successfully updated.'
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 67206415f7b..8a5a9aa4adb 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -144,7 +144,7 @@ module Ci
status_sql = statuses.latest.where('stage=sg.stage').status_sql
- warnings_sql = statuses.latest.select('COUNT(*) > 0')
+ warnings_sql = statuses.latest.select('COUNT(*)')
.where('stage=sg.stage').failed_but_allowed.to_sql
stages_with_statuses = CommitStatus.from(stages_query, :sg)
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index ca74c91b062..e7d6b17d445 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -46,10 +46,10 @@ module Ci
end
def has_warnings?
- if @warnings.nil?
- statuses.latest.failed_but_allowed.any?
+ if @warnings.is_a?(Integer)
+ @warnings > 0
else
- @warnings
+ statuses.latest.failed_but_allowed.any?
end
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 37c727b5d9f..3cf4c67d7e7 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -63,6 +63,7 @@ module Issuable
scope :authored, ->(user) { where(author_id: user) }
scope :assigned_to, ->(u) { where(assignee_id: u.id)}
scope :recent, -> { reorder(id: :desc) }
+ scope :order_position_asc, -> { reorder(position: :asc) }
scope :assigned, -> { where("assignee_id IS NOT NULL") }
scope :unassigned, -> { where("assignee_id IS NULL") }
scope :of_projects, ->(ids) { where(project_id: ids) }
@@ -144,6 +145,7 @@ module Issuable
when 'downvotes_desc' then order_downvotes_desc
when 'upvotes_desc' then order_upvotes_desc
when 'priority' then order_labels_priority(excluded_labels: excluded_labels)
+ when 'position_asc' then order_position_asc
else
order_by(method)
end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 9e65fdbf9d6..50435b67eda 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -1,4 +1,6 @@
class IssueTrackerService < Service
+ validate :one_issue_tracker, if: :activated?, on: :manual_change
+
default_value_for :category, 'issue_tracker'
# Pattern used to extract links from comments
@@ -92,4 +94,13 @@ class IssueTrackerService < Service
def issues_tracker
Gitlab.config.issues_tracker[to_param]
end
+
+ def one_issue_tracker
+ return if template?
+ return if project.blank?
+
+ if project.services.external_issue_trackers.where.not(id: id).any?
+ errors.add(:base, 'Another issue tracker is already in use. Only one issue tracker service can be active at a time')
+ end
+ end
end
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 30e63d991bb..a2f6a7ab1cb 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -4,7 +4,7 @@
.devise-errors
= devise_error_messages!
.form-group
- = f.label :name
+ = f.label :name, 'Full name'
= f.text_field :name, class: "form-control top", required: true, title: "This field is required."
.username.form-group
= f.label :username
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index ed279cfe168..62135d3ae32 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -14,13 +14,13 @@
%td.new_line.diff-line-num
%td.line_content.match= line.text
- else
- %td.old_line.diff-line-num.js-avatar-container{ class: type, data: { linenumber: line.old_pos } }
+ %td.old_line.diff-line-num{ class: [type, ("js-avatar-container" if !plain)], data: { linenumber: line.old_pos } }
- link_text = type == "new" ? " " : line.old_pos
- if plain
= link_text
- else
%a{ href: "##{line_code}", data: { linenumber: link_text } }
- - if discussion && !plain
+ - if discussion && discussion.resolvable? && !plain
%diff-note-avatars{ "discussion-id" => discussion.id }
%td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
- link_text = type == "old" ? " " : line.new_pos
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 6448748113b..e7758c8bdfa 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -20,7 +20,7 @@
- left_position = diff_file.position(left)
%td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } }
%a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } }
- - if discussion_left
+ - if discussion_left && discussion_left.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_left.id }
%td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text)
- else
@@ -39,7 +39,7 @@
- right_position = diff_file.position(right)
%td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } }
%a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } }
- - if discussion_right
+ - if discussion_right && discussion_right.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_right.id }
%td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text)
- else
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index cb92b2e97a7..70470c83c51 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -62,24 +62,25 @@
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
.row-content-block{ class: (is_footer ? "footer-block" : "middle-block") }
- - if issuable.new_record?
- = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
- - else
- = form.submit 'Save changes', class: 'btn btn-save'
+ .pull-right
+ - if issuable.new_record?
+ = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
+ - else
+ - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project)
+ = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped'
+ = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
+
+ %span.append-right-10
+ - if issuable.new_record?
+ = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
+ - else
+ = form.submit 'Save changes', class: 'btn btn-save'
- if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project))
- .inline.prepend-left-10
+ .inline.prepend-top-10
Please review the
%strong= link_to('contribution guidelines', guide_url)
for this project.
- - if issuable.new_record?
- = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
- - else
- .pull-right
- - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project)
- = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" },
- method: :delete, class: 'btn btn-danger btn-grouped'
- = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
= form.hidden_field :lock_version
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 32128f3b3dc..f8123846596 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -14,18 +14,18 @@
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
- %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) }
+ %input.form-control.filtered-search{ placeholder: 'Search or filter results...', data: { id: 'filtered-search', 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } }
= icon('filter')
%button.clear-search.hidden{ type: 'button' }
= icon('times')
#js-dropdown-hint.dropdown-menu.hint-dropdown
- %ul{ 'data-dropdown' => true }
- %li.filter-dropdown-item{ 'data-action' => 'submit' }
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { action: 'submit' } }
%button.btn.btn-link
= icon('search')
%span
Keep typing and press Enter
- %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
@@ -36,50 +36,50 @@
%span.js-filter-tag.dropdown-light-content
{{tag}}
#js-dropdown-author.dropdown-menu{ data: { icon: 'pencil', hint: 'author', tag: '@author' } }
- %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.dropdown-user
- %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' }
+ %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } }
.dropdown-user-details
%span
{{name}}
%span.dropdown-light-content
@{{username}}
#js-dropdown-assignee.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } }
- %ul{ 'data-dropdown' => true }
- %li.filter-dropdown-item{ 'data-value' => 'none' }
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link
No Assignee
%li.divider
- %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.dropdown-user
- %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' }
+ %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } }
.dropdown-user-details
%span
{{name}}
%span.dropdown-light-content
@{{username}}
#js-dropdown-milestone.dropdown-menu{ data: { icon: 'clock-o', hint: 'milestone', tag: '%milestone' } }
- %ul{ 'data-dropdown' => true }
- %li.filter-dropdown-item{ 'data-value' => 'none' }
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link
No Milestone
- %li.filter-dropdown-item{ 'data-value' => 'upcoming' }
+ %li.filter-dropdown-item{ data: { value: 'upcoming' } }
%button.btn.btn-link
Upcoming
%li.divider
- %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value
{{title}}
#js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label' } }
- %ul{ 'data-dropdown' => true }
- %li.filter-dropdown-item{ 'data-value' => 'none' }
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link
No Label
%li.divider
- %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link
%span.dropdown-label-box{ style: 'background: {{color}}' }
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
index a93cbd1041f..8af3bd597c5 100644
--- a/app/views/shared/milestones/_issuables.html.haml
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -13,6 +13,6 @@
- class_prefix = dom_class(issuables).pluralize
%ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id }
= render partial: 'shared/milestones/issuable',
- collection: issuables.sort_by(&:position),
+ collection: issuables.order_position_asc,
as: :issuable,
locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name }
diff --git a/changelogs/unreleased/24166-close-builds-dropdown.yml b/changelogs/unreleased/24166-close-builds-dropdown.yml
new file mode 100644
index 00000000000..c57ffed6b45
--- /dev/null
+++ b/changelogs/unreleased/24166-close-builds-dropdown.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent builds dropdown to close when the user clicks in a build
+merge_request:
+author:
diff --git a/changelogs/unreleased/28030-infinite-offset.yml b/changelogs/unreleased/28030-infinite-offset.yml
new file mode 100644
index 00000000000..6f4082d7684
--- /dev/null
+++ b/changelogs/unreleased/28030-infinite-offset.yml
@@ -0,0 +1,4 @@
+---
+title: allow offset query parameter for infinite list pages
+merge_request:
+author:
diff --git a/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml b/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml
new file mode 100644
index 00000000000..0177394aa0f
--- /dev/null
+++ b/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml
@@ -0,0 +1,4 @@
+---
+title: Order milestone issues by position ascending in api
+merge_request: 9635
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml b/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml
new file mode 100644
index 00000000000..f869249c22b
--- /dev/null
+++ b/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml
@@ -0,0 +1,4 @@
+---
+title: Fix create issue form buttons are misaligned on mobile
+merge_request: 9706
+author: TM Lee
diff --git a/changelogs/unreleased/29209-sign-up-form-name.yml b/changelogs/unreleased/29209-sign-up-form-name.yml
new file mode 100644
index 00000000000..e8e3a71f875
--- /dev/null
+++ b/changelogs/unreleased/29209-sign-up-form-name.yml
@@ -0,0 +1,4 @@
+---
+title: Change label for name on sign up form
+merge_request:
+author:
diff --git a/changelogs/unreleased/adam-prevent-two-issue-trackers.yml b/changelogs/unreleased/adam-prevent-two-issue-trackers.yml
new file mode 100644
index 00000000000..307b7ec7359
--- /dev/null
+++ b/changelogs/unreleased/adam-prevent-two-issue-trackers.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent more than one issue tracker to be active for the same project
+merge_request:
+author: luisdgs19
diff --git a/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml b/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml
new file mode 100644
index 00000000000..6365b1a1910
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml
@@ -0,0 +1,4 @@
+---
+title: Fix "passed with warnings" stage status on MySQL installations
+merge_request: 9802
+author:
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 720df0cac2d..b4f6cba1833 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -441,6 +441,16 @@ production: &base
shared:
# path: /mnt/gitlab # Default: shared
+ # Gitaly settings
+ gitaly:
+ # The socket_path setting is optional and obsolete. When this is set
+ # GitLab assumes it can reach a Gitaly services via a Unix socket at
+ # this path. When this is commented out GitLab will not use Gitaly.
+ #
+ # This setting is obsolete because we expect it to be moved under
+ # repositories/storages in GitLab 9.1.
+ #
+ # socket_path: tmp/sockets/gitaly.socket
#
# 4. Advanced settings
diff --git a/config/webpack.config.js b/config/webpack.config.js
index ff5f1412261..8e2b11a4145 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -133,8 +133,7 @@ var config = {
extensions: ['.js', '.es6', '.js.es6'],
alias: {
'~': path.join(ROOT_PATH, 'app/assets/javascripts'),
- 'emoji-map$': path.join(ROOT_PATH, 'fixtures/emojis/digests.json'),
- 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'),
+ 'emojis': path.join(ROOT_PATH, 'fixtures/emojis'),
'empty_states': path.join(ROOT_PATH, 'app/views/shared/empty_states'),
'icons': path.join(ROOT_PATH, 'app/views/shared/icons'),
'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md
index 463715e48ca..f6f50e2c571 100644
--- a/doc/administration/pages/source.md
+++ b/doc/administration/pages/source.md
@@ -17,14 +17,17 @@ Pages to the latest supported version.
## Prerequisites
-Before proceeding with the Pages configuration, you will need to:
-
-1. Have a separate domain under which the GitLab Pages will be served. In this
- document we assume that to be `example.io`.
-1. Configure a **wildcard DNS record**.
-1. (Optional) Have a **wildcard certificate** for that domain if you decide to
- serve Pages under HTTPS.
-1. (Optional but recommended) Enable [Shared runners](../../ci/runners/README.md)
+Before proceeding with the Pages configuration, make sure that:
+
+1. You have a separate domain under which GitLab Pages will be served. In
+ this document we assume that to be `example.io`.
+1. You have configured a **wildcard DNS record** for that domain.
+1. You have installed the `zip` and `unzip` packages in the same server that
+ GitLab is installed since they are needed to compress/uncompress the
+ Pages artifacts.
+1. (Optional) You have a **wildcard certificate** for the Pages domain if you
+ decide to serve Pages (`*.example.io`) under HTTPS.
+1. (Optional but recommended) You have configured and enabled the [Shared Runners][]
so that your users don't have to bring their own.
### DNS configuration
@@ -390,3 +393,4 @@ than GitLab to prevent XSS attacks.
[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[restart]: ../restart_gitlab.md#installations-from-source
[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4
+[shared runners]: ../../ci/runners/README.md
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
index 9ba820eaee5..d646de7c54a 100644
--- a/doc/development/frontend.md
+++ b/doc/development/frontend.md
@@ -16,6 +16,22 @@ minification, and compression of our assets.
[jQuery][jquery] is used throughout the application's JavaScript, with
[Vue.js][vue] for particularly advanced, dynamic elements.
+### Architecture
+
+The Frontend Architect is an expert who makes high-level frontend design choices
+and decides on technical standards, including coding standards, and frameworks.
+
+When you are assigned a new feature that requires architectural design,
+make sure it is discussed with one of the Frontend Architecture Experts.
+
+This rule also applies if you plan to change the architecture of an existing feature.
+
+These decisions should be accessible to everyone, so please document it on the Merge Request.
+
+You can find the Frontend Architecture experts on the [team page][team-page].
+
+You can find documentation about the desired architecture for a new feature built with Vue.js in [here][vue-section].
+
### Vue
For more complex frontend features, we recommend using Vue.js. It shares
@@ -238,8 +254,8 @@ readability.
See the relevant style guides for our guidelines and for information on linting:
- [SCSS][scss-style-guide]
-- JavaScript - We defer to [AirBnb][airbnb-js-style-guide] on most style-related
-conventions and enforce them with eslint. See [our current .eslintrc][eslistrc]
+- JavaScript - We defer to [AirBnb][airbnb-js-style-guide] on most style-related
+conventions and enforce them with eslint. See [our current .eslintrc][eslintrc]
for specific rules and patterns.
## Testing
@@ -439,3 +455,5 @@ Scenario: Developer can approve merge request
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
[airbnb-js-style-guide]: https://github.com/airbnb/javascript
[eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc
+[team-page]: https://about.gitlab.com/team
+[vue-section]: https://docs.gitlab.com/ce/development/frontend.html#how-to-build-a-new-feature-with-vue-js
diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md
index ead79ba6a10..794c8eb6bfe 100644
--- a/doc/development/ux_guide/copy.md
+++ b/doc/development/ux_guide/copy.md
@@ -167,6 +167,15 @@ A **comment** is a written piece of text that users of GitLab can create. Commen
#### Discussion
A **discussion** is a group of 1 or more comments. A discussion can include subdiscussions. Some discussions have the special capability of being able to be **resolved**. Both the comments in the discussion and the discussion itself can be resolved.
+## Confirmation dialogs
+
+- Destruction buttons should be clear and always say what they are destroying.
+ E.g., `Delete page` instead of just `Delete`.
+- If the copy describes another action the user can take instead of the
+ destructive one, provide a way for them to do that as a secondary button.
+- Avoid the word `cancel` or `canceled` in the descriptive copy. It can be
+ confusing when you then see the `Cancel` button.
+
---
Portions of this page are modifications based on work created and shared by the [Android Open Source Project][android project] and used according to terms described in the [Creative Commons 2.5 Attribution License][creative commons].
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 8e74970b8e9..177e1a9378b 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -288,9 +288,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-17-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-0-stable gitlab
-**Note:** You can change `8-17-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `9-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md
index 4cc8be752c4..1fe38cf8d2a 100644
--- a/doc/update/8.17-to-9.0.md
+++ b/doc/update/8.17-to-9.0.md
@@ -1,3 +1,162 @@
+# From 8.17 to 9.0
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+We will continue supporting Ruby < 2.3 for the time being but we recommend you
+upgrade to Ruby 2.3 if you're running a source installation, as this is the same
+version that ships with our Omnibus package.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
+echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
+cd ruby-2.3.3
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
+it has a minimum requirement of node v4.3.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v4.3.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+
+Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
+JavaScript dependencies.
+
+```bash
+curl --location https://yarnpkg.com/install.sh | bash -
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 9-0-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 9-0-stable-ee
+```
+
+### 6. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Install/update frontend asset dependencies
+sudo -u git -H npm install --production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 7. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
+```
+
+### 8. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v5.0.0
+```
+
+### 9. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/8-17-stable:config/gitlab.yml.example origin/9-0-stable:config/gitlab.yml.example
+```
+
#### Configuration changes for repository storages
This version introduces a new configuration structure for repository storages.
@@ -85,3 +244,78 @@ via [/etc/default/gitlab].
[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 10. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 11. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.17)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.16 to 8.17](8.16-to-8.17.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/config/gitlab.yml.example
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index e7f7edd95c7..abd263c1dfc 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -116,7 +116,8 @@ module API
finder_params = {
project_id: user_project.id,
- milestone_title: milestone.title
+ milestone_title: milestone.title,
+ sort: 'position_asc'
}
issues = IssuesFinder.new(current_user, finder_params).execute
@@ -138,7 +139,8 @@ module API
finder_params = {
project_id: user_project.id,
- milestone_id: milestone.id
+ milestone_id: milestone.id,
+ sort: 'position_asc'
}
merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index ecf6b6e068b..5476438b8fa 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -62,7 +62,7 @@ namespace :gitlab do
ref = Shellwords.escape(args[:ref])
- migrations = `git diff #{ref}.. --name-only -- db/migrate`.lines
+ migrations = `git diff #{ref}.. --diff-filter=A --name-only -- db/migrate`.lines
.map { |file| Rails.root.join(file.strip).to_s }
.select { |file| File.file?(file) }
diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb
index 7df102067d6..a6c72b0b3ac 100644
--- a/spec/features/merge_requests/diff_notes_avatars_spec.rb
+++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb
@@ -23,6 +23,56 @@ feature 'Diff note avatars', feature: true, js: true do
login_as user
end
+ context 'discussion tab' do
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not show avatars on discussion tab' do
+ expect(page).not_to have_selector('.js-avatar-container')
+ expect(page).not_to have_selector('.diff-comment-avatar-holders')
+ end
+
+ it 'does not render avatars after commening on discussion tab' do
+ click_button 'Reply...'
+
+ page.within('.js-discussion-note-form') do
+ find('.note-textarea').native.send_keys('Test comment')
+
+ click_button 'Comment'
+ end
+
+ expect(page).to have_content('Test comment')
+ expect(page).not_to have_selector('.js-avatar-container')
+ expect(page).not_to have_selector('.diff-comment-avatar-holders')
+ end
+ end
+
+ context 'commit view' do
+ before do
+ visit namespace_project_commit_path(project.namespace, project, merge_request.commits.first.id)
+ end
+
+ it 'does not render avatar after commenting' do
+ first('.diff-line-num').trigger('mouseover')
+ find('.js-add-diff-note-button').click
+
+ page.within('.js-discussion-note-form') do
+ find('.note-textarea').native.send_keys('test comment')
+
+ click_button 'Comment'
+
+ wait_for_ajax
+ end
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ expect(page).to have_content('test comment')
+ expect(page).not_to have_selector('.js-avatar-container')
+ expect(page).not_to have_selector('.diff-comment-avatar-holders')
+ end
+ end
+
%w(inline parallel).each do |view|
context "#{view} view" do
before do
diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb
index a1643fd1f43..7c319af893b 100644
--- a/spec/features/projects/edit_spec.rb
+++ b/spec/features/projects/edit_spec.rb
@@ -21,36 +21,28 @@ feature 'Project edit', feature: true, js: true do
expect(page).to have_selector('.merge-requests-feature', visible: false)
end
- it 'hides merge requests section after save' do
- select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level')
-
- expect(page).to have_selector('.merge-requests-feature', visible: false)
-
- click_button 'Save changes'
+ context 'given project with merge_requests_disabled access level' do
+ let(:project) { create(:project, :merge_requests_disabled) }
- wait_for_ajax
-
- expect(page).to have_selector('.merge-requests-feature', visible: false)
+ it 'hides merge requests section' do
+ expect(page).to have_selector('.merge-requests-feature', visible: false)
+ end
end
end
context 'builds select' do
- it 'hides merge requests section' do
+ it 'hides builds select section' do
select('Disabled', from: 'project_project_feature_attributes_builds_access_level')
expect(page).to have_selector('.builds-feature', visible: false)
end
- it 'hides merge requests section after save' do
- select('Disabled', from: 'project_project_feature_attributes_builds_access_level')
-
- expect(page).to have_selector('.builds-feature', visible: false)
+ context 'given project with builds_disabled access level' do
+ let(:project) { create(:project, :builds_disabled) }
- click_button 'Save changes'
-
- wait_for_ajax
-
- expect(page).to have_selector('.builds-feature', visible: false)
+ it 'hides builds select section' do
+ expect(page).to have_selector('.builds-feature', visible: false)
+ end
end
end
end
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index d8517e4d3d1..dc0a62ade50 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,8 +1,9 @@
/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */
-require('es6-promise').polyfill();
+import promisePolyfill from 'es6-promise';
+import AwardsHandler from '~/awards_handler';
-const AwardsHandler = require('~/awards_handler');
+promisePolyfill.polyfill();
(function() {
var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu;
diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js
index e94e220b19f..7ab0b37f2ec 100644
--- a/spec/javascripts/gl_emoji_spec.js
+++ b/spec/javascripts/gl_emoji_spec.js
@@ -1,16 +1,15 @@
+import '~/extensions/string';
+import '~/extensions/array';
-require('~/extensions/string');
-require('~/extensions/array');
-
-const glEmoji = require('~/behaviors/gl_emoji');
-
-const glEmojiTag = glEmoji.glEmojiTag;
-const isEmojiUnicodeSupported = glEmoji.isEmojiUnicodeSupported;
-const isFlagEmoji = glEmoji.isFlagEmoji;
-const isKeycapEmoji = glEmoji.isKeycapEmoji;
-const isSkinToneComboEmoji = glEmoji.isSkinToneComboEmoji;
-const isHorceRacingSkinToneComboEmoji = glEmoji.isHorceRacingSkinToneComboEmoji;
-const isPersonZwjEmoji = glEmoji.isPersonZwjEmoji;
+import { glEmojiTag } from '~/behaviors/gl_emoji';
+import {
+ isEmojiUnicodeSupported,
+ isFlagEmoji,
+ isKeycapEmoji,
+ isSkinToneComboEmoji,
+ isHorceRacingSkinToneComboEmoji,
+ isPersonZwjEmoji,
+} from '~/behaviors/gl_emoji/is_emoji_unicode_supported';
const emptySupportMap = {
personZwj: false,
diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
index 7cdade01e00..e504d41d4d4 100644
--- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
+++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
@@ -1,7 +1,7 @@
/* eslint-disable no-new */
-require('~/flash');
-require('~/mini_pipeline_graph_dropdown');
+import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
+import '~/flash';
(() => {
describe('Mini Pipeline Graph Dropdown', () => {
@@ -13,7 +13,7 @@ require('~/mini_pipeline_graph_dropdown');
describe('When is initialized', () => {
it('should initialize without errors when no options are given', () => {
- const miniPipelineGraph = new window.gl.MiniPipelineGraph();
+ const miniPipelineGraph = new MiniPipelineGraph();
expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container');
});
@@ -21,7 +21,7 @@ require('~/mini_pipeline_graph_dropdown');
it('should set the container as the given prop', () => {
const container = '.foo';
- const miniPipelineGraph = new window.gl.MiniPipelineGraph({ container });
+ const miniPipelineGraph = new MiniPipelineGraph({ container });
expect(miniPipelineGraph.container).toEqual(container);
});
@@ -29,9 +29,9 @@ require('~/mini_pipeline_graph_dropdown');
describe('When dropdown is clicked', () => {
it('should call getBuildsList', () => {
- const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {});
+ const getBuildsListSpy = spyOn(MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {});
- new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+ new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
document.querySelector('.js-builds-dropdown-button').click();
@@ -41,11 +41,32 @@ require('~/mini_pipeline_graph_dropdown');
it('should make a request to the endpoint provided in the html', () => {
const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {});
- new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+ new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
document.querySelector('.js-builds-dropdown-button').click();
expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
});
+
+ it('should not close when user uses cmd/ctrl + click', () => {
+ spyOn($, 'ajax').and.callFake(function (params) {
+ params.success({
+ html: `<li>
+ <a class="mini-pipeline-graph-dropdown-item" href="#">
+ <span class="ci-status-icon ci-status-icon-failed"></span>
+ <span class="ci-build-text">build</span>
+ </a>
+ <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a>
+ </li>`,
+ });
+ });
+ new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+
+ document.querySelector('.js-builds-dropdown-button').click();
+
+ document.querySelector('a.mini-pipeline-graph-dropdown-item').click();
+
+ expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true);
+ });
});
});
})();
diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js
new file mode 100644
index 00000000000..d966226909b
--- /dev/null
+++ b/spec/javascripts/pager_spec.js
@@ -0,0 +1,90 @@
+/* global fixture */
+
+require('~/pager');
+
+describe('pager', () => {
+ const Pager = window.Pager;
+
+ it('is defined on window', () => {
+ expect(window.Pager).toBeDefined();
+ });
+
+ describe('init', () => {
+ const originalHref = window.location.href;
+
+ beforeEach(() => {
+ setFixtures('<div class="content_list"></div><div class="loading"></div>');
+ spyOn($, 'ajax');
+ });
+
+ afterEach(() => {
+ window.history.replaceState({}, null, originalHref);
+ });
+
+ it('should use data-href attribute from list element', () => {
+ const href = `${gl.TEST_HOST}/some_list.json`;
+ setFixtures(`<div class="content_list" data-href="${href}"></div>`);
+ Pager.init();
+ expect(Pager.url).toBe(href);
+ });
+
+ it('should use current url if data-href attribute not provided', () => {
+ const href = `${gl.TEST_HOST}/some_list`;
+ spyOn(gl.utils, 'removeParams').and.returnValue(href);
+ Pager.init();
+ expect(Pager.url).toBe(href);
+ });
+
+ it('should get initial offset from query parameter', () => {
+ window.history.replaceState({}, null, '?offset=100');
+ Pager.init();
+ expect(Pager.offset).toBe(100);
+ });
+
+ it('keeps extra query parameters from url', () => {
+ window.history.replaceState({}, null, '?filter=test&offset=100');
+ const href = `${gl.TEST_HOST}/some_list?filter=test`;
+ spyOn(gl.utils, 'removeParams').and.returnValue(href);
+ Pager.init();
+ expect(gl.utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']);
+ expect(Pager.url).toEqual(href);
+ });
+ });
+
+ describe('getOld', () => {
+ beforeEach(() => {
+ setFixtures('<div class="content_list" data-href="/some_list"></div><div class="loading"></div>');
+ Pager.init();
+ });
+
+ it('shows loader while loading next page', () => {
+ spyOn(Pager.loading, 'show');
+ Pager.getOld();
+ expect(Pager.loading.show).toHaveBeenCalled();
+ });
+
+ it('hides loader on success', () => {
+ spyOn($, 'ajax').and.callFake(options => options.success({}));
+ spyOn(Pager.loading, 'hide');
+ Pager.getOld();
+ expect(Pager.loading.hide).toHaveBeenCalled();
+ });
+
+ it('hides loader on error', () => {
+ spyOn($, 'ajax').and.callFake(options => options.error());
+ spyOn(Pager.loading, 'hide');
+ Pager.getOld();
+ expect(Pager.loading.hide).toHaveBeenCalled();
+ });
+
+ it('sends request to url with offset and limit params', () => {
+ spyOn($, 'ajax');
+ Pager.offset = 100;
+ Pager.limit = 20;
+ Pager.getOld();
+ const [{ data, url }] = $.ajax.calls.argsFor(0);
+ expect(data).toBe('limit=20&offset=100');
+ expect(url).toBe('/some_list');
+ });
+ });
+});
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 3ea62df62f2..9962c987110 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -197,6 +197,24 @@ describe Ci::Pipeline, models: true do
end
end
end
+
+ context 'when there is a stage with warnings' do
+ before do
+ create(:commit_status, pipeline: pipeline,
+ stage: 'deploy',
+ name: 'prod:2',
+ stage_idx: 2,
+ status: 'failed',
+ allow_failure: true)
+ end
+
+ it 'populates stage with correct number of warnings' do
+ deploy_stage = pipeline.stages.third
+
+ expect(deploy_stage).not_to receive(:statuses)
+ expect(deploy_stage).to have_warnings
+ end
+ end
end
describe '#stages_count' do
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index c4a9743a4e2..c38faf32f7d 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -170,22 +170,31 @@ describe Ci::Stage, models: true do
context 'when stage has warnings' do
context 'when using memoized warnings flag' do
context 'when there are warnings' do
- let(:stage) { build(:ci_stage, warnings: true) }
+ let(:stage) { build(:ci_stage, warnings: 2) }
- it 'has memoized warnings' do
+ it 'returns true using memoized value' do
expect(stage).not_to receive(:statuses)
expect(stage).to have_warnings
end
end
context 'when there are no warnings' do
- let(:stage) { build(:ci_stage, warnings: false) }
+ let(:stage) { build(:ci_stage, warnings: 0) }
- it 'has memoized warnings' do
+ it 'returns false using memoized value' do
expect(stage).not_to receive(:statuses)
expect(stage).not_to have_warnings
end
end
+
+ context 'when number of warnings is not a valid value' do
+ let(:stage) { build(:ci_stage, warnings: true) }
+
+ it 'calculates statuses using database queries' do
+ expect(stage).to receive(:statuses).and_call_original
+ expect(stage).not_to have_warnings
+ end
+ end
end
context 'when calculating warnings from statuses' do
diff --git a/spec/models/project_services/issue_tracker_service_spec.rb b/spec/models/project_services/issue_tracker_service_spec.rb
new file mode 100644
index 00000000000..fbe6f344a98
--- /dev/null
+++ b/spec/models/project_services/issue_tracker_service_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe IssueTrackerService, models: true do
+ describe 'Validations' do
+ let(:project) { create :project }
+
+ describe 'only one issue tracker per project' do
+ let(:service) { RedmineService.new(project: project, active: true) }
+
+ before do
+ create(:service, project: project, active: true, category: 'issue_tracker')
+ end
+
+ context 'when service is changed manually by user' do
+ it 'executes the validation' do
+ valid = service.valid?(:manual_change)
+
+ expect(valid).to be_falsey
+ expect(service.errors[:base]).to include(
+ 'Another issue tracker is already in use. Only one issue tracker service can be active at a time'
+ )
+ end
+ end
+
+ context 'when service is changed internally' do
+ it 'does not execute the validation' do
+ expect(service.valid?).to be_truthy
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index 3bb8b6fdbeb..7fb728fed6f 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -243,8 +243,8 @@ describe API::Milestones, api: true do
describe 'confidential issues' do
let(:public_project) { create(:empty_project, :public) }
let(:milestone) { create(:milestone, project: public_project) }
- let(:issue) { create(:issue, project: public_project) }
- let(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
+ let(:issue) { create(:issue, project: public_project, position: 2) }
+ let(:confidential_issue) { create(:issue, confidential: true, project: public_project, position: 1) }
before do
public_project.team << [user, :developer]
@@ -283,11 +283,24 @@ describe API::Milestones, api: true do
expect(json_response.size).to eq(1)
expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
end
+
+ it 'returns issues ordered by position asc' do
+ get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(2)
+ expect(json_response.first['id']).to eq(confidential_issue.id)
+ expect(json_response.second['id']).to eq(issue.id)
+ end
end
end
describe 'GET /projects/:id/milestones/:milestone_id/merge_requests' do
- let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project, position: 2) }
+ let(:another_merge_request) { create(:merge_request, :simple, source_project: project, position: 1) }
+
before do
milestone.merge_requests << merge_request
end
@@ -320,5 +333,18 @@ describe API::Milestones, api: true do
expect(response).to have_http_status(401)
end
+
+ it 'returns merge_requests ordered by position asc' do
+ milestone.merge_requests << another_merge_request
+
+ get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(2)
+ expect(json_response.first['id']).to eq(another_merge_request.id)
+ expect(json_response.second['id']).to eq(merge_request.id)
+ end
end
end