summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2017-04-03 17:22:18 +0100
committerFilipa Lacerda <filipa@gitlab.com>2017-04-03 17:22:18 +0100
commit1b85c5a73fb4e7b466d3d871be7d7eb4b889b3ce (patch)
tree7fd8628974752a279cbe27c61983f8755d92cc51 /app/assets/javascripts
parent16cca3a0ea7f4b95e99d7b3e8d4953334fa7bec7 (diff)
parentb2700e64cce7c9b258e117a995eda8de00a8a988 (diff)
downloadgitlab-ce-1b85c5a73fb4e7b466d3d871be7d7eb4b889b3ce.tar.gz
Merge branch 'master' into tc-fix-unplayable-build-action-404
* master: (525 commits) Introduce "polling_interval_multiplier" as application setting fix spelling CI_REPOSITORY_URL (line:355) gitab-ci-token to gitlab-ci-token. Pass Gitaly Repository messages to workhorse Use gitaly 0.5.0 Fix specs Improve specs examples Minor refactor Fix BrachFormatter for removed users Changelog Fix specs One more change to the branch names to preserve metadata Prefixes source branch name with short SHA to avoid collision Fix GitHub importer for PRs of deleted forked repositories Change order of specs Clean history after every test that changes history Clean history state after each test Fixes method not replacing URL parameters correctly Fix a transient failure caused by FFaker Remove unnecessary ORDER BY clause when updating todos Add a wait_for_ajax call to ensure Todos page cleans up properly ...
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/activities.js3
-rw-r--r--app/assets/javascripts/awards_handler.js2
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js8
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/spread_string.js50
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js4
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js42
-rw-r--r--app/assets/javascripts/blob/blob_dockerfile_selector.js19
-rw-r--r--app/assets/javascripts/blob/blob_dockerfile_selectors.js27
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js121
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js23
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js26
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js28
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js23
-rw-r--r--app/assets/javascripts/blob/notebook/index.js85
-rw-r--r--app/assets/javascripts/blob/notebook_viewer.js3
-rw-r--r--app/assets/javascripts/blob/template_selector.js101
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js9
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js23
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js9
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js23
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js9
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js23
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_license_selector.js13
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_license_selectors.js24
-rw-r--r--app/assets/javascripts/blob/template_selectors/template_selector.js92
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js32
-rw-r--r--app/assets/javascripts/blob_edit/blob_edit_bundle.js15
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js177
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js9
-rw-r--r--app/assets/javascripts/boards/components/board.js2
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js4
-rw-r--r--app/assets/javascripts/boards/components/board_card.js5
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js3
-rw-r--r--app/assets/javascripts/boards/components/board_list.js4
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js3
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js8
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js7
-rw-r--r--app/assets/javascripts/boards/components/modal/filters.js4
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js3
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js20
-rw-r--r--app/assets/javascripts/boards/components/modal/list.js4
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.js3
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js5
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js6
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js2
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js3
-rw-r--r--app/assets/javascripts/boards/models/issue.js3
-rw-r--r--app/assets/javascripts/boards/models/list.js2
-rw-r--r--app/assets/javascripts/boards/services/board_service.js3
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js7
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js1
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js13
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_service.js44
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js160
-rw-r--r--app/assets/javascripts/commons/polyfills.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/limit_warning_component.js17
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_code_component.js4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_issue_component.js4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_plan_component.js7
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_production_component.js4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.js4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/total_time_component.js3
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js9
-rw-r--r--app/assets/javascripts/diff.js11
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js3
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js5
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js3
-rw-r--r--app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js3
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js3
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js3
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js2
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js4
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js3
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js7
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js3
-rw-r--r--app/assets/javascripts/dispatcher.js8
-rw-r--r--app/assets/javascripts/droplab/droplab_filter.js2
-rw-r--r--app/assets/javascripts/due_date_select.js2
-rw-r--r--app/assets/javascripts/environments/components/environment.js14
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js11
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js13
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js27
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.js31
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.js12
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.js16
-rw-r--r--app/assets/javascripts/environments/components/environments_table.js4
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.js15
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js3
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js1
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js3
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js28
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_tokenizer.js20
-rw-r--r--app/assets/javascripts/group_name.js42
-rw-r--r--app/assets/javascripts/groups_select.js78
-rw-r--r--app/assets/javascripts/header.js2
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js2
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js3
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js3
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/help_state.js3
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js3
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js3
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/time_tracker.js2
-rw-r--r--app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js7
-rw-r--r--app/assets/javascripts/issuable_context.js3
-rw-r--r--app/assets/javascripts/issue.js8
-rw-r--r--app/assets/javascripts/labels_select.js2
-rw-r--r--app/assets/javascripts/layout_nav.js5
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js43
-rw-r--r--app/assets/javascripts/lib/utils/poll.js100
-rw-r--r--app/assets/javascripts/main.js13
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js3
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js3
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js3
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js5
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js4
-rw-r--r--app/assets/javascripts/merge_request_tabs.js40
-rw-r--r--app/assets/javascripts/merge_request_widget.js4
-rw-r--r--app/assets/javascripts/milestone_select.js7
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js6
-rw-r--r--app/assets/javascripts/monitoring/prometheus_graph.js48
-rw-r--r--app/assets/javascripts/notes.js4
-rw-r--r--app/assets/javascripts/profile/profile.js1
-rw-r--r--app/assets/javascripts/project.js3
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_dropdown.js8
-rw-r--r--app/assets/javascripts/render_gfm.js3
-rw-r--r--app/assets/javascripts/render_math.js4
-rw-r--r--app/assets/javascripts/right_sidebar.js54
-rw-r--r--app/assets/javascripts/shortcuts.js4
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js3
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js3
-rw-r--r--app/assets/javascripts/subscription.js2
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js14
-rw-r--r--app/assets/javascripts/user.js3
-rw-r--r--app/assets/javascripts/user_callout.js49
-rw-r--r--app/assets/javascripts/user_tabs.js21
-rw-r--r--app/assets/javascripts/users_select.js7
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/async_button.js93
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/empty_state.js33
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/error_state.js19
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/nav_controls.js52
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js72
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js56
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js71
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js33
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/stage.js116
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/status.js60
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/time_ago.js71
-rw-r--r--app/assets/javascripts/vue_pipelines_index/event_hub.js3
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js25
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js123
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_url.js63
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js291
-rw-r--r--app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js44
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js119
-rw-r--r--app/assets/javascripts/vue_pipelines_index/status.js64
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js31
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js (renamed from app/assets/javascripts/commit/pipelines/pipelines_store.js)37
-rw-r--r--app/assets/javascripts/vue_pipelines_index/time_ago.js78
-rw-r--r--app/assets/javascripts/vue_shared/common_vue.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js307
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table.js80
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js393
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.js254
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js19
166 files changed, 2858 insertions, 2041 deletions
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index aebda7780e1..d816df831eb 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -1,6 +1,7 @@
/* eslint-disable no-param-reassign, class-methods-use-this */
/* global Pager */
-/* global Cookies */
+
+import Cookies from 'js-cookie';
class Activities {
constructor() {
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 9349918f7a0..c743dd551d7 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,4 +1,4 @@
-/* global Cookies */
+import Cookies from 'js-cookie';
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
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
index 5e3c45f7e92..20ab2d7e827 100644
--- a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js
+++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js
@@ -1,5 +1,3 @@
-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)
@@ -20,7 +18,7 @@ function isKeycapEmoji(emojiUnicode) {
const tone1 = 127995;// parseInt('1F3FB', 16)
const tone5 = 127999;// parseInt('1F3FF', 16)
function isSkinToneComboEmoji(emojiUnicode) {
- return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => {
+ return emojiUnicode.length > 2 && Array.from(emojiUnicode).some((char) => {
const cp = char.codePointAt(0);
return cp >= tone1 && cp <= tone5;
});
@@ -30,7 +28,7 @@ function isSkinToneComboEmoji(emojiUnicode) {
// 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 &&
+ return Array.from(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode);
}
@@ -42,7 +40,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16)
function isPersonZwjEmoji(emojiUnicode) {
let hasPersonEmoji = false;
let hasZwj = false;
- spreadString(emojiUnicode).forEach((character) => {
+ Array.from(emojiUnicode).forEach((character) => {
const cp = character.codePointAt(0);
if (cp === zwj) {
hasZwj = true;
diff --git a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js
deleted file mode 100644
index 327764ec6e9..00000000000
--- a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js
+++ /dev/null
@@ -1,50 +0,0 @@
-// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt#Fixing_charCodeAt()_to_handle_non-Basic-Multilingual-Plane_characters_if_their_presence_earlier_in_the_string_is_known
-function knownCharCodeAt(givenString, index) {
- const str = `${givenString}`;
- const end = str.length;
-
- const surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
- let idx = index;
- while ((surrogatePairs.exec(str)) != null) {
- const li = surrogatePairs.lastIndex;
- if (li - 2 < idx) {
- idx += 1;
- } else {
- break;
- }
- }
-
- if (idx >= end || idx < 0) {
- return NaN;
- }
-
- const code = str.charCodeAt(idx);
-
- let high;
- let low;
- if (code >= 0xD800 && code <= 0xDBFF) {
- high = code;
- low = str.charCodeAt(idx + 1);
- // Go one further, since one of the "characters" is part of a surrogate pair
- return ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
- }
- return code;
-}
-
-// See http://stackoverflow.com/a/38901550/796832
-// ES5/PhantomJS compatible version of spreading a string
-//
-// [...'foo'] -> ['f', 'o', 'o']
-// [...'🖐🏿'] -> ['🖐', '🏿']
-function spreadString(str) {
- const arr = [];
- let i = 0;
- while (!isNaN(knownCharCodeAt(str, i))) {
- const codePoint = knownCharCodeAt(str, i);
- arr.push(String.fromCodePoint(codePoint));
- i += 1;
- }
- return arr;
-}
-
-export default spreadString;
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 92f3bb3ff52..576b8a0425f 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -18,13 +18,13 @@
// Button does not change visibility. If button has icon - it changes chevron style.
//
// %div.js-toggle-container
- // %a.js-toggle-button
+ // %button.js-toggle-button
// %div.js-toggle-content
//
$('body').on('click', '.js-toggle-button', function(e) {
toggleContainer($(this).closest('.js-toggle-container'));
- const targetTag = e.target.tagName.toLowerCase();
+ const targetTag = e.currentTarget.tagName.toLowerCase();
if (targetTag === 'a' || targetTag === 'button') {
e.preventDefault();
}
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js b/app/assets/javascripts/blob/blob_ci_yaml.js
deleted file mode 100644
index ec1c018424d..00000000000
--- a/app/assets/javascripts/blob/blob_ci_yaml.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/* eslint-disable no-param-reassign, comma-dangle */
-/* global Api */
-
-require('./template_selector');
-
-((global) => {
- class BlobCiYamlSelector extends gl.TemplateSelector {
- requestFile(query) {
- return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
- }
-
- requestFileSuccess(file) {
- return super.requestFileSuccess(file);
- }
- }
-
- global.BlobCiYamlSelector = BlobCiYamlSelector;
-
- class BlobCiYamlSelectors {
- constructor({ editor, $dropdowns } = {}) {
- this.editor = editor;
- this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
- this.initSelectors();
- }
-
- initSelectors() {
- const editor = this.editor;
- this.$dropdowns.each((i, dropdown) => {
- const $dropdown = $(dropdown);
- return new BlobCiYamlSelector({
- editor,
- pattern: /(.gitlab-ci.yml)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
- dropdown: $dropdown
- });
- });
- }
- }
-
- global.BlobCiYamlSelectors = BlobCiYamlSelectors;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js b/app/assets/javascripts/blob/blob_dockerfile_selector.js
deleted file mode 100644
index d4f60cc6ecd..00000000000
--- a/app/assets/javascripts/blob/blob_dockerfile_selector.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/* global Api */
-
-require('./template_selector');
-
-(() => {
- const global = window.gl || (window.gl = {});
-
- class BlobDockerfileSelector extends gl.TemplateSelector {
- requestFile(query) {
- return Api.dockerfileYml(query.name, this.requestFileSuccess.bind(this));
- }
-
- requestFileSuccess(file) {
- return super.requestFileSuccess(file);
- }
- }
-
- global.BlobDockerfileSelector = BlobDockerfileSelector;
-})();
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selectors.js b/app/assets/javascripts/blob/blob_dockerfile_selectors.js
deleted file mode 100644
index 9cee79fa5d5..00000000000
--- a/app/assets/javascripts/blob/blob_dockerfile_selectors.js
+++ /dev/null
@@ -1,27 +0,0 @@
-(() => {
- const global = window.gl || (window.gl = {});
-
- class BlobDockerfileSelectors {
- constructor({ editor, $dropdowns } = {}) {
- this.editor = editor;
- this.$dropdowns = $dropdowns || $('.js-dockerfile-selector');
- this.initSelectors();
- }
-
- initSelectors() {
- const editor = this.editor;
- this.$dropdowns.each((i, dropdown) => {
- const $dropdown = $(dropdown);
- return new gl.BlobDockerfileSelector({
- editor,
- pattern: /(Dockerfile)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'),
- dropdown: $dropdown,
- });
- });
- }
- }
-
- global.BlobDockerfileSelectors = BlobDockerfileSelectors;
-})();
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 8f6bf162d6e..c9fe23aec75 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -1,66 +1,63 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, max-len */
+/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */
/* global Dropzone */
-(function() {
- this.BlobFileDropzone = (function() {
- function BlobFileDropzone(form, method) {
- var dropzone, form_dropzone, submitButton;
- form_dropzone = form.find('.dropzone');
- Dropzone.autoDiscover = false;
- dropzone = form_dropzone.dropzone({
- autoDiscover: false,
- autoProcessQueue: false,
- url: form.attr('action'),
- // Rails uses a hidden input field for PUT
- // http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails
- method: method,
- clickable: true,
- uploadMultiple: false,
- paramName: "file",
- maxFilesize: gon.max_file_size || 10,
- parallelUploads: 1,
- maxFiles: 1,
- addRemoveLinks: true,
- previewsContainer: '.dropzone-previews',
- headers: {
- "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
- },
- init: function() {
- this.on('addedfile', function(file) {
- $('.dropzone-alerts').html('').hide();
- });
- this.on('success', function(header, response) {
- window.location.href = response.filePath;
- });
- this.on('maxfilesexceeded', function(file) {
- this.removeFile(file);
- });
- return this.on('sending', function(file, xhr, formData) {
- formData.append('target_branch', form.find('input[name="target_branch"]').val());
- formData.append('create_merge_request', form.find('.js-create-merge-request').val());
- formData.append('commit_message', form.find('.js-commit-message').val());
- });
- },
- // Override behavior of adding error underneath preview
- error: function(file, errorMessage) {
- var stripped;
- stripped = $("<div/>").html(errorMessage).text();
- $('.dropzone-alerts').html('Error uploading file: \"' + stripped + '\"').show();
+export default class BlobFileDropzone {
+ constructor(form, method) {
+ const formDropzone = form.find('.dropzone');
+ Dropzone.autoDiscover = false;
+
+ const dropzone = formDropzone.dropzone({
+ autoDiscover: false,
+ autoProcessQueue: false,
+ url: form.attr('action'),
+ // Rails uses a hidden input field for PUT
+ // http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails
+ method: method,
+ clickable: true,
+ uploadMultiple: false,
+ paramName: 'file',
+ maxFilesize: gon.max_file_size || 10,
+ parallelUploads: 1,
+ maxFiles: 1,
+ addRemoveLinks: true,
+ previewsContainer: '.dropzone-previews',
+ headers: {
+ 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content'),
+ },
+ init: function () {
+ this.on('addedfile', function () {
+ $('.dropzone-alerts').html('').hide();
+ });
+ this.on('success', function (header, response) {
+ window.location.href = response.filePath;
+ });
+ this.on('maxfilesexceeded', function (file) {
this.removeFile(file);
- }
- });
- submitButton = form.find('#submit-all')[0];
- submitButton.addEventListener('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
- if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
- alert("Please select a file");
- }
- dropzone[0].dropzone.processQueue();
- return false;
- });
- }
+ });
+ this.on('sending', function (file, xhr, formData) {
+ formData.append('target_branch', form.find('input[name="target_branch"]').val());
+ formData.append('create_merge_request', form.find('.js-create-merge-request').val());
+ formData.append('commit_message', form.find('.js-commit-message').val());
+ });
+ },
+ // Override behavior of adding error underneath preview
+ error: function (file, errorMessage) {
+ const stripped = $('<div/>').html(errorMessage).text();
+ $('.dropzone-alerts').html(`Error uploading file: "${stripped}"`).show();
+ this.removeFile(file);
+ },
+ });
- return BlobFileDropzone;
- })();
-}).call(window);
+ const submitButton = form.find('#submit-all')[0];
+ submitButton.addEventListener('click', function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
+ // eslint-disable-next-line no-alert
+ alert('Please select a file');
+ }
+ dropzone[0].dropzone.processQueue();
+ return false;
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
deleted file mode 100644
index de20eab9cd1..00000000000
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params */
-/* global Api */
-
-require('./template_selector');
-
-(function() {
- var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
-
- this.BlobGitignoreSelector = (function(superClass) {
- extend(BlobGitignoreSelector, superClass);
-
- function BlobGitignoreSelector() {
- return BlobGitignoreSelector.__super__.constructor.apply(this, arguments);
- }
-
- BlobGitignoreSelector.prototype.requestFile = function(query) {
- return Api.gitignoreText(query.name, this.requestFileSuccess.bind(this));
- };
-
- return BlobGitignoreSelector;
- })(gl.TemplateSelector);
-}).call(window);
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js
deleted file mode 100644
index 43e5c0a5641..00000000000
--- a/app/assets/javascripts/blob/blob_gitignore_selectors.js
+++ /dev/null
@@ -1,26 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, max-len */
-/* global BlobGitignoreSelector */
-
-(function() {
- this.BlobGitignoreSelectors = (function() {
- function BlobGitignoreSelectors(opts) {
- var ref;
- this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitignore-selector'), this.editor = opts.editor;
- this.$dropdowns.each((function(_this) {
- return function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return new BlobGitignoreSelector({
- pattern: /(.gitignore)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
- dropdown: $dropdown,
- editor: _this.editor
- });
- };
- })(this));
- }
-
- return BlobGitignoreSelectors;
- })();
-}).call(window);
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
deleted file mode 100644
index b582052a76e..00000000000
--- a/app/assets/javascripts/blob/blob_license_selector.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle */
-/* global Api */
-
-require('./template_selector');
-
-(function() {
- var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
-
- this.BlobLicenseSelector = (function(superClass) {
- extend(BlobLicenseSelector, superClass);
-
- function BlobLicenseSelector() {
- return BlobLicenseSelector.__super__.constructor.apply(this, arguments);
- }
-
- BlobLicenseSelector.prototype.requestFile = function(query) {
- var data;
- data = {
- project: this.dropdown.data('project'),
- fullname: this.dropdown.data('fullname')
- };
- return Api.licenseText(query.id, data, this.requestFileSuccess.bind(this));
- };
-
- return BlobLicenseSelector;
- })(gl.TemplateSelector);
-}).call(window);
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js b/app/assets/javascripts/blob/blob_license_selectors.js
deleted file mode 100644
index c5067b0feae..00000000000
--- a/app/assets/javascripts/blob/blob_license_selectors.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable no-unused-vars, no-param-reassign */
-/* global BlobLicenseSelector */
-
-((global) => {
- class BlobLicenseSelectors {
- constructor({ $dropdowns, editor }) {
- this.$dropdowns = $('.js-license-selector');
- this.editor = editor;
- this.$dropdowns.each((i, dropdown) => {
- const $dropdown = $(dropdown);
- return new BlobLicenseSelector({
- editor,
- pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-license-selector-wrap'),
- dropdown: $dropdown,
- });
- });
- }
- }
-
- global.BlobLicenseSelectors = BlobLicenseSelectors;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js
new file mode 100644
index 00000000000..9b8bfbfc8c0
--- /dev/null
+++ b/app/assets/javascripts/blob/notebook/index.js
@@ -0,0 +1,85 @@
+/* eslint-disable no-new */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import NotebookLab from 'vendor/notebooklab';
+
+Vue.use(VueResource);
+Vue.use(NotebookLab);
+
+export default () => {
+ const el = document.getElementById('js-notebook-viewer');
+
+ new Vue({
+ el,
+ data() {
+ return {
+ error: false,
+ loadError: false,
+ loading: true,
+ json: {},
+ };
+ },
+ template: `
+ <div class="container-fluid md prepend-top-default append-bottom-default">
+ <div
+ class="text-center loading"
+ v-if="loading && !error">
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"
+ aria-label="iPython notebook loading">
+ </i>
+ </div>
+ <notebook-lab
+ v-if="!loading && !error"
+ :notebook="json"
+ code-css-class="code white" />
+ <p
+ class="text-center"
+ v-if="error">
+ <span v-if="loadError">
+ An error occured whilst loading the file. Please try again later.
+ </span>
+ <span v-else>
+ An error occured whilst parsing the file.
+ </span>
+ </p>
+ </div>
+ `,
+ methods: {
+ loadFile() {
+ this.$http.get(el.dataset.endpoint)
+ .then((res) => {
+ this.json = res.json();
+ this.loading = false;
+ })
+ .catch((e) => {
+ if (e.status) {
+ this.loadError = true;
+ }
+
+ this.error = true;
+ });
+ },
+ },
+ mounted() {
+ if (gon.katex_css_url) {
+ const katexStyles = document.createElement('link');
+ katexStyles.setAttribute('rel', 'stylesheet');
+ katexStyles.setAttribute('href', gon.katex_css_url);
+ document.head.appendChild(katexStyles);
+ }
+
+ if (gon.katex_js_url) {
+ const katexScript = document.createElement('script');
+ katexScript.addEventListener('load', () => {
+ this.loadFile();
+ });
+ katexScript.setAttribute('src', gon.katex_js_url);
+ document.head.appendChild(katexScript);
+ } else {
+ this.loadFile();
+ }
+ },
+ });
+};
diff --git a/app/assets/javascripts/blob/notebook_viewer.js b/app/assets/javascripts/blob/notebook_viewer.js
new file mode 100644
index 00000000000..b7a0a195a92
--- /dev/null
+++ b/app/assets/javascripts/blob/notebook_viewer.js
@@ -0,0 +1,3 @@
+import renderNotebook from './notebook';
+
+document.addEventListener('DOMContentLoaded', renderNotebook);
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
deleted file mode 100644
index 7e03ec3b391..00000000000
--- a/app/assets/javascripts/blob/template_selector.js
+++ /dev/null
@@ -1,101 +0,0 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, no-param-reassign, max-len */
-
-((global) => {
- class TemplateSelector {
- constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
- this.onClick = this.onClick.bind(this);
- this.dropdown = dropdown;
- this.data = data;
- this.pattern = pattern;
- this.wrapper = wrapper;
- this.editor = editor;
- this.fileEndpoint = fileEndpoint;
- this.$input = $input || $('#file_name');
- this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
- this.buildDropdown();
- this.bindEvents();
- this.onFilenameUpdate();
-
- this.autosizeUpdateEvent = document.createEvent('Event');
- this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
- }
-
- buildDropdown() {
- return this.dropdown.glDropdown({
- data: this.data,
- filterable: true,
- selectable: true,
- toggleLabel: this.toggleLabel,
- search: {
- fields: ['name']
- },
- clicked: this.onClick,
- text: function(item) {
- return item.name;
- }
- });
- }
-
- bindEvents() {
- return this.$input.on('keyup blur', (e) => this.onFilenameUpdate());
- }
-
- toggleLabel(item) {
- return item.name;
- }
-
- onFilenameUpdate() {
- var filenameMatches;
- if (!this.$input.length) {
- return;
- }
- filenameMatches = this.pattern.test(this.$input.val().trim());
- if (!filenameMatches) {
- this.wrapper.addClass('hidden');
- return;
- }
- return this.wrapper.removeClass('hidden');
- }
-
- onClick(item, el, e) {
- e.preventDefault();
- return this.requestFile(item);
- }
-
- requestFile(item) {
- // This `requestFile` method is an abstract method that should
- // be added by all subclasses.
- }
-
- // To be implemented on the extending class
- // e.g.
- // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
- requestFileSuccess(file, { skipFocus } = {}) {
- if (!file) return;
-
- const oldValue = this.editor.getValue();
- const newValue = file.content;
-
- this.editor.setValue(newValue, 1);
- if (!skipFocus) this.editor.focus();
-
- if (this.editor instanceof jQuery) {
- this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
- }
- }
-
- startLoadingSpinner() {
- this.dropdownIcon
- .addClass('fa-spinner fa-spin')
- .removeClass('fa-chevron-down');
- }
-
- stopLoadingSpinner() {
- this.dropdownIcon
- .addClass('fa-chevron-down')
- .removeClass('fa-spinner fa-spin');
- }
- }
-
- global.TemplateSelector = TemplateSelector;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js
new file mode 100644
index 00000000000..5a5954e7751
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js
@@ -0,0 +1,9 @@
+/* global Api */
+
+import TemplateSelector from './template_selector';
+
+export default class BlobCiYamlSelector extends TemplateSelector {
+ requestFile(query) {
+ return Api.gitlabCiYml(query.name, (file, config) => this.setEditorContent(file, config));
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js
new file mode 100644
index 00000000000..7a4d6a42a03
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js
@@ -0,0 +1,23 @@
+/* global Api */
+
+import BlobCiYamlSelector from './blob_ci_yaml_selector';
+
+export default class BlobCiYamlSelectors {
+ constructor({ editor, $dropdowns }) {
+ this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
+ this.initSelectors(editor);
+ }
+
+ initSelectors(editor) {
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+ return new BlobCiYamlSelector({
+ editor,
+ pattern: /(.gitlab-ci.yml)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
+ dropdown: $dropdown,
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js
new file mode 100644
index 00000000000..19f8820a0cb
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js
@@ -0,0 +1,9 @@
+/* global Api */
+
+import TemplateSelector from './template_selector';
+
+export default class BlobDockerfileSelector extends TemplateSelector {
+ requestFile(query) {
+ return Api.dockerfileYml(query.name, (file, config) => this.setEditorContent(file, config));
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js
new file mode 100644
index 00000000000..da067035b43
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js
@@ -0,0 +1,23 @@
+import BlobDockerfileSelector from './blob_dockerfile_selector';
+
+export default class BlobDockerfileSelectors {
+ constructor({ editor, $dropdowns }) {
+ this.editor = editor;
+ this.$dropdowns = $dropdowns || $('.js-dockerfile-selector');
+ this.initSelectors();
+ }
+
+ initSelectors() {
+ const editor = this.editor;
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+ return new BlobDockerfileSelector({
+ editor,
+ pattern: /(Dockerfile)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'),
+ dropdown: $dropdown,
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js
new file mode 100644
index 00000000000..0b6b02fc2b3
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js
@@ -0,0 +1,9 @@
+/* global Api */
+
+import TemplateSelector from './template_selector';
+
+export default class BlobGitignoreSelector extends TemplateSelector {
+ requestFile(query) {
+ return Api.gitignoreText(query.name, (file, config) => this.setEditorContent(file, config));
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js
new file mode 100644
index 00000000000..dc485d97677
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js
@@ -0,0 +1,23 @@
+import BlobGitignoreSelector from './blob_gitignore_selector';
+
+export default class BlobGitignoreSelectors {
+ constructor({ editor, $dropdowns }) {
+ this.$dropdowns = $dropdowns || $('.js-gitignore-selector');
+ this.editor = editor;
+ this.initSelectors();
+ }
+
+ initSelectors() {
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+
+ return new BlobGitignoreSelector({
+ pattern: /(.gitignore)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
+ dropdown: $dropdown,
+ editor: this.editor,
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_license_selector.js b/app/assets/javascripts/blob/template_selectors/blob_license_selector.js
new file mode 100644
index 00000000000..e9cb31cc2dc
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_license_selector.js
@@ -0,0 +1,13 @@
+/* global Api */
+
+import TemplateSelector from './template_selector';
+
+export default class BlobLicenseSelector extends TemplateSelector {
+ requestFile(query) {
+ const data = {
+ project: this.dropdown.data('project'),
+ fullname: this.dropdown.data('fullname'),
+ };
+ return Api.licenseText(query.id, data, (file, config) => this.setEditorContent(file, config));
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js
new file mode 100644
index 00000000000..a44f4f78b2d
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js
@@ -0,0 +1,24 @@
+/* eslint-disable no-unused-vars, no-param-reassign */
+
+import BlobLicenseSelector from './blob_license_selector';
+
+export default class BlobLicenseSelectors {
+ constructor({ $dropdowns, editor }) {
+ this.$dropdowns = $dropdowns || $('.js-license-selector');
+ this.initSelectors(editor);
+ }
+
+ initSelectors(editor) {
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+
+ return new BlobLicenseSelector({
+ editor,
+ pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-license-selector-wrap'),
+ dropdown: $dropdown,
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/template_selector.js b/app/assets/javascripts/blob/template_selectors/template_selector.js
new file mode 100644
index 00000000000..d7c1c32efbd
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/template_selector.js
@@ -0,0 +1,92 @@
+/* eslint-disable class-methods-use-this, no-unused-vars */
+
+export default class TemplateSelector {
+ constructor({ dropdown, data, pattern, wrapper, editor, $input } = {}) {
+ this.pattern = pattern;
+ this.editor = editor;
+ this.dropdown = dropdown;
+ this.$dropdownContainer = wrapper;
+ this.$filenameInput = $input || $('#file_name');
+ this.$dropdownIcon = $('.fa-chevron-down', dropdown);
+
+ this.initDropdown(dropdown, data);
+ this.listenForFilenameInput();
+ this.renderMatchedDropdown();
+ this.initAutosizeUpdateEvent();
+ }
+
+ initDropdown(dropdown, data) {
+ return $(dropdown).glDropdown({
+ data,
+ filterable: true,
+ selectable: true,
+ toggleLabel: item => item.name,
+ search: {
+ fields: ['name'],
+ },
+ clicked: (item, el, e) => this.fetchFileTemplate(item, el, e),
+ text: item => item.name,
+ });
+ }
+
+ initAutosizeUpdateEvent() {
+ this.autosizeUpdateEvent = document.createEvent('Event');
+ this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
+ }
+
+ listenForFilenameInput() {
+ return this.$filenameInput.on('keyup blur', e => this.renderMatchedDropdown(e));
+ }
+
+ renderMatchedDropdown() {
+ if (!this.$filenameInput.length) {
+ return null;
+ }
+
+ const filenameMatches = this.pattern.test(this.$filenameInput.val().trim());
+
+ if (!filenameMatches) {
+ return this.$dropdownContainer.addClass('hidden');
+ }
+ return this.$dropdownContainer.removeClass('hidden');
+ }
+
+ fetchFileTemplate(item, el, e) {
+ e.preventDefault();
+ return this.requestFile(item);
+ }
+
+ requestFile(item) {
+ // This `requestFile` method is an abstract method that should
+ // be added by all subclasses.
+ }
+
+ // To be implemented on the extending class
+ // e.g. Api.gitlabCiYml(query.name, file => this.setEditorContent(file));
+
+ setEditorContent(file, { skipFocus } = {}) {
+ if (!file) return;
+
+ const newValue = file.content;
+
+ this.editor.setValue(newValue, 1);
+
+ if (!skipFocus) this.editor.focus();
+
+ if (this.editor instanceof jQuery) {
+ this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
+ }
+ }
+
+ startLoadingSpinner() {
+ this.$dropdownIcon
+ .addClass('fa-spinner fa-spin')
+ .removeClass('fa-chevron-down');
+ }
+
+ stopLoadingSpinner() {
+ this.$dropdownIcon
+ .addClass('fa-chevron-down')
+ .removeClass('fa-spinner fa-spin');
+ }
+}
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
new file mode 100644
index 00000000000..c5deccf631e
--- /dev/null
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -0,0 +1,32 @@
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
+/* global EditBlob */
+/* global NewCommitForm */
+
+import EditBlob from './edit_blob';
+import BlobFileDropzone from '../blob/blob_file_dropzone';
+
+$(() => {
+ const editBlobForm = $('.js-edit-blob-form');
+ const uploadBlobForm = $('.js-upload-blob-form');
+
+ if (editBlobForm.length) {
+ const urlRoot = editBlobForm.data('relative-url-root');
+ const assetsPath = editBlobForm.data('assets-prefix');
+ const blobLanguage = editBlobForm.data('blob-language');
+
+ new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage);
+ new NewCommitForm(editBlobForm);
+ }
+
+ if (uploadBlobForm.length) {
+ const method = uploadBlobForm.data('method');
+
+ new BlobFileDropzone(uploadBlobForm, method);
+ new NewCommitForm(uploadBlobForm);
+
+ window.gl.utils.disableButtonIfEmptyField(
+ uploadBlobForm.find('.js-commit-message'),
+ '.btn-upload-file',
+ );
+ }
+});
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
deleted file mode 100644
index 0436bbb0eaf..00000000000
--- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
-/* global EditBlob */
-/* global NewCommitForm */
-
-require('./edit_blob');
-
-(function() {
- $(function() {
- var url = $(".js-edit-blob-form").data("relative-url-root");
- url += $(".js-edit-blob-form").data("assets-prefix");
-
- var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language'));
- new NewCommitForm($('.js-edit-blob-form'));
- });
-}).call(window);
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index a1127b9e30e..d3560d5df3b 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,88 +1,99 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, max-len */
/* global ace */
-/* global BlobGitignoreSelectors */
-
-(function() {
- var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
-
- this.EditBlob = (function() {
- function EditBlob(assets_path, ace_mode) {
- if (ace_mode == null) {
- ace_mode = null;
- }
- this.editModeLinkClickHandler = bind(this.editModeLinkClickHandler, this);
- ace.config.set("modePath", assets_path + "/ace");
- ace.config.loadModule("ace/ext/searchbox");
- this.editor = ace.edit("editor");
- this.editor.focus();
- if (ace_mode) {
- this.editor.getSession().setMode("ace/mode/" + ace_mode);
- }
- $('form').submit((function(_this) {
- return function() {
- return $("#file-content").val(_this.editor.getValue());
- };
- // Before a form submission, move the content from the Ace editor into the
- // submitted textarea
- })(this));
- this.initModePanesAndLinks();
- this.initSoftWrap();
- new gl.BlobLicenseSelectors({
- editor: this.editor
- });
+
+import BlobLicenseSelectors from '../blob/template_selectors/blob_license_selectors';
+import BlobGitignoreSelectors from '../blob/template_selectors/blob_gitignore_selectors';
+import BlobCiYamlSelectors from '../blob/template_selectors/blob_ci_yaml_selectors';
+import BlobDockerfileSelectors from '../blob/template_selectors/blob_dockerfile_selectors';
+
+export default class EditBlob {
+ constructor(assetsPath, aceMode) {
+ this.configureAceEditor(aceMode, assetsPath);
+ this.prepFileContentForSubmit();
+ this.initModePanesAndLinks();
+ this.initSoftWrap();
+ this.initFileSelectors();
+ }
+
+ configureAceEditor(aceMode, assetsPath) {
+ ace.config.set('modePath', `${assetsPath}/ace`);
+ ace.config.loadModule('ace/ext/searchbox');
+
+ this.editor = ace.edit('editor');
+ this.editor.focus();
+
+ if (aceMode) {
+ this.editor.getSession().setMode(`ace/mode/${aceMode}`);
+ }
+ }
+
+ prepFileContentForSubmit() {
+ $('form').submit(() => {
+ $('#file-content').val(this.editor.getValue());
+ });
+ }
+
+ initFileSelectors() {
+ this.blobTemplateSelectors = [
+ new BlobLicenseSelectors({
+ editor: this.editor,
+ }),
new BlobGitignoreSelectors({
- editor: this.editor
- });
- new gl.BlobCiYamlSelectors({
- editor: this.editor
- });
- new gl.BlobDockerfileSelectors({
- editor: this.editor
+ editor: this.editor,
+ }),
+ new BlobCiYamlSelectors({
+ editor: this.editor,
+ }),
+ new BlobDockerfileSelectors({
+ editor: this.editor,
+ }),
+ ];
+ }
+
+ initModePanesAndLinks() {
+ this.$editModePanes = $('.js-edit-mode-pane');
+ this.$editModeLinks = $('.js-edit-mode a');
+ this.$editModeLinks.on('click', e => this.editModeLinkClickHandler(e));
+ }
+
+ editModeLinkClickHandler(e) {
+ e.preventDefault();
+
+ const currentLink = $(e.target);
+ const paneId = currentLink.attr('href');
+ const currentPane = this.$editModePanes.filter(paneId);
+
+ this.$editModeLinks.parent().removeClass('active hover');
+
+ currentLink.parent().addClass('active hover');
+
+ this.$editModePanes.hide();
+
+ currentPane.fadeIn(200);
+
+ if (paneId === '#preview') {
+ this.$toggleButton.hide();
+ return $.post(currentLink.data('preview-url'), {
+ content: this.editor.getValue(),
+ }, (response) => {
+ currentPane.empty().append(response);
+ return currentPane.renderGFM();
});
}
- EditBlob.prototype.initModePanesAndLinks = function() {
- this.$editModePanes = $(".js-edit-mode-pane");
- this.$editModeLinks = $(".js-edit-mode a");
- return this.$editModeLinks.click(this.editModeLinkClickHandler);
- };
-
- EditBlob.prototype.editModeLinkClickHandler = function(event) {
- var currentLink, currentPane, paneId;
- event.preventDefault();
- currentLink = $(event.target);
- paneId = currentLink.attr("href");
- currentPane = this.$editModePanes.filter(paneId);
- this.$editModeLinks.parent().removeClass("active hover");
- currentLink.parent().addClass("active hover");
- this.$editModePanes.hide();
- currentPane.fadeIn(200);
- if (paneId === "#preview") {
- this.$toggleButton.hide();
- return $.post(currentLink.data("preview-url"), {
- content: this.editor.getValue()
- }, function(response) {
- currentPane.empty().append(response);
- return currentPane.renderGFM();
- });
- } else {
- this.$toggleButton.show();
- return this.editor.focus();
- }
- };
-
- EditBlob.prototype.initSoftWrap = function() {
- this.isSoftWrapped = false;
- this.$toggleButton = $('.soft-wrap-toggle');
- this.$toggleButton.on('click', this.toggleSoftWrap.bind(this));
- };
-
- EditBlob.prototype.toggleSoftWrap = function(e) {
- this.isSoftWrapped = !this.isSoftWrapped;
- this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
- this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
- };
-
- return EditBlob;
- })();
-}).call(window);
+ this.$toggleButton.show();
+
+ return this.editor.focus();
+ }
+
+ initSoftWrap() {
+ this.isSoftWrapped = false;
+ this.$toggleButton = $('.soft-wrap-toggle');
+ this.$toggleButton.on('click', () => this.toggleSoftWrap());
+ }
+
+ toggleSoftWrap() {
+ this.isSoftWrapped = !this.isSoftWrapped;
+ this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
+ this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
+ }
+}
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index 3874c2819a5..e057ac8df02 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -1,12 +1,11 @@
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
-/* global Vue */
/* global BoardService */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
require('./models/issue');
require('./models/label');
require('./models/list');
@@ -24,6 +23,8 @@ require('./components/new_list_dropdown');
require('./components/modal/index');
require('../vue_shared/vue_resource_interceptor');
+Vue.use(VueResource);
+
$(() => {
const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore;
@@ -78,7 +79,7 @@ $(() => {
resp.json().forEach((board) => {
const list = Store.addList(board);
- if (list.type === 'done') {
+ if (list.type === 'closed') {
list.position = Infinity;
}
});
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 67c0c419713..35b3205cca7 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -1,7 +1,7 @@
/* eslint-disable comma-dangle, space-before-function-paren, one-var */
-/* global Vue */
/* global Sortable */
+import Vue from 'vue';
import boardBlankState from './board_blank_state';
require('./board_delete');
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.js
index 52893d4642b..3fc68457961 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js
+++ b/app/assets/javascripts/boards/components/board_blank_state.js
@@ -1,5 +1,7 @@
/* global ListLabel */
-/* global Cookies */
+
+import Cookies from 'js-cookie';
+
const Store = gl.issueBoards.BoardsStore;
export default {
diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js
index 4b72090df31..f591134c548 100644
--- a/app/assets/javascripts/boards/components/board_card.js
+++ b/app/assets/javascripts/boards/components/board_card.js
@@ -1,4 +1,3 @@
-/* global Vue */
require('./issue_card_inner');
const Store = gl.issueBoards.BoardsStore;
@@ -51,9 +50,7 @@ export default {
this.showDetail = false;
},
showIssue(e) {
- const targetTagName = e.target.tagName.toLowerCase();
-
- if (targetTagName === 'a' || targetTagName === 'button') return;
+ if (e.target.classList.contains('js-no-trigger')) return;
if (this.showDetail) {
this.showDetail = false;
diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js
index 861600424a5..af621cfd57f 100644
--- a/app/assets/javascripts/boards/components/board_delete.js
+++ b/app/assets/javascripts/boards/components/board_delete.js
@@ -1,5 +1,6 @@
/* eslint-disable comma-dangle, space-before-function-paren, no-alert */
-/* global Vue */
+
+import Vue from 'vue';
(() => {
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js
index 1330d4ae840..86e6c26e570 100644
--- a/app/assets/javascripts/boards/components/board_list.js
+++ b/app/assets/javascripts/boards/components/board_list.js
@@ -1,7 +1,7 @@
/* eslint-disable comma-dangle, space-before-function-paren, max-len */
-/* global Vue */
/* global Sortable */
+import Vue from 'vue';
import boardNewIssue from './board_new_issue';
import boardCard from './board_card';
@@ -48,7 +48,7 @@ import boardCard from './board_card';
this.list.getIssues(false);
}
- if (this.scrollHeight() > this.listHeight()) {
+ if (this.scrollHeight() > Math.ceil(this.listHeight())) {
this.showCount = true;
} else {
this.showCount = false;
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index dfc6eed785c..3c080008244 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -1,10 +1,11 @@
/* eslint-disable comma-dangle, space-before-function-paren, no-new */
-/* global Vue */
/* global IssuableContext */
/* global MilestoneSelect */
/* global LabelsSelect */
/* global Sidebar */
+import Vue from 'vue';
+
require('./sidebar/remove_issue');
(() => {
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index 69e30cec4c5..a4629b092bf 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -1,4 +1,4 @@
-/* global Vue */
+import Vue from 'vue';
import eventHub from '../eventhub';
(() => {
@@ -84,20 +84,20 @@ import eventHub from '../eventhub';
#{{ issue.id }}
</span>
<a
- class="card-assignee has-tooltip"
+ class="card-assignee has-tooltip js-no-trigger"
:href="rootPath + issue.assignee.username"
:title="'Assigned to ' + issue.assignee.name"
v-if="issue.assignee"
data-container="body">
<img
- class="avatar avatar-inline s20"
+ class="avatar avatar-inline s20 js-no-trigger"
:src="issue.assignee.avatar"
width="20"
height="20"
:alt="'Avatar for ' + issue.assignee.name" />
</a>
<button
- class="label color-label has-tooltip"
+ class="label color-label has-tooltip js-no-trigger"
v-for="label in issue.labels"
type="button"
v-if="showLabel(label)"
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js
index 9538f5b69e9..823319df6e7 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.js
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js
@@ -1,4 +1,5 @@
-/* global Vue */
+import Vue from 'vue';
+
(() => {
const ModalStore = gl.issueBoards.ModalStore;
@@ -30,7 +31,7 @@
if (this.activeTab === 'selected') {
obj.title = 'You haven\'t selected any issues yet';
obj.content = `
- Go back to <strong>All issues</strong> and select some issues
+ Go back to <strong>Open issues</strong> and select some issues
to add to your board.
`;
}
@@ -59,7 +60,7 @@
class="btn btn-default"
@click="changeTab('all')"
v-if="activeTab === 'selected'">
- All issues
+ Open issues
</button>
</div>
</div>
diff --git a/app/assets/javascripts/boards/components/modal/filters.js b/app/assets/javascripts/boards/components/modal/filters.js
index bd394a2318c..b214b5a7199 100644
--- a/app/assets/javascripts/boards/components/modal/filters.js
+++ b/app/assets/javascripts/boards/components/modal/filters.js
@@ -14,8 +14,10 @@ export default {
this.filteredSearch = new FilteredSearchBoards(this.store);
this.filteredSearch.removeTokens();
+ this.filteredSearch.handleInputPlaceholder();
+ this.filteredSearch.toggleClearSearchButton();
},
- beforeDestroy() {
+ destroyed() {
this.filteredSearch.cleanup();
FilteredSearchContainer.container = document;
this.store.path = '';
diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js
index 1cbc422c961..887ce373096 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ b/app/assets/javascripts/boards/components/modal/footer.js
@@ -1,7 +1,8 @@
/* eslint-disable no-new */
-/* global Vue */
/* global Flash */
+import Vue from 'vue';
+
require('./lists_dropdown');
(() => {
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
index 1b66c8b922d..91c08cde13a 100644
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -1,5 +1,6 @@
-/* global Vue */
/* global ListIssue */
+
+import Vue from 'vue';
import queryData from '../../utils/query_data';
require('./header');
@@ -64,7 +65,15 @@ require('./empty_state');
},
filter: {
handler() {
- this.loadIssues(true);
+ if (this.$el.tagName) {
+ this.page = 1;
+ this.filterLoading = true;
+
+ this.loadIssues(true)
+ .then(() => {
+ this.filterLoading = false;
+ });
+ }
},
deep: true,
},
@@ -115,6 +124,9 @@ require('./empty_state');
return this.activeTab === 'selected' && this.selectedIssues.length === 0;
},
},
+ created() {
+ this.page = 1;
+ },
components: {
'modal-header': gl.issueBoards.ModalHeader,
'modal-list': gl.issueBoards.ModalList,
@@ -135,14 +147,14 @@ require('./empty_state');
:image="blankStateImage"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
- v-if="!loading && showList"></modal-list>
+ v-if="!loading && showList && !filterLoading"></modal-list>
<empty-state
v-if="showEmptyState"
:image="blankStateImage"
:new-issue-path="newIssuePath"></empty-state>
<section
class="add-issues-list text-center"
- v-if="loading">
+ v-if="loading || filterLoading">
<div class="add-issues-list-loading">
<i class="fa fa-spinner fa-spin"></i>
</div>
diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js
index 3730c1ecaeb..aba56d4aa31 100644
--- a/app/assets/javascripts/boards/components/modal/list.js
+++ b/app/assets/javascripts/boards/components/modal/list.js
@@ -1,6 +1,8 @@
-/* global Vue */
/* global ListIssue */
/* global bp */
+
+import Vue from 'vue';
+
(() => {
const ModalStore = gl.issueBoards.ModalStore;
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
index 3c05120a2da..9e9ed46ab8d 100644
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
@@ -1,4 +1,5 @@
-/* global Vue */
+import Vue from 'vue';
+
(() => {
const ModalStore = gl.issueBoards.ModalStore;
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js
index e8cb43f3503..23cb1b13d11 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.js
+++ b/app/assets/javascripts/boards/components/modal/tabs.js
@@ -1,4 +1,5 @@
-/* global Vue */
+import Vue from 'vue';
+
(() => {
const ModalStore = gl.issueBoards.ModalStore;
@@ -23,7 +24,7 @@
href="#"
role="button"
@click.prevent="changeTab('all')">
- All issues
+ Open issues
<span class="badge">
{{ issuesCount }}
</span>
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
index e74935e1cb0..772ea4c5565 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
@@ -1,6 +1,8 @@
/* eslint-disable no-new */
-/* global Vue */
/* global Flash */
+
+import Vue from 'vue';
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -46,7 +48,7 @@
template: `
<div
class="block list"
- v-if="list.type !== 'done'">
+ v-if="list.type !== 'closed'">
<button
class="btn btn-default btn-block"
type="button"
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index 101732309ea..1264280284c 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -28,6 +28,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
[].forEach.call(tokens, (el) => {
el.parentNode.removeChild(el);
});
+
+ this.filteredSearchInput.value = '';
}
updateTokens() {
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js b/app/assets/javascripts/boards/filters/due_date_filters.js
index 03425bb145b..70132dbfa6f 100644
--- a/app/assets/javascripts/boards/filters/due_date_filters.js
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js
@@ -1,6 +1,7 @@
-/* global Vue */
/* global dateFormat */
+import Vue from 'vue';
+
Vue.filter('due-date', (value) => {
const date = new Date(value);
return dateFormat(date, 'mmm d, yyyy', true);
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index ca5e6fa7e9d..d6175069e37 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -1,9 +1,10 @@
/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */
-/* global Vue */
/* global ListLabel */
/* global ListMilestone */
/* global ListUser */
+import Vue from 'vue';
+
class ListIssue {
constructor (obj) {
this.globalId = obj.id;
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index f18ad2a0fac..91e5fb2a666 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -10,7 +10,7 @@ class List {
this.position = obj.position;
this.title = obj.title;
this.type = obj.list_type;
- this.preset = ['done', 'blank'].indexOf(this.type) > -1;
+ this.preset = ['closed', 'blank'].indexOf(this.type) > -1;
this.page = 1;
this.loading = true;
this.loadingMore = false;
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index e54102814d6..db9bced2f89 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -1,5 +1,6 @@
/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */
-/* global Vue */
+
+import Vue from 'vue';
class BoardService {
constructor (root, bulkUpdatePath, boardId) {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 28ecb322df7..bcda70d0638 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -1,7 +1,8 @@
/* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */
-/* global Cookies */
/* global List */
+import Cookies from 'js-cookie';
+
(() => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
@@ -44,7 +45,7 @@
},
shouldAddBlankState () {
// Decide whether to add the blank state
- return !(this.state.lists.filter(list => list.type !== 'done')[0]);
+ return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
},
addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
@@ -97,7 +98,7 @@
issueTo.removeLabel(listFrom.label);
}
- if (listTo.type === 'done') {
+ if (listTo.type === 'closed') {
issueLists.forEach((list) => {
list.removeIssue(issue);
});
diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js
index 7ee266a831f..9b009483a3c 100644
--- a/app/assets/javascripts/boards/stores/modal_store.js
+++ b/app/assets/javascripts/boards/stores/modal_store.js
@@ -15,6 +15,7 @@
searchTerm: '',
loading: false,
loadingNewPage: false,
+ filterLoading: false,
page: 1,
perPage: 50,
filter: {
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index b5a988df897..a92e068ca5a 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -1,8 +1,11 @@
-/* eslint-disable no-new, no-param-reassign */
-/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
+/* eslint-disable no-param-reassign */
+
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import CommitPipelinesTable from './pipelines_table';
+
+Vue.use(VueResource);
-window.Vue = require('vue');
-require('./pipelines_table');
/**
* Commits View > Pipelines Tab > Pipelines Table.
* Merge Request View > Pipelines Tab > Pipelines Table.
@@ -21,7 +24,7 @@ $(() => {
}
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView();
+ gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable();
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js b/app/assets/javascripts/commit/pipelines/pipelines_service.js
deleted file mode 100644
index 8ae98f9bf97..00000000000
--- a/app/assets/javascripts/commit/pipelines/pipelines_service.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/* globals Vue */
-/* eslint-disable no-unused-vars, no-param-reassign */
-
-/**
- * Pipelines service.
- *
- * Used to fetch the data used to render the pipelines table.
- * Uses Vue.Resource
- */
-class PipelinesService {
-
- /**
- * FIXME: The url provided to request the pipelines in the new merge request
- * page already has `.json`.
- * This should be fixed when the endpoint is improved.
- *
- * @param {String} root
- */
- constructor(root) {
- let endpoint;
-
- if (root.indexOf('.json') === -1) {
- endpoint = `${root}.json`;
- } else {
- endpoint = root;
- }
- this.pipelines = Vue.resource(endpoint);
- }
-
- /**
- * Given the root param provided when the class is initialized, will
- * make a GET request.
- *
- * @return {Promise}
- */
- all() {
- return this.pipelines.get();
- }
-}
-
-window.gl = window.gl || {};
-gl.commits = gl.commits || {};
-gl.commits.pipelines = gl.commits.pipelines || {};
-gl.commits.pipelines.PipelinesService = PipelinesService;
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
index 631ed34851c..4d5a857d705 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -1,13 +1,12 @@
-/* eslint-disable no-new, no-param-reassign */
-/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
-
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
-require('../../vue_shared/components/pipelines_table');
-require('./pipelines_service');
-const PipelineStore = require('./pipelines_store');
+import Vue from 'vue';
+import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
+import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
+import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
+import eventHub from '../../vue_pipelines_index/event_hub';
+import EmptyState from '../../vue_pipelines_index/components/empty_state';
+import ErrorState from '../../vue_pipelines_index/components/error_state';
+import '../../lib/utils/common_utils';
+import '../../vue_shared/vue_resource_interceptor';
/**
*
@@ -20,48 +19,74 @@ const PipelineStore = require('./pipelines_store');
* as soon as we have Webpack and can load them directly into JS files.
*/
-(() => {
- window.gl = window.gl || {};
- gl.commits = gl.commits || {};
- gl.commits.pipelines = gl.commits.pipelines || {};
+export default Vue.component('pipelines-table', {
+ components: {
+ 'pipelines-table-component': PipelinesTableComponent,
+ 'error-state': ErrorState,
+ 'empty-state': EmptyState,
+ },
+
+ /**
+ * Accesses the DOM to provide the needed data.
+ * Returns the necessary props to render `pipelines-table-component` component.
+ *
+ * @return {Object}
+ */
+ data() {
+ const store = new PipelineStore();
- gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', {
+ return {
+ endpoint: null,
+ helpPagePath: null,
+ store,
+ state: store.state,
+ isLoading: false,
+ hasError: false,
+ };
+ },
- components: {
- 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
+ computed: {
+ shouldRenderErrorState() {
+ return this.hasError && !this.isLoading;
},
- /**
- * Accesses the DOM to provide the needed data.
- * Returns the necessary props to render `pipelines-table-component` component.
- *
- * @return {Object}
- */
- data() {
- const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
- const store = new PipelineStore();
-
- return {
- endpoint: pipelinesTableData.endpoint,
- store,
- state: store.state,
- isLoading: false,
- };
+ shouldRenderEmptyState() {
+ return !this.state.pipelines.length && !this.isLoading;
},
+ },
+
+ /**
+ * When the component is about to be mounted, tell the service to fetch the data
+ *
+ * A request to fetch the pipelines will be made.
+ * In case of a successfull response we will store the data in the provided
+ * store, in case of a failed response we need to warn the user.
+ *
+ */
+ beforeMount() {
+ this.endpoint = this.$el.dataset.endpoint;
+ this.helpPagePath = this.$el.dataset.helpPagePath;
+ this.service = new PipelinesService(this.endpoint);
+
+ this.fetchPipelines();
+
+ eventHub.$on('refreshPipelines', this.fetchPipelines);
+ },
- /**
- * When the component is about to be mounted, tell the service to fetch the data
- *
- * A request to fetch the pipelines will be made.
- * In case of a successfull response we will store the data in the provided
- * store, in case of a failed response we need to warn the user.
- *
- */
- beforeMount() {
- const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
+ beforeUpdate() {
+ if (this.state.pipelines.length && this.$children) {
+ this.store.startTimeAgoLoops.call(this, Vue);
+ }
+ },
+ beforeDestroyed() {
+ eventHub.$off('refreshPipelines');
+ },
+
+ methods: {
+ fetchPipelines() {
this.isLoading = true;
- return pipelinesService.all()
+ return this.service.getPipelines()
.then(response => response.json())
.then((json) => {
// depending of the endpoint the response can either bring a `pipelines` key or not.
@@ -70,35 +95,30 @@ const PipelineStore = require('./pipelines_store');
this.isLoading = false;
})
.catch(() => {
+ this.hasError = true;
this.isLoading = false;
- new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert');
});
},
+ },
- beforeUpdate() {
- if (this.state.pipelines.length && this.$children) {
- PipelineStore.startTimeAgoLoops.call(this, Vue);
- }
- },
+ template: `
+ <div class="content-list pipelines">
+ <div class="realtime-loading" v-if="isLoading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+
+ <empty-state
+ v-if="shouldRenderEmptyState"
+ :help-page-path="helpPagePath" />
+
+ <error-state v-if="shouldRenderErrorState" />
- template: `
- <div class="pipelines">
- <div class="realtime-loading" v-if="isLoading">
- <i class="fa fa-spinner fa-spin"></i>
- </div>
-
- <div class="blank-state blank-state-no-icon"
- v-if="!isLoading && state.pipelines.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- No pipelines to show
- </h2>
- </div>
-
- <div class="table-holder pipelines"
- v-if="!isLoading && state.pipelines.length > 0">
- <pipelines-table-component :pipelines="state.pipelines"/>
- </div>
+ <div class="table-holder"
+ v-if="!isLoading && state.pipelines.length > 0">
+ <pipelines-table-component
+ :pipelines="state.pipelines"
+ :service="service" />
</div>
- `,
- });
-})();
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index fbd0db64ca7..3253eebd9b5 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -1,9 +1,11 @@
// ECMAScript polyfills
import 'core-js/fn/array/find';
+import 'core-js/fn/array/from';
import 'core-js/fn/object/assign';
import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point';
+import 'core-js/fn/symbol';
// Browser polyfills
import './polyfills/custom_event';
diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js
new file mode 100644
index 00000000000..abe48572347
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js
@@ -0,0 +1,17 @@
+export default {
+ props: {
+ count: {
+ type: Number,
+ required: true,
+ },
+ },
+ template: `
+ <span v-if="count === 50" class="events-info pull-right">
+ <i class="fa fa-warning has-tooltip"
+ aria-hidden="true"
+ title="Limited to showing 50 events at most"
+ data-placement="top"></i>
+ Showing 50 events
+ </span>
+ `,
+};
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
index b83a4c63fad..3f419a96ff9 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
-/* global Vue */
+
+import Vue from 'vue';
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
@@ -13,6 +14,7 @@
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
index cb1687dcc7a..7ffa38edd9e 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
-/* global Vue */
+
+import Vue from 'vue';
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
@@ -13,6 +14,7 @@
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
index 42e1bbce744..d736c8b0c28 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
@@ -19,12 +19,7 @@ import iconCommit from '../svg/icon_commit.svg';
<div>
<div class="events-description">
{{ stage.description }}
- <span v-if="items.length === 50" class="events-info pull-right">
- <i class="fa fa-warning has-tooltip"
- title="Limited to showing 50 events at most"
- data-placement="top"></i>
- Showing 50 events
- </span>
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
index 73f4205b578..698a79ca68c 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
-/* global Vue */
+
+import Vue from 'vue';
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
@@ -13,6 +14,7 @@
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
index 501ffb1fac9..e63c41f2a57 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
-/* global Vue */
+
+import Vue from 'vue';
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
@@ -13,6 +14,7 @@
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
index 8fa63734cf1..d51f7134e25 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
@@ -17,6 +17,7 @@ import iconBranch from '../svg/icon_branch.svg';
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
index 0015249cfaa..17ae3a9ddc1 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
@@ -18,6 +18,7 @@ import iconBranch from '../svg/icon_branch.svg';
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js b/app/assets/javascripts/cycle_analytics/components/total_time_component.js
index 0d85e1a4678..b4442ea5566 100644
--- a/app/assets/javascripts/cycle_analytics/components/total_time_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
-/* global Vue */
+
+import Vue from 'vue';
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index beff293b587..b099b39e58f 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -1,9 +1,9 @@
-/* global Vue */
-/* global Cookies */
/* global Flash */
-window.Vue = require('vue');
-window.Cookies = require('js-cookie');
+import Vue from 'vue';
+import Cookies from 'js-cookie';
+import LimitWarningComponent from './components/limit_warning_component';
+
require('./components/stage_code_component');
require('./components/stage_issue_component');
require('./components/stage_plan_component');
@@ -131,5 +131,6 @@ $(() => {
});
// Register global components
+ Vue.component('limit-warning', LimitWarningComponent);
Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
});
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index cfa60325fcc..88180149715 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -33,11 +33,7 @@ class Diff {
handleClickUnfold(e) {
const $target = $(e.target);
- // current babel config relies on iterators implementation, so we cannot simply do:
- // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
- const ref = this.lineNumbers($target.parent());
- const oldLineNumber = ref[0];
- const newLineNumber = ref[1];
+ const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
const offset = newLineNumber - oldLineNumber;
const bottom = $target.hasClass('js-unfold-bottom');
let since;
@@ -105,10 +101,11 @@ class Diff {
}
lineNumbers(line) {
- if (!line.children().length) {
+ const children = line.find('.diff-line-num').toArray();
+ if (children.length !== 2) {
return [0, 0];
}
- return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10));
+ return children.map(elm => parseInt($(elm).data('linenumber'), 10) || 0);
}
highlightSelectedLine() {
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
index d948dff58ec..fc2f20e3bcb 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
@@ -1,6 +1,7 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */
/* global CommentsStore */
-const Vue = require('vue');
+
+import Vue from 'vue';
(() => {
const CommentAndResolveBtn = Vue.extend({
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
index 788daa96b3d..0297add94d5 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -1,4 +1,6 @@
-/* global CommentsStore Cookies notes */
+/* global CommentsStore */
+/* global notes */
+
import Vue from 'vue';
import collapseIcon from '../icons/collapse_icon.svg';
@@ -25,6 +27,7 @@ import collapseIcon from '../icons/collapse_icon.svg';
role="button"
data-container="body"
data-placement="top"
+ data-html="true"
:data-line-type="lineType"
:title="note.authorName + ': ' + note.noteTruncated"
:src="note.authorAvatar"
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
index 283dc330cad..8edc45130fc 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -1,7 +1,8 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, space-before-function-paren, no-lonely-if, no-continue, brace-style, max-len, quotes */
/* global DiscussionMixins */
/* global CommentsStore */
-const Vue = require('vue');
+
+import Vue from 'vue';
(() => {
const JumpToDiscussion = Vue.extend({
diff --git a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
index e86bef47172..8eb0e10b832 100644
--- a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
@@ -1,6 +1,7 @@
-/* global Vue */
/* global CommentsStore */
+import Vue from 'vue';
+
(() => {
const NewIssueForDiscussion = Vue.extend({
props: {
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index fbd980f0fce..312f38ce241 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -2,7 +2,8 @@
/* global CommentsStore */
/* global ResolveService */
/* global Flash */
-const Vue = require('vue');
+
+import Vue from 'vue';
(() => {
const ResolveBtn = Vue.extend({
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js
index de9367f2136..27147ac6b5c 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js
@@ -1,7 +1,8 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */
/* global DiscussionMixins */
/* global CommentsStore */
-const Vue = require('vue');
+
+import Vue from 'vue';
((w) => {
w.ResolveCount = Vue.extend({
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
index 7c5fcd04d2d..a964b7d0c6b 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
@@ -2,7 +2,7 @@
/* global CommentsStore */
/* global ResolveService */
-const Vue = require('vue');
+import Vue from 'vue';
(() => {
const ResolveDiscussionBtn = Vue.extend({
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
index 4f6b86a917c..b6b47e2da6f 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
@@ -1,8 +1,8 @@
/* eslint-disable func-names, comma-dangle, new-cap, no-new, max-len */
-/* global Vue */
/* global ResolveCount */
-const Vue = require('vue');
+import Vue from 'vue';
+
require('./models/discussion');
require('./models/note');
require('./stores/comments');
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js
index dce1a9b58bd..dc43e4b2cc7 100644
--- a/app/assets/javascripts/diff_notes/models/discussion.js
+++ b/app/assets/javascripts/diff_notes/models/discussion.js
@@ -1,7 +1,8 @@
/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */
-/* global Vue */
/* global NoteModel */
+import Vue from 'vue';
+
class DiscussionModel {
constructor (discussionId) {
this.id = discussionId;
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index 090c454e9e4..bfa4fc9037a 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -2,10 +2,13 @@
/* global Flash */
/* global CommentsStore */
-const Vue = window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
require('../../vue_shared/vue_resource_interceptor');
+Vue.use(VueResource);
+
(() => {
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js
index 69c4d7a8434..e6cbda56c91 100644
--- a/app/assets/javascripts/diff_notes/stores/comments.js
+++ b/app/assets/javascripts/diff_notes/stores/comments.js
@@ -1,7 +1,8 @@
/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */
-/* global Vue */
/* global DiscussionModel */
+import Vue from 'vue';
+
((w) => {
w.CommentsStore = {
state: {},
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index db1a2848d8d..80490052389 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -1,4 +1,3 @@
-import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make this a bundle
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global UsernameValidator */
/* global ActiveTabMemoizer */
@@ -34,6 +33,8 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make
/* global ProjectShow */
/* global Labels */
/* global Shortcuts */
+/* global Sidebar */
+
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
@@ -42,9 +43,9 @@ import GroupsList from './groups_list';
import ProjectsList from './projects_list';
import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
+import UserCallout from './user_callout';
const ShortcutsBlob = require('./shortcuts_blob');
-const UserCallout = require('./user_callout');
(function() {
var Dispatcher;
@@ -119,6 +120,7 @@ const UserCallout = require('./user_callout');
case 'groups:milestones:show':
case 'dashboard:milestones:show':
new Milestone();
+ new Sidebar();
break;
case 'dashboard:todos:index':
new gl.Todos();
@@ -329,8 +331,6 @@ const UserCallout = require('./user_callout');
case 'ci:lints:show':
new gl.CILintEditor();
break;
- case 'projects:environments:metrics':
- new PrometheusGraph();
case 'users:show':
new UserCallout();
break;
diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js
index 9b40a3f20a4..7f7d93f3e27 100644
--- a/app/assets/javascripts/droplab/droplab_filter.js
+++ b/app/assets/javascripts/droplab/droplab_filter.js
@@ -56,10 +56,12 @@ require('../window')(function(w){
this.hookInput = hookInput;
this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper);
+ this.hookInput.trigger.addEventListener('mousedown.dl', this.keydownWrapper);
},
destroy: function destroy(){
this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper);
+ this.hookInput.trigger.removeEventListener('mousedown.dl', this.keydownWrapper);
}
};
});
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index fdbb4644971..db10b383913 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -132,7 +132,7 @@ class DueDateSelect {
const selectedDateValue = this.datePayload[this.abilityName].due_date;
const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
- this.$loading.fadeIn();
+ this.$loading.removeClass('hidden').fadeIn();
if (isDropdown) {
this.$dropdown.trigger('loading.gl.dropdown');
diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.js
index 0923ce6b550..51aab8460f6 100644
--- a/app/assets/javascripts/environments/components/environment.js
+++ b/app/assets/javascripts/environments/components/environment.js
@@ -1,21 +1,18 @@
-/* eslint-disable no-param-reassign, no-new */
+/* eslint-disable no-new */
/* global Flash */
+import Vue from 'vue';
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table';
import EnvironmentsStore from '../stores/environments_store';
+import TablePaginationComponent from '../../vue_shared/components/table_pagination';
+import '../../lib/utils/common_utils';
import eventHub from '../event_hub';
-const Vue = window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../../vue_shared/components/table_pagination');
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
-
export default Vue.component('environment-component', {
components: {
'environment-table': EnvironmentTable,
- 'table-pagination': gl.VueGlPagination,
+ 'table-pagination': TablePaginationComponent,
},
data() {
@@ -59,7 +56,6 @@ export default Vue.component('environment-component', {
canCreateEnvironmentParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
},
-
},
/**
diff --git a/app/assets/javascripts/environments/components/environment_actions.js b/app/assets/javascripts/environments/components/environment_actions.js
index 455a8819549..385085c03e2 100644
--- a/app/assets/javascripts/environments/components/environment_actions.js
+++ b/app/assets/javascripts/environments/components/environment_actions.js
@@ -25,6 +25,12 @@ export default {
};
},
+ computed: {
+ title() {
+ return 'Deploy to...';
+ },
+ },
+
methods: {
onClickAction(endpoint) {
this.isLoading = true;
@@ -44,8 +50,11 @@ export default {
template: `
<div class="btn-group" role="group">
<button
- class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
+ class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
+ data-container="body"
data-toggle="dropdown"
+ :title="title"
+ :aria-label="title"
:disabled="isLoading">
<span>
<span v-html="playIconSvg"></span>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js
index a554998f52c..d79b916c360 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.js
+++ b/app/assets/javascripts/environments/components/environment_external_url.js
@@ -9,12 +9,21 @@ export default {
},
},
+ computed: {
+ title() {
+ return 'Open';
+ },
+ },
+
template: `
<a
- class="btn external_url"
+ class="btn external-url has-tooltip"
+ data-container="body"
:href="externalUrl"
target="_blank"
- title="Environment external URL">
+ rel="noopener noreferrer nofollow"
+ :title="title"
+ :aria-label="title">
<i class="fa fa-external-link" aria-hidden="true"></i>
</a>
`,
diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js
index 9d753b4f808..fcae5a55120 100644
--- a/app/assets/javascripts/environments/components/environment_item.js
+++ b/app/assets/javascripts/environments/components/environment_item.js
@@ -1,29 +1,29 @@
import Timeago from 'timeago.js';
+import '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions';
import ExternalUrlComponent from './environment_external_url';
import StopComponent from './environment_stop';
import RollbackComponent from './environment_rollback';
import TerminalButtonComponent from './environment_terminal_button';
-import '../../lib/utils/text_utility';
-import '../../vue_shared/components/commit';
+import MonitoringButtonComponent from './environment_monitoring';
+import CommitComponent from '../../vue_shared/components/commit';
/**
* Envrionment Item Component
*
* Renders a table row for each environment.
*/
-
const timeagoInstance = new Timeago();
export default {
-
components: {
- 'commit-component': gl.CommitComponent,
+ 'commit-component': CommitComponent,
'actions-component': ActionsComponent,
'external-url-component': ExternalUrlComponent,
'stop-component': StopComponent,
'rollback-component': RollbackComponent,
'terminal-button-component': TerminalButtonComponent,
+ 'monitoring-button-component': MonitoringButtonComponent,
},
props: {
@@ -395,6 +395,14 @@ export default {
return '';
},
+ monitoringUrl() {
+ if (this.model && this.model.metrics_path) {
+ return this.model.metrics_path;
+ }
+
+ return '';
+ },
+
/**
* Constructs folder URL based on the current location and the folder id.
*
@@ -499,13 +507,16 @@ export default {
<external-url-component v-if="externalURL && canReadEnvironment"
:external-url="externalURL"/>
- <stop-component v-if="hasStopAction && canCreateDeployment"
- :stop-url="model.stop_path"
- :service="service"/>
+ <monitoring-button-component v-if="monitoringUrl && canReadEnvironment"
+ :monitoring-url="monitoringUrl"/>
<terminal-button-component v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"/>
+ <stop-component v-if="hasStopAction && canCreateDeployment"
+ :stop-url="model.stop_path"
+ :service="service"/>
+
<rollback-component v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl"
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.js b/app/assets/javascripts/environments/components/environment_monitoring.js
new file mode 100644
index 00000000000..064e2fc7434
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_monitoring.js
@@ -0,0 +1,31 @@
+/**
+ * Renders the Monitoring (Metrics) link in environments table.
+ */
+export default {
+ props: {
+ monitoringUrl: {
+ type: String,
+ default: '',
+ required: true,
+ },
+ },
+
+ computed: {
+ title() {
+ return 'Monitoring';
+ },
+ },
+
+ template: `
+ <a
+ class="btn monitoring-url has-tooltip"
+ data-container="body"
+ :href="monitoringUrl"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ :title="title"
+ :aria-label="title">
+ <i class="fa fa-area-chart" aria-hidden="true"></i>
+ </a>
+ `,
+};
diff --git a/app/assets/javascripts/environments/components/environment_stop.js b/app/assets/javascripts/environments/components/environment_stop.js
index 5404d647745..47102692024 100644
--- a/app/assets/javascripts/environments/components/environment_stop.js
+++ b/app/assets/javascripts/environments/components/environment_stop.js
@@ -25,6 +25,12 @@ export default {
};
},
+ computed: {
+ title() {
+ return 'Stop';
+ },
+ },
+
methods: {
onClick() {
if (confirm('Are you sure you want to stop this environment?')) {
@@ -45,10 +51,12 @@ export default {
template: `
<button type="button"
- class="btn stop-env-link"
+ class="btn stop-env-link has-tooltip"
+ data-container="body"
@click="onClick"
:disabled="isLoading"
- title="Stop Environment">
+ :title="title"
+ :aria-label="title">
<i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</button>
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js b/app/assets/javascripts/environments/components/environment_terminal_button.js
index 66a71faa02f..092a50a0d6f 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.js
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.js
@@ -14,12 +14,22 @@ export default {
},
data() {
- return { terminalIconSvg };
+ return {
+ terminalIconSvg,
+ };
+ },
+
+ computed: {
+ title() {
+ return 'Terminal';
+ },
},
template: `
- <a class="btn terminal-button"
- title="Open web terminal"
+ <a class="btn terminal-button has-tooltip"
+ data-container="body"
+ :title="title"
+ :aria-label="title"
:href="terminalPath">
${terminalIconSvg}
</a>
diff --git a/app/assets/javascripts/environments/components/environments_table.js b/app/assets/javascripts/environments/components/environments_table.js
index 5f07b612b91..338dff40bc9 100644
--- a/app/assets/javascripts/environments/components/environments_table.js
+++ b/app/assets/javascripts/environments/components/environments_table.js
@@ -1,11 +1,11 @@
/**
* Render environments table.
*/
-import EnvironmentItem from './environment_item';
+import EnvironmentTableRowComponent from './environment_item';
export default {
components: {
- 'environment-item': EnvironmentItem,
+ 'environment-item': EnvironmentTableRowComponent,
},
props: {
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.js
index 7abcf6dbbea..8abbcf0c227 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.js
@@ -1,20 +1,17 @@
-/* eslint-disable no-param-reassign, no-new */
+/* eslint-disable no-new */
/* global Flash */
+import Vue from 'vue';
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table';
import EnvironmentsStore from '../stores/environments_store';
-
-const Vue = window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../../vue_shared/components/table_pagination');
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
+import TablePaginationComponent from '../../vue_shared/components/table_pagination';
+import '../../lib/utils/common_utils';
+import '../../vue_shared/vue_resource_interceptor';
export default Vue.component('environment-folder-view', {
-
components: {
'environment-table': EnvironmentTable,
- 'table-pagination': gl.VueGlPagination,
+ 'table-pagination': TablePaginationComponent,
},
data() {
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 76296c83d11..07040bf0d73 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -1,5 +1,8 @@
/* eslint-disable class-methods-use-this */
import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
export default class EnvironmentsService {
constructor(endpoint) {
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index d3fe3872c56..3c3084f3b78 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -1,5 +1,4 @@
import '~/lib/utils/common_utils';
-
/**
* Environments Store.
*
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index 134bdc6ad80..e7bf530d343 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -38,6 +38,7 @@
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
}
+ this.resetFilters();
this.dismissDropdown();
this.dispatchInputEvent();
}
@@ -107,7 +108,7 @@
const hook = this.getCurrentHook();
if (hook) {
- const data = hook.list.data;
+ const data = hook.list.data || [];
const results = data.map((o) => {
const updated = o;
updated.droplab_hidden = false;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 7ace51748aa..22352950452 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -40,6 +40,8 @@ import FilteredSearchContainer from './container';
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.editTokenWrapper = this.editToken.bind(this);
this.tokenChange = this.tokenChange.bind(this);
+ this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
+ this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
this.filteredSearchInputForm = this.filteredSearchInput.form;
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
@@ -51,11 +53,13 @@ import FilteredSearchContainer from './container';
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.addEventListener('click', this.tokenChange);
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
+ this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper);
+ document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenWrapper);
}
@@ -69,11 +73,13 @@ import FilteredSearchContainer from './container';
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
+ this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper);
+ document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
}
@@ -124,6 +130,26 @@ import FilteredSearchContainer from './container';
}
}
+ addInputContainerFocus() {
+ const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container');
+
+ if (inputContainer) {
+ inputContainer.classList.add('focus');
+ }
+ }
+
+ removeInputContainerFocus(e) {
+ const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container');
+ const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
+ const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
+ const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
+
+ if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
+ !isElementInStaticFilterDropdown && inputContainer) {
+ inputContainer.classList.remove('focus');
+ }
+ }
+
static selectToken(e) {
const button = e.target.closest('.selectable');
@@ -358,7 +384,7 @@ import FilteredSearchContainer from './container';
paths.push(`search=${sanitized}`);
}
- const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`;
+ const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`;
if (this.updateObject) {
this.updateObject(parameterizedUrl);
diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
index 9bf1b1ced88..a2729dc0e95 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
@@ -8,21 +8,31 @@ require('./filtered_search_token_keys');
// Values that start with a double quote must end in a double quote (same for single)
const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
const tokens = [];
+ const tokenIndexes = []; // stores key+value for simple search
let lastToken = null;
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
let tokenValue = v1 || v2 || v3;
let tokenSymbol = symbol;
+ let tokenIndex = '';
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
tokenSymbol = tokenValue;
tokenValue = '';
}
- tokens.push({
- key,
- value: tokenValue || '',
- symbol: tokenSymbol || '',
- });
+ tokenIndex = `${key}:${tokenValue}`;
+
+ // Prevent adding duplicates
+ if (tokenIndexes.indexOf(tokenIndex) === -1) {
+ tokenIndexes.push(tokenIndex);
+
+ tokens.push({
+ key,
+ value: tokenValue || '',
+ symbol: tokenSymbol || '',
+ });
+ }
+
return '';
}).replace(/\s{2,}/g, ' ').trim() || '';
diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js
index 6a028f299b1..62675d7e67e 100644
--- a/app/assets/javascripts/group_name.js
+++ b/app/assets/javascripts/group_name.js
@@ -1,40 +1,64 @@
-const GROUP_LIMIT = 2;
+
+import _ from 'underscore';
export default class GroupName {
constructor() {
- this.titleContainer = document.querySelector('.title');
- this.groups = document.querySelectorAll('.group-path');
+ this.titleContainer = document.querySelector('.title-container');
+ this.title = document.querySelector('.title');
+ this.titleWidth = this.title.offsetWidth;
this.groupTitle = document.querySelector('.group-title');
+ this.groups = document.querySelectorAll('.group-path');
this.toggle = null;
this.isHidden = false;
this.init();
}
init() {
- if (this.groups.length > GROUP_LIMIT) {
+ if (this.groups.length > 0) {
this.groups[this.groups.length - 1].classList.remove('hidable');
- this.addToggle();
+ this.toggleHandler();
+ window.addEventListener('resize', _.debounce(this.toggleHandler.bind(this), 100));
}
this.render();
}
- addToggle() {
- const header = document.querySelector('.header-content');
+ toggleHandler() {
+ if (this.titleWidth > this.titleContainer.offsetWidth) {
+ if (!this.toggle) this.createToggle();
+ this.showToggle();
+ } else if (this.toggle) {
+ this.hideToggle();
+ }
+ }
+
+ createToggle() {
this.toggle = document.createElement('button');
this.toggle.className = 'text-expander group-name-toggle';
this.toggle.setAttribute('aria-label', 'Toggle full path');
this.toggle.innerHTML = '...';
this.toggle.addEventListener('click', this.toggleGroups.bind(this));
- header.insertBefore(this.toggle, this.titleContainer);
+ this.titleContainer.insertBefore(this.toggle, this.title);
this.toggleGroups();
}
+ showToggle() {
+ this.title.classList.add('wrap');
+ this.toggle.classList.remove('hidden');
+ if (this.isHidden) this.groupTitle.classList.add('is-hidden');
+ }
+
+ hideToggle() {
+ this.title.classList.remove('wrap');
+ this.toggle.classList.add('hidden');
+ if (this.isHidden) this.groupTitle.classList.remove('is-hidden');
+ }
+
toggleGroups() {
this.isHidden = !this.isHidden;
this.groupTitle.classList.toggle('is-hidden');
}
render() {
- this.titleContainer.classList.remove('initializing');
+ this.title.classList.remove('initializing');
}
}
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index e5dfa30edab..602a3b78189 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -6,23 +6,60 @@ var slice = [].slice;
window.GroupsSelect = (function() {
function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) {
+ const self = _this;
+
return function(i, select) {
var all_available, skip_groups;
- all_available = $(select).data('all-available');
- skip_groups = $(select).data('skip-groups') || [];
- return $(select).select2({
+ const $select = $(select);
+ all_available = $select.data('all-available');
+ skip_groups = $select.data('skip-groups') || [];
+
+ $select.select2({
placeholder: "Search for a group",
- multiple: $(select).hasClass('multiselect'),
+ multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
- query: function(query) {
- var options = { all_available: all_available, skip_groups: skip_groups };
- return Api.groups(query.term, options, function(groups) {
- var data;
- data = {
- results: groups
+ ajax: {
+ url: Api.buildUrl(Api.groupsPath),
+ dataType: 'json',
+ quietMillis: 250,
+ transport: function (params) {
+ $.ajax(params).then((data, status, xhr) => {
+ const results = data || [];
+
+ const headers = gl.utils.normalizeCRLFHeaders(xhr.getAllResponseHeaders());
+ const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
+ const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
+ const more = currentPage < totalPages;
+
+ return {
+ results,
+ pagination: {
+ more,
+ },
+ };
+ }).then(params.success).fail(params.error);
+ },
+ data: function (search, page) {
+ return {
+ search,
+ page,
+ per_page: GroupsSelect.PER_PAGE,
+ all_available,
+ skip_groups,
+ };
+ },
+ results: function (data, page) {
+ if (data.length) return { results: [] };
+
+ const results = data.length ? data : data.results || [];
+ const more = data.pagination ? data.pagination.more : false;
+
+ return {
+ results,
+ page,
+ more,
};
- return query.callback(data);
- });
+ },
},
initSelection: function(element, callback) {
var id;
@@ -34,19 +71,23 @@ window.GroupsSelect = (function() {
formatResult: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
- return _this.formatResult.apply(_this, args);
+ return self.formatResult.apply(self, args);
},
formatSelection: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
- return _this.formatSelection.apply(_this, args);
+ return self.formatSelection.apply(self, args);
},
- dropdownCssClass: "ajax-groups-dropdown",
+ dropdownCssClass: "ajax-groups-dropdown select2-infinite",
// we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) {
return m;
}
});
+
+ self.dropdown = document.querySelector('.select2-infinite .select2-results');
+
+ $select.on('select2-loaded', self.forceOverflow.bind(self));
};
})(this));
}
@@ -65,5 +106,12 @@ window.GroupsSelect = (function() {
return group.full_name;
};
+ GroupsSelect.prototype.forceOverflow = function (e) {
+ const itemHeight = this.dropdown.querySelector('.select2-result:first-child').clientHeight;
+ this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight - (itemHeight * 0.9))}px`;
+ };
+
+ GroupsSelect.PER_PAGE = 20;
+
return GroupsSelect;
})();
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 34f44dad7a5..dc170c60456 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */
$(document).on('todo:toggle', function(e, count) {
- var $todoPendingCount = $('.todos-pending-count');
+ var $todoPendingCount = $('.todos-count');
$todoPendingCount.text(gl.text.highCountTrim(count));
$todoPendingCount.toggleClass('hidden', count === 0);
});
diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js
index 357b3487ca9..aec13e78f42 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js
+++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js
@@ -1,4 +1,4 @@
-/* global Vue */
+import Vue from 'vue';
import stopwatchSvg from 'icons/_icon_stopwatch.svg';
require('../../../lib/utils/pretty_time');
diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js
index 750468c679b..c55e263f6f4 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js
+++ b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js
@@ -1,4 +1,5 @@
-/* global Vue */
+import Vue from 'vue';
+
require('../../../lib/utils/pretty_time');
(() => {
diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js
index 309e9f2f9ef..a7fbd704c40 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js
+++ b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js
@@ -1,4 +1,5 @@
-/* global Vue */
+import Vue from 'vue';
+
(() => {
Vue.component('time-tracking-estimate-only-pane', {
name: 'time-tracking-estimate-only-pane',
diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js b/app/assets/javascripts/issuable/time_tracking/components/help_state.js
index d7ced6d7151..344b29ebea4 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/help_state.js
+++ b/app/assets/javascripts/issuable/time_tracking/components/help_state.js
@@ -1,4 +1,5 @@
-/* global Vue */
+import Vue from 'vue';
+
(() => {
Vue.component('time-tracking-help-state', {
name: 'time-tracking-help-state',
diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js
index 1d2ca643b5b..b081adf5e64 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js
+++ b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js
@@ -1,4 +1,5 @@
-/* global Vue */
+import Vue from 'vue';
+
(() => {
Vue.component('time-tracking-no-tracking-pane', {
name: 'time-tracking-no-tracking-pane',
diff --git a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js
index ed283fec3c3..edb9169112f 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js
+++ b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js
@@ -1,4 +1,5 @@
-/* global Vue */
+import Vue from 'vue';
+
(() => {
Vue.component('time-tracking-spent-only-pane', {
name: 'time-tracking-spent-only-pane',
diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js
index 1fae2d62b14..0213522f551 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js
+++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js
@@ -1,4 +1,4 @@
-/* global Vue */
+import Vue from 'vue';
require('./help_state');
require('./collapsed_state');
diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js
index 0134b7cb6f3..1689a69e1ed 100644
--- a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js
+++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js
@@ -1,11 +1,12 @@
-/* global Vue */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
require('./components/time_tracker');
require('../../smart_interval');
require('../../subbable_resource');
+Vue.use(VueResource);
+
(() => {
/* This Vue instance represents what will become the parent instance for the
* sidebar. It will be responsible for managing `issuable` state and propagating
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 115312d4b83..834b98e8601 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -1,8 +1,9 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
/* global UsersSelect */
-/* global Cookies */
/* global bp */
+import Cookies from 'js-cookie';
+
(function() {
this.IssuableContext = (function() {
function IssuableContext(currentUser) {
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index ef4029a8623..47e675f537e 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -2,6 +2,7 @@
/* global Flash */
require('./flash');
+require('~/lib/utils/text_utility');
require('vendor/jquery.waitforimages');
require('./task_list');
@@ -50,20 +51,21 @@ class Issue {
success: function(data, textStatus, jqXHR) {
if ('id' in data) {
$(document).trigger('issuable:change');
- const currentTotal = Number($('.issue_counter').text());
+ let total = Number($('.issue_counter').text().replace(/[^\d]/, ''));
if (isClose) {
$('a.btn-close').addClass('hidden');
$('a.btn-reopen').removeClass('hidden');
$('div.status-box-closed').removeClass('hidden');
$('div.status-box-open').addClass('hidden');
- $('.issue_counter').text(currentTotal - 1);
+ total -= 1;
} else {
$('a.btn-reopen').addClass('hidden');
$('a.btn-close').removeClass('hidden');
$('div.status-box-closed').addClass('hidden');
$('div.status-box-open').removeClass('hidden');
- $('.issue_counter').text(currentTotal + 1);
+ total += 1;
}
+ $('.issue_counter').text(gl.text.addDelimiter(total));
} else {
new Flash(issueFailMessage, 'alert');
}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index c648a0f076c..443fb3e0ca9 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -76,7 +76,7 @@
if (!selected.length) {
data[abilityName].label_ids = [''];
}
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 08ca9e4fa4d..a5f99bcdd8f 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -11,8 +11,9 @@
});
};
- $(function() {
- var $scrollingTabs = $('.scrolling-tabs');
+ $(document).on('init.scrolling-tabs', () => {
+ const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized');
+ $scrollingTabs.addClass('is-initialized');
hideEndFade($scrollingTabs);
$(window).off('resize.nav').on('resize.nav', function() {
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index a1423b6fda5..46b80c04e20 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -232,6 +232,22 @@
};
/**
+ this will take in the getAllResponseHeaders result and normalize them
+ this way we don't run into production issues when nginx gives us lowercased header keys
+ */
+ w.gl.utils.normalizeCRLFHeaders = (headers) => {
+ const headersObject = {};
+ const headersArray = headers.split('\n');
+
+ headersArray.forEach((header) => {
+ const keyValue = header.split(': ');
+ headersObject[keyValue[0]] = keyValue[1];
+ });
+
+ return w.gl.utils.normalizeHeaders(headersObject);
+ };
+
+ /**
* Parses pagination object string values into numbers.
*
* @param {Object} paginationInformation
@@ -247,7 +263,7 @@
});
/**
- * Updates the search parameter of a URL given the parameter and values provided.
+ * Updates the search parameter of a URL given the parameter and value provided.
*
* If no search params are present we'll add it.
* If param for page is already present, we'll update it
@@ -262,17 +278,24 @@
let search;
const locationSearch = window.location.search;
- if (locationSearch.length === 0) {
- search = `?${param}=${value}`;
- }
+ if (locationSearch.length) {
+ const parameters = locationSearch.substring(1, locationSearch.length)
+ .split('&')
+ .reduce((acc, element) => {
+ const val = element.split('=');
+ acc[val[0]] = decodeURIComponent(val[1]);
+ return acc;
+ }, {});
- if (locationSearch.indexOf(param) !== -1) {
- const regex = new RegExp(param + '=\\d');
- search = locationSearch.replace(regex, `${param}=${value}`);
- }
+ parameters[param] = value;
- if (locationSearch.length && locationSearch.indexOf(param) === -1) {
- search = `${locationSearch}&${param}=${value}`;
+ const toString = Object.keys(parameters)
+ .map(val => `${val}=${encodeURIComponent(parameters[val])}`)
+ .join('&');
+
+ search = `?${toString}`;
+ } else {
+ search = `?${param}=${value}`;
}
return search;
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
new file mode 100644
index 00000000000..5c22aea51cd
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -0,0 +1,100 @@
+import httpStatusCodes from './http_status';
+
+/**
+ * Polling utility for handling realtime updates.
+ * Service for vue resouce and method need to be provided as props
+ *
+ * @example
+ * new Poll({
+ * resource: resource,
+ * method: 'name',
+ * data: {page: 1, scope: 'all'}, // optional
+ * successCallback: () => {},
+ * errorCallback: () => {},
+ * notificationCallback: () => {}, // optional
+ * }).makeRequest();
+ *
+ * Usage in pipelines table with visibility lib:
+ *
+ * const poll = new Poll({
+ * resource: this.service,
+ * method: 'getPipelines',
+ * data: { page: pageNumber, scope },
+ * successCallback: this.successCallback,
+ * errorCallback: this.errorCallback,
+ * notificationCallback: this.updateLoading,
+ * });
+ *
+ * if (!Visibility.hidden()) {
+ * poll.makeRequest();
+ * }
+ *
+ * Visibility.change(() => {
+ * if (!Visibility.hidden()) {
+ * poll.restart();
+ * } else {
+ * poll.stop();
+ * }
+* });
+ *
+ * 1. Checks for response and headers before start polling
+ * 2. Interval is provided by `Poll-Interval` header.
+ * 3. If `Poll-Interval` is -1, we stop polling
+ * 4. If HTTP response is 200, we poll.
+ * 5. If HTTP response is different from 200, we stop polling.
+ *
+ */
+export default class Poll {
+ constructor(options = {}) {
+ this.options = options;
+ this.options.data = options.data || {};
+ this.options.notificationCallback = options.notificationCallback ||
+ function notificationCallback() {};
+
+ this.intervalHeader = 'POLL-INTERVAL';
+ this.timeoutID = null;
+ this.canPoll = true;
+ }
+
+ checkConditions(response) {
+ const headers = gl.utils.normalizeHeaders(response.headers);
+ const pollInterval = parseInt(headers[this.intervalHeader], 10);
+
+ if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
+ this.timeoutID = setTimeout(() => {
+ this.makeRequest();
+ }, pollInterval);
+ }
+
+ this.options.successCallback(response);
+ }
+
+ makeRequest() {
+ const { resource, method, data, errorCallback, notificationCallback } = this.options;
+
+ // It's called everytime a new request is made. Useful to update the status.
+ notificationCallback(true);
+
+ return resource[method](data)
+ .then(response => this.checkConditions(response))
+ .catch(error => errorCallback(error));
+ }
+
+ /**
+ * Stops the polling recursive chain
+ * and guarantees if the timeout is already running it won't make another request by
+ * cancelling the previously established timeout.
+ */
+ stop() {
+ this.canPoll = false;
+ clearTimeout(this.timeoutID);
+ }
+
+ /**
+ * Restarts polling after it has been stoped
+ */
+ restart() {
+ this.canPoll = true;
+ this.makeRequest();
+ }
+}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 81d5748191d..665a59f3183 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,6 +1,5 @@
/* 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 */
/* global ConfirmDangerModal */
/* global Aside */
@@ -24,7 +23,6 @@ import './extensions/array';
window.jQuery = jQuery;
window.$ = jQuery;
window._ = _;
-window.Cookies = Cookies;
window.Pikaday = Pikaday;
window.Dropzone = Dropzone;
window.Sortable = Sortable;
@@ -49,15 +47,6 @@ import { installGlEmojiElement } from './behaviors/gl_emoji';
installGlEmojiElement();
// blob
-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';
import './blob/create_branch_dropdown';
import './blob/target_branch_dropdown';
@@ -381,4 +370,6 @@ $(function () {
new Aside();
gl.utils.initTimeagoTimeout();
+
+ $(document).trigger('init.scrolling-tabs');
});
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
index c7e78fed8fe..645045fea88 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
@@ -1,8 +1,9 @@
/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-new, no-param-reassign, max-len */
-/* global Vue */
/* global ace */
/* global Flash */
+import Vue from 'vue';
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
index 240c8f98932..56d6678e1bd 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign, comma-dangle */
-/* global Vue */
+
+import Vue from 'vue';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
index 97753c50b60..0fc4a13450a 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign, comma-dangle */
-/* global Vue */
+
+import Vue from 'vue';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
index 74587df22c5..c4e379a4a0b 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
@@ -1,6 +1,7 @@
/* eslint-disable comma-dangle, object-shorthand, no-param-reassign, camelcase, no-nested-ternary, no-continue, max-len */
-/* global Cookies */
-/* global Vue */
+
+import Vue from 'vue';
+import Cookies from 'js-cookie';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index 653e52fb6bf..15992460146 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -1,8 +1,8 @@
/* eslint-disable new-cap, comma-dangle, no-new */
-/* global Vue */
/* global Flash */
-window.Vue = require('vue');
+import Vue from 'vue';
+
require('./merge_conflict_store');
require('./merge_conflict_service');
require('./mixins/line_conflict_utils');
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 190336dbd20..3c4e6102469 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,11 +1,13 @@
/* eslint-disable no-new, class-methods-use-this */
/* global Breakpoints */
-/* global Cookies */
/* global Flash */
-require('./breakpoints');
-window.Cookies = require('js-cookie');
-require('./flash');
+import Cookies from 'js-cookie';
+
+import CommitPipelinesTable from './commit/pipelines/pipelines_table';
+
+import './breakpoints';
+import './flash';
/* eslint-disable max-len */
// MergeRequestTabs
@@ -97,6 +99,13 @@ require('./flash');
.off('click', this.clickTab);
}
+ destroy() {
+ this.unbindEvents();
+ if (this.commitPipelinesTable) {
+ this.commitPipelinesTable.$destroy();
+ }
+ }
+
showTab(e) {
e.preventDefault();
this.activateTab($(e.target).data('action'));
@@ -127,16 +136,9 @@ require('./flash');
if (this.diffViewType() === 'parallel') {
this.expandViewContainer();
}
- $.scrollTo('.merge-request-details .merge-request-tabs', {
- offset: 0,
- });
} else if (action === 'pipelines') {
- if (this.pipelinesLoaded) {
- return;
- }
- const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
- this.pipelinesLoaded = true;
+ this.resetViewContainer();
+ this.loadPipelines();
} else {
this.expandView();
this.resetViewContainer();
@@ -225,6 +227,18 @@ require('./flash');
});
}
+ loadPipelines() {
+ if (this.pipelinesLoaded) {
+ return;
+ }
+ const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
+ // Could already be mounted from the `pipelines_bundle`
+ if (pipelineTableViewEl) {
+ this.commitPipelinesTable = new CommitPipelinesTable().$mount(pipelineTableViewEl);
+ }
+ this.pipelinesLoaded = true;
+ }
+
loadDiff(source) {
if (this.diffsLoaded) {
return;
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
index 94a4f24f1d7..0e2af3df071 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -14,13 +14,13 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
<%= ci_success_icon %>
<span>
Deployed to
- <a href="<%- url %>" target="_blank" class="environment">
+ <a href="<%- url %>" target="_blank" rel="noopener noreferrer" class="environment">
<%- name %>
</a>
<span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>">
<%- deployed_at %>
</span>
- <a class="js-environment-link" href="<%- external_url %>" target="_blank">
+ <a class="js-environment-link" href="<%- external_url %>" target="_blank" rel="noopener noreferrer">
<i class="fa fa-external-link"></i>
View on <%- external_url_formatted %>
</a>
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 02ff6f5682c..ac4fad88fe5 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,8 +1,9 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
-/* global Vue */
/* global Issuable */
/* global ListMilestone */
+import Vue from 'vue';
+
(function() {
this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject, els) {
@@ -159,7 +160,7 @@
}
$dropdown.trigger('loading.gl.dropdown');
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () {
@@ -171,7 +172,7 @@
data = {};
data[abilityName] = {};
data[abilityName].milestone_id = selected != null ? selected : null;
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
new file mode 100644
index 00000000000..b3ce9310417
--- /dev/null
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -0,0 +1,6 @@
+import PrometheusGraph from './prometheus_graph';
+
+document.addEventListener('DOMContentLoaded', function onLoad() {
+ document.removeEventListener('DOMContentLoaded', onLoad, false);
+ return new PrometheusGraph();
+}, false);
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
index 71eb746edac..844a0785bc9 100644
--- a/app/assets/javascripts/monitoring/prometheus_graph.js
+++ b/app/assets/javascripts/monitoring/prometheus_graph.js
@@ -1,11 +1,10 @@
-/* eslint-disable no-new */
+/* eslint-disable no-new*/
/* global Flash */
import d3 from 'd3';
-import _ from 'underscore';
import statusCodes from '~/lib/utils/http_status';
-import '~/lib/utils/common_utils';
-import '~/flash';
+import '../lib/utils/common_utils';
+import '../flash';
const prometheusGraphsContainer = '.prometheus-graph';
const metricsEndpoint = 'metrics.json';
@@ -31,22 +30,21 @@ class PrometheusGraph {
}
createGraph() {
- const self = this;
- _.each(this.data, (value, key) => {
- if (value.length > 0 && (key === 'cpu_values' || key === 'memory_values')) {
- self.plotValues(value, key);
+ Object.keys(this.data).forEach((key) => {
+ const value = this.data[key];
+ if (value.length > 0) {
+ this.plotValues(value, key);
}
});
}
init() {
- const self = this;
this.getData().then((metricsResponse) => {
- if (metricsResponse === {}) {
+ if (Object.keys(metricsResponse).length === 0) {
new Flash('Empty metrics', 'alert');
} else {
- self.transformData(metricsResponse);
- self.createGraph();
+ this.transformData(metricsResponse);
+ this.createGraph();
}
});
}
@@ -182,7 +180,7 @@ class PrometheusGraph {
// Metric Usage
axisLabelContainer.append('rect')
.attr('x', this.originalWidth - 170)
- .attr('y', (this.originalHeight / 2) - 80)
+ .attr('y', (this.originalHeight / 2) - 60)
.style('fill', graphSpecifics.area_fill_color)
.attr('width', 20)
.attr('height', 35);
@@ -190,13 +188,13 @@ class PrometheusGraph {
axisLabelContainer.append('text')
.attr('class', 'label-axis-text')
.attr('x', this.originalWidth - 140)
- .attr('y', (this.originalHeight / 2) - 65)
- .text(graphSpecifics.graph_legend_title);
+ .attr('y', (this.originalHeight / 2) - 50)
+ .text('Average');
axisLabelContainer.append('text')
.attr('class', 'text-metric-usage')
.attr('x', this.originalWidth - 140)
- .attr('y', (this.originalHeight / 2) - 50);
+ .attr('y', (this.originalHeight / 2) - 25);
}
handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, key) {
@@ -265,12 +263,12 @@ class PrometheusGraph {
cpu_values: {
area_fill_color: '#edf3fc',
line_color: '#5b99f7',
- graph_legend_title: 'CPU Usage (Cores)',
+ graph_legend_title: 'CPU utilization (%)',
},
memory_values: {
area_fill_color: '#fca326',
line_color: '#fc6d26',
- graph_legend_title: 'Memory Usage (MB)',
+ graph_legend_title: 'Memory usage (MB)',
},
};
@@ -321,12 +319,14 @@ class PrometheusGraph {
transformData(metricsResponse) {
const metricTypes = {};
- _.each(metricsResponse.metrics, (value, key) => {
- const metricValues = value[0].values;
- metricTypes[key] = _.map(metricValues, metric => ({
- time: new Date(metric[0] * 1000),
- value: metric[1],
- }));
+ Object.keys(metricsResponse.metrics).forEach((key) => {
+ if (key === 'cpu_values' || key === 'memory_values') {
+ const metricValues = (metricsResponse.metrics[key])[0];
+ metricTypes[key] = metricValues.values.map(metric => ({
+ time: new Date(metric[0] * 1000),
+ value: metric[1],
+ }));
+ }
});
this.data = metricTypes;
}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 47cc34e7a20..1d563c63f39 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,14 +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 */
+import Cookies from 'js-cookie';
+
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
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index c38bc762675..4ccea0624ee 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -25,6 +25,7 @@
bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
$('#user_notification_email').on('change', this.submitForm);
+ $('#user_notified_of_own_activity').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
$('.update-username').on('ajax:complete', this.afterUpdateUsername);
$('.update-notifications').on('ajax:success', this.onUpdateNotifs);
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index db7ceaa2421..f944fcc5a58 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,7 +1,8 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
-/* global Cookies */
/* global ProjectSelect */
+import Cookies from 'js-cookie';
+
(function() {
this.Project = (function() {
function Project() {
diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
index 5cf28aa7a73..1d4bb8a13d6 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
@@ -6,7 +6,7 @@ class ProtectedBranchDropdown {
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
- this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch');
+ this.$protectedBranch = this.$dropdownContainer.find('.js-create-new-protected-branch');
this.buildDropdown();
this.bindEvents();
@@ -46,7 +46,9 @@ class ProtectedBranchDropdown {
this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this));
}
- onClickCreateWildcard() {
+ onClickCreateWildcard(e) {
+ e.preventDefault();
+
// Refresh the dropdown's data, which ends up calling `getProtectedBranches`
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex();
@@ -69,7 +71,7 @@ class ProtectedBranchDropdown {
if (branchName) {
this.$dropdownContainer
- .find('.create-new-protected-branch code')
+ .find('.js-create-new-protected-branch code')
.text(branchName);
}
diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js
index 48cae8a4fa9..ea91aaa10a6 100644
--- a/app/assets/javascripts/render_gfm.js
+++ b/app/assets/javascripts/render_gfm.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, max-len */
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len */
+
// Render Gitlab flavoured Markdown
//
// Delegates to syntax highlight and render math
diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js
index 76c61c001ba..8b3fee49cb9 100644
--- a/app/assets/javascripts/render_math.js
+++ b/app/assets/javascripts/render_math.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, max-len, no-console */
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len, no-console */
+/* global katex */
+
// Renders math using KaTeX in any element with the
// `js-render-math` class
//
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 903862cac6b..a9b3de281e1 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */
-/* global Cookies */
+
+import Cookies from 'js-cookie';
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
@@ -55,14 +56,15 @@
Sidebar.prototype.toggleTodo = function(e) {
var $btnText, $this, $todoLoading, ajaxType, url;
$this = $(e.currentTarget);
- $todoLoading = $('.js-issuable-todo-loading');
- $btnText = $('.js-issuable-todo-text', $this);
ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST';
if ($this.attr('data-delete-path')) {
url = "" + ($this.attr('data-delete-path'));
} else {
url = "" + ($this.data('url'));
}
+
+ $this.tooltip('hide');
+
return $.ajax({
url: url,
type: ajaxType,
@@ -73,34 +75,44 @@
},
beforeSend: (function(_this) {
return function() {
- return _this.beforeTodoSend($this, $todoLoading);
+ $('.js-issuable-todo').disable()
+ .addClass('is-loading');
};
})(this)
}).done((function(_this) {
return function(data) {
- return _this.todoUpdateDone(data, $this, $btnText, $todoLoading);
+ return _this.todoUpdateDone(data);
};
})(this));
};
- Sidebar.prototype.beforeTodoSend = function($btn, $todoLoading) {
- $btn.disable();
- return $todoLoading.removeClass('hidden');
- };
+ Sidebar.prototype.todoUpdateDone = function(data) {
+ const deletePath = data.delete_path ? data.delete_path : null;
+ const attrPrefix = deletePath ? 'mark' : 'todo';
+ const $todoBtns = $('.js-issuable-todo');
- Sidebar.prototype.todoUpdateDone = function(data, $btn, $btnText, $todoLoading) {
$(document).trigger('todo:toggle', data.count);
- $btn.enable();
- $todoLoading.addClass('hidden');
+ $todoBtns.each((i, el) => {
+ const $el = $(el);
+ const $elText = $el.find('.js-issuable-todo-inner');
- if (data.delete_path != null) {
- $btn.attr('aria-label', $btn.data('mark-text')).attr('data-delete-path', data.delete_path);
- return $btnText.text($btn.data('mark-text'));
- } else {
- $btn.attr('aria-label', $btn.data('todo-text')).removeAttr('data-delete-path');
- return $btnText.text($btn.data('todo-text'));
- }
+ $el.removeClass('is-loading')
+ .enable()
+ .attr('aria-label', $el.data(`${attrPrefix}-text`))
+ .attr('data-delete-path', deletePath)
+ .attr('title', $el.data(`${attrPrefix}-text`));
+
+ if ($el.hasClass('has-tooltip')) {
+ $el.tooltip('fixTitle');
+ }
+
+ if ($el.data(`${attrPrefix}-icon`)) {
+ $elText.html($el.data(`${attrPrefix}-icon`));
+ } else {
+ $elText.text($el.data(`${attrPrefix}-text`));
+ }
+ });
};
Sidebar.prototype.sidebarDropdownLoading = function(e) {
@@ -197,9 +209,9 @@
};
Sidebar.prototype.setSidebarHeight = function() {
- const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
+ const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + $('.sub-nav-scroll').outerHeight();
const $rightSidebar = $('.js-right-sidebar');
- const diff = $navHeight - $('body').scrollTop();
+ const diff = $navHeight - $(window).scrollTop();
if (diff > 0) {
$rightSidebar.outerHeight($(window).height() - diff);
} else {
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 81766f4bd55..fd5097696ad 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -33,6 +33,10 @@
};
Shortcuts.prototype.toggleMarkdownPreview = function(e) {
+ // Check if short-cut was triggered while in Write Mode
+ if ($(e.target).hasClass('js-note-text')) {
+ $('.js-md-preview-button').focus();
+ }
return $(document).triggerHandler('markdown-preview:toggle', [e]);
};
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index e7baea894f6..4f1a19924a4 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -22,6 +22,9 @@ require('./shortcuts');
Mousetrap.bind('g m', function() {
return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-merge_requests');
});
+ Mousetrap.bind('g t', function() {
+ return ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-todos');
+ });
Mousetrap.bind('g p', function() {
return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-projects');
});
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 09a58cad2b2..3f5d6724417 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -43,6 +43,9 @@ require('./shortcuts');
Mousetrap.bind('g m', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests');
});
+ Mousetrap.bind('g t', function() {
+ return ShortcutsNavigation.findAndFollowLink('.shortcuts-todos');
+ });
Mousetrap.bind('g w', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki');
});
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
index 62d1604fe9e..9c307915ec4 100644
--- a/app/assets/javascripts/subscription.js
+++ b/app/assets/javascripts/subscription.js
@@ -1,4 +1,4 @@
-/* global Vue */
+import Vue from 'vue';
(() => {
class Subscription {
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js
index e9e9aafd71a..32067ed1fee 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js
+++ b/app/assets/javascripts/templates/issuable_template_selector.js
@@ -1,15 +1,15 @@
/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */
/* global Api */
-require('../blob/template_selector');
+import TemplateSelector from '../blob/template_selectors/template_selector';
((global) => {
- class IssuableTemplateSelector extends gl.TemplateSelector {
+ class IssuableTemplateSelector extends TemplateSelector {
constructor(...args) {
super(...args);
this.projectPath = this.dropdown.data('project-path');
this.namespacePath = this.dropdown.data('namespace-path');
- this.issuableType = this.wrapper.data('issuable-type');
+ this.issuableType = this.$dropdownContainer.data('issuable-type');
this.titleInput = $(`#${this.issuableType}_title`);
const initialQuery = {
@@ -41,16 +41,16 @@ require('../blob/template_selector');
}
setInputValueToTemplateContent() {
- // `this.requestFileSuccess` sets the value of the description input field
+ // `this.setEditorContent` sets the value of the description input field
// to the content of the template selected.
if (this.titleInput.val() === '') {
// If the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the
- // `skipFocus` option to `requestFileSuccess`.
- this.requestFileSuccess(this.currentTemplate, { skipFocus: true });
+ // `skipFocus` option to `setEditorContent`.
+ this.setEditorContent(this.currentTemplate, { skipFocus: true });
this.titleInput.focus();
} else {
- this.requestFileSuccess(this.currentTemplate, { skipFocus: false });
+ this.setEditorContent(this.currentTemplate, { skipFocus: false });
}
return;
}
diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js
index 059e6c628b3..19c9efe7fbd 100644
--- a/app/assets/javascripts/user.js
+++ b/app/assets/javascripts/user.js
@@ -1,5 +1,6 @@
/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign */
-/* global Cookies */
+
+import Cookies from 'js-cookie';
((global) => {
global.User = class {
diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js
index 99419e85b20..fa078b48bf8 100644
--- a/app/assets/javascripts/user_callout.js
+++ b/app/assets/javascripts/user_callout.js
@@ -1,60 +1,27 @@
-/* global Cookies */
-
-const userCalloutElementName = '.user-callout';
-const closeButton = '.close-user-callout';
-const userCalloutBtn = '.user-callout-btn';
-const userCalloutSvgAttrName = 'callout-svg';
+import Cookies from 'js-cookie';
const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
-const USER_CALLOUT_TEMPLATE = `
- <div class="bordered-box landing content-block">
- <button class="btn btn-default close close-user-callout" type="button">
- <i class="fa fa-times dismiss-icon"></i>
- </button>
- <div class="row">
- <div class="col-sm-3 col-xs-12 svg-container">
- </div>
- <div class="col-sm-8 col-xs-12 inner-content">
- <h4>
- Customize your experience
- </h4>
- <p>
- Change syntax themes, default project pages, and more in preferences.
- </p>
- <a class="btn user-callout-btn" href="/profile/preferences">Check it out</a>
- </div>
- </div>
-</div>`;
-
-class UserCallout {
+export default class UserCallout {
constructor() {
this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE);
- this.userCalloutBody = $(userCalloutElementName);
- this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName);
- $(userCalloutElementName).removeAttr(userCalloutSvgAttrName);
+ this.userCalloutBody = $('.user-callout');
this.init();
}
init() {
- const $template = $(USER_CALLOUT_TEMPLATE);
if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
- $template.find('.svg-container').append(this.userCalloutSvg);
- this.userCalloutBody.append($template);
- $template.find(closeButton).on('click', e => this.dismissCallout(e));
- $template.find(userCalloutBtn).on('click', e => this.dismissCallout(e));
- } else {
- this.userCalloutBody.remove();
+ $('.js-close-callout').on('click', e => this.dismissCallout(e));
}
}
dismissCallout(e) {
- Cookies.set(USER_CALLOUT_COOKIE, 'true');
const $currentTarget = $(e.currentTarget);
- if ($currentTarget.hasClass('close-user-callout')) {
+
+ Cookies.set(USER_CALLOUT_COOKIE, 'true');
+
+ if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove();
}
}
}
-
-module.exports = UserCallout;
diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js
index 465618e3d53..5db0d936ad8 100644
--- a/app/assets/javascripts/user_tabs.js
+++ b/app/assets/javascripts/user_tabs.js
@@ -1,4 +1,4 @@
-/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, no-param-reassign */
+/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, no-param-reassign, class-methods-use-this */
/*
UserTabs
@@ -82,8 +82,19 @@ content on the Users#show page.
}
bindEvents() {
- return this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
+ this.changeProjectsPageWrapper = this.changeProjectsPage.bind(this);
+
+ this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
+
+ this.$parentEl.on('click', '.gl-pagination a', this.changeProjectsPageWrapper);
+ }
+
+ changeProjectsPage(e) {
+ e.preventDefault();
+
+ $('.tab-pane.active').empty();
+ this.loadTab($(e.target).attr('href'), this.getCurrentAction());
}
tabShown(event) {
@@ -119,7 +130,7 @@ content on the Users#show page.
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET',
- url: `${source}.json`,
+ url: source,
success: (data) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
@@ -153,6 +164,10 @@ content on the Users#show page.
}, document.title, new_state);
return new_state;
}
+
+ getCurrentAction() {
+ return this.$parentEl.find('.nav-links .active a').data('action');
+ }
}
global.UserTabs = UserTabs;
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index c7a57b47834..48e20cf501f 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,8 +1,9 @@
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, no-param-reassign */
-/* global Vue */
/* global Issuable */
/* global ListUser */
+import Vue from 'vue';
+
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
slice = [].slice;
@@ -53,7 +54,7 @@
$loading = $block.find('.block-loading').fadeOut();
var updateIssueBoardsIssue = function () {
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () {
$loading.fadeOut();
@@ -90,7 +91,7 @@
data = {};
data[abilityName] = {};
data[abilityName].assignee_id = selected != null ? selected : null;
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
diff --git a/app/assets/javascripts/vue_pipelines_index/components/async_button.js b/app/assets/javascripts/vue_pipelines_index/components/async_button.js
new file mode 100644
index 00000000000..58b8db4d519
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/async_button.js
@@ -0,0 +1,93 @@
+/* eslint-disable no-new, no-alert */
+/* global Flash */
+import '~/flash';
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+
+ title: {
+ type: String,
+ required: true,
+ },
+
+ icon: {
+ type: String,
+ required: true,
+ },
+
+ cssClass: {
+ type: String,
+ required: true,
+ },
+
+ confirmActionMessage: {
+ type: String,
+ required: false,
+ },
+ },
+
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+
+ computed: {
+ iconClass() {
+ return `fa fa-${this.icon}`;
+ },
+
+ buttonClass() {
+ return `btn has-tooltip ${this.cssClass}`;
+ },
+ },
+
+ methods: {
+ onClick() {
+ if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
+ this.makeRequest();
+ } else if (!this.confirmActionMessage) {
+ this.makeRequest();
+ }
+ },
+
+ makeRequest() {
+ this.isLoading = true;
+
+ this.service.postAction(this.endpoint)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshPipelines');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.');
+ });
+ },
+ },
+
+ template: `
+ <button
+ type="button"
+ @click="onClick"
+ :class="buttonClass"
+ :title="title"
+ :aria-label="title"
+ data-container="body"
+ data-placement="top"
+ :disabled="isLoading">
+ <i :class="iconClass" aria-hidden="true"/>
+ <i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading" />
+ </button>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/empty_state.js b/app/assets/javascripts/vue_pipelines_index/components/empty_state.js
new file mode 100644
index 00000000000..56b4858f4b4
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/empty_state.js
@@ -0,0 +1,33 @@
+import pipelinesEmptyStateSVG from 'empty_states/icons/_pipelines_empty.svg';
+
+export default {
+ props: {
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+
+ template: `
+ <div class="row empty-state">
+ <div class="col-xs-12">
+ <div class="svg-content">
+ ${pipelinesEmptyStateSVG}
+ </div>
+ </div>
+
+ <div class="col-xs-12 text-center">
+ <div class="text-content">
+ <h4>Build with confidence</h4>
+ <p>
+ Continous Integration can help catch bugs by running your tests automatically,
+ while Continuous Deployment can help you deliver code to your product environment.
+ </p>
+ <a :href="helpPagePath" class="btn btn-info">
+ Get started with Pipelines
+ </a>
+ </div>
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/error_state.js b/app/assets/javascripts/vue_pipelines_index/components/error_state.js
new file mode 100644
index 00000000000..e5d228bddf8
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/error_state.js
@@ -0,0 +1,19 @@
+import pipelinesErrorStateSVG from 'empty_states/icons/_pipelines_failed.svg';
+
+export default {
+ template: `
+ <div class="row empty-state js-pipelines-error-state">
+ <div class="col-xs-12">
+ <div class="svg-content">
+ ${pipelinesErrorStateSVG}
+ </div>
+ </div>
+
+ <div class="col-xs-12 text-center">
+ <div class="text-content">
+ <h4>The API failed to fetch the pipelines.</h4>
+ </div>
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js b/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js
new file mode 100644
index 00000000000..6aa10531034
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js
@@ -0,0 +1,52 @@
+export default {
+ props: {
+ newPipelinePath: {
+ type: String,
+ required: true,
+ },
+
+ hasCiEnabled: {
+ type: Boolean,
+ required: true,
+ },
+
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+
+ ciLintPath: {
+ type: String,
+ required: true,
+ },
+
+ canCreatePipeline: {
+ type: Boolean,
+ required: true,
+ },
+ },
+
+ template: `
+ <div class="nav-controls">
+ <a
+ v-if="canCreatePipeline"
+ :href="newPipelinePath"
+ class="btn btn-create">
+ Run Pipeline
+ </a>
+
+ <a
+ v-if="!hasCiEnabled"
+ :href="helpPagePath"
+ class="btn btn-info">
+ Get started with Pipelines
+ </a>
+
+ <a
+ :href="ciLintPath"
+ class="btn btn-default">
+ CI Lint
+ </a>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js
new file mode 100644
index 00000000000..1626ae17a30
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js
@@ -0,0 +1,72 @@
+export default {
+ props: {
+ scope: {
+ type: String,
+ required: true,
+ },
+
+ count: {
+ type: Object,
+ required: true,
+ },
+
+ paths: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ mounted() {
+ $(document).trigger('init.scrolling-tabs');
+ },
+
+ template: `
+ <ul class="nav-links scrolling-tabs">
+ <li
+ class="js-pipelines-tab-all"
+ :class="{ 'active': scope === 'all'}">
+ <a :href="paths.allPath">
+ All
+ <span class="badge js-totalbuilds-count">
+ {{count.all}}
+ </span>
+ </a>
+ </li>
+ <li class="js-pipelines-tab-pending"
+ :class="{ 'active': scope === 'pending'}">
+ <a :href="paths.pendingPath">
+ Pending
+ <span class="badge">
+ {{count.pending}}
+ </span>
+ </a>
+ </li>
+ <li class="js-pipelines-tab-running"
+ :class="{ 'active': scope === 'running'}">
+ <a :href="paths.runningPath">
+ Running
+ <span class="badge">
+ {{count.running}}
+ </span>
+ </a>
+ </li>
+ <li class="js-pipelines-tab-finished"
+ :class="{ 'active': scope === 'finished'}">
+ <a :href="paths.finishedPath">
+ Finished
+ <span class="badge">
+ {{count.finished}}
+ </span>
+ </a>
+ </li>
+ <li class="js-pipelines-tab-branches"
+ :class="{ 'active': scope === 'branches'}">
+ <a :href="paths.branchesPath">Branches</a>
+ </li>
+ <li class="js-pipelines-tab-tags"
+ :class="{ 'active': scope === 'tags'}">
+ <a :href="paths.tagsPath">Tags</a>
+ </li>
+ </ul>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js b/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js
new file mode 100644
index 00000000000..4e183d5c8ec
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js
@@ -0,0 +1,56 @@
+export default {
+ props: [
+ 'pipeline',
+ ],
+ computed: {
+ user() {
+ return !!this.pipeline.user;
+ },
+ },
+ template: `
+ <td>
+ <a
+ :href="pipeline.path"
+ class="js-pipeline-url-link">
+ <span class="pipeline-id">#{{pipeline.id}}</span>
+ </a>
+ <span>by</span>
+ <a
+ class="js-pipeline-url-user"
+ v-if="user"
+ :href="pipeline.user.web_url">
+ <img
+ v-if="user"
+ class="avatar has-tooltip s20 "
+ :title="pipeline.user.name"
+ data-container="body"
+ :src="pipeline.user.avatar_url"
+ >
+ </a>
+ <span
+ v-if="!user"
+ class="js-pipeline-url-api api monospace">
+ API
+ </span>
+ <span
+ v-if="pipeline.flags.latest"
+ class="js-pipeline-url-lastest label label-success has-tooltip"
+ title="Latest pipeline for this branch"
+ data-original-title="Latest pipeline for this branch">
+ latest
+ </span>
+ <span
+ v-if="pipeline.flags.yaml_errors"
+ class="js-pipeline-url-yaml label label-danger has-tooltip"
+ :title="pipeline.yaml_errors"
+ :data-original-title="pipeline.yaml_errors">
+ yaml invalid
+ </span>
+ <span
+ v-if="pipeline.flags.stuck"
+ class="js-pipeline-url-stuck label label-warning">
+ stuck
+ </span>
+ </td>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
new file mode 100644
index 00000000000..4bb2b048884
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
@@ -0,0 +1,71 @@
+/* eslint-disable no-new */
+/* global Flash */
+import '~/flash';
+import playIconSvg from 'icons/_icon_play.svg';
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ actions: {
+ type: Array,
+ required: true,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ playIconSvg,
+ isLoading: false,
+ };
+ },
+
+ methods: {
+ onClickAction(endpoint) {
+ this.isLoading = true;
+
+ this.service.postAction(endpoint)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshPipelines');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.');
+ });
+ },
+ },
+
+ template: `
+ <div class="btn-group" v-if="actions">
+ <button
+ type="button"
+ class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
+ title="Manual job"
+ data-toggle="dropdown"
+ data-placement="top"
+ aria-label="Manual job"
+ :disabled="isLoading">
+ ${playIconSvg}
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ </button>
+
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="action in actions">
+ <button
+ type="button"
+ class="js-pipeline-action-link no-btn"
+ @click="onClickAction(action.path)">
+ ${playIconSvg}
+ <span>{{action.name}}</span>
+ </button>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
new file mode 100644
index 00000000000..f18e2dfadaf
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
@@ -0,0 +1,33 @@
+export default {
+ props: {
+ artifacts: {
+ type: Array,
+ required: true,
+ },
+ },
+
+ template: `
+ <div class="btn-group" role="group">
+ <button
+ class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
+ title="Artifacts"
+ data-placement="top"
+ data-toggle="dropdown"
+ aria-label="Artifacts">
+ <i class="fa fa-download" aria-hidden="true"></i>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="artifact in artifacts">
+ <a
+ rel="nofollow"
+ download
+ :href="artifact.path">
+ <i class="fa fa-download" aria-hidden="true"></i>
+ <span>Download {{artifact.name}} artifacts</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/stage.js b/app/assets/javascripts/vue_pipelines_index/components/stage.js
new file mode 100644
index 00000000000..a2c29002707
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/stage.js
@@ -0,0 +1,116 @@
+/* global Flash */
+import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
+import createdSvg from 'icons/_icon_status_created_borderless.svg';
+import failedSvg from 'icons/_icon_status_failed_borderless.svg';
+import manualSvg from 'icons/_icon_status_manual_borderless.svg';
+import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
+import runningSvg from 'icons/_icon_status_running_borderless.svg';
+import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
+import successSvg from 'icons/_icon_status_success_borderless.svg';
+import warningSvg from 'icons/_icon_status_warning_borderless.svg';
+
+export default {
+ data() {
+ const svgsDictionary = {
+ icon_status_canceled: canceledSvg,
+ icon_status_created: createdSvg,
+ icon_status_failed: failedSvg,
+ icon_status_manual: manualSvg,
+ icon_status_pending: pendingSvg,
+ icon_status_running: runningSvg,
+ icon_status_skipped: skippedSvg,
+ icon_status_success: successSvg,
+ icon_status_warning: warningSvg,
+ };
+
+ return {
+ builds: '',
+ spinner: '<span class="fa fa-spinner fa-spin"></span>',
+ svg: svgsDictionary[this.stage.status.icon],
+ };
+ },
+
+ props: {
+ stage: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ updated() {
+ if (this.builds) {
+ this.stopDropdownClickPropagation();
+ }
+ },
+
+ methods: {
+ fetchBuilds(e) {
+ const ariaExpanded = e.currentTarget.attributes['aria-expanded'];
+
+ if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null;
+
+ return this.$http.get(this.stage.dropdown_path)
+ .then((response) => {
+ this.builds = JSON.parse(response.body).html;
+ }, () => {
+ const flash = new Flash('Something went wrong on our end.');
+ return flash;
+ });
+ },
+
+ /**
+ * 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() {
+ $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
+ e.stopPropagation();
+ });
+ },
+ },
+ computed: {
+ buildsOrSpinner() {
+ return this.builds ? this.builds : this.spinner;
+ },
+ dropdownClass() {
+ if (this.builds) return 'js-builds-dropdown-container';
+ return 'js-builds-dropdown-loading builds-dropdown-loading';
+ },
+ buildStatus() {
+ return `Build: ${this.stage.status.label}`;
+ },
+ tooltip() {
+ return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
+ },
+ triggerButtonClass() {
+ return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
+ },
+ },
+ template: `
+ <div>
+ <button
+ @click="fetchBuilds($event)"
+ :class="triggerButtonClass"
+ :title="stage.title"
+ data-placement="top"
+ data-toggle="dropdown"
+ type="button"
+ :aria-label="stage.title">
+ <span v-html="svg" aria-hidden="true"></span>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
+ <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
+ <div class="arrow-up" aria-hidden="true"></div>
+ <div
+ :class="dropdownClass"
+ class="js-builds-dropdown-list scrollable-menu"
+ v-html="buildsOrSpinner">
+ </div>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/status.js b/app/assets/javascripts/vue_pipelines_index/components/status.js
new file mode 100644
index 00000000000..21a281af438
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/status.js
@@ -0,0 +1,60 @@
+import canceledSvg from 'icons/_icon_status_canceled.svg';
+import createdSvg from 'icons/_icon_status_created.svg';
+import failedSvg from 'icons/_icon_status_failed.svg';
+import manualSvg from 'icons/_icon_status_manual.svg';
+import pendingSvg from 'icons/_icon_status_pending.svg';
+import runningSvg from 'icons/_icon_status_running.svg';
+import skippedSvg from 'icons/_icon_status_skipped.svg';
+import successSvg from 'icons/_icon_status_success.svg';
+import warningSvg from 'icons/_icon_status_warning.svg';
+
+export default {
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ const svgsDictionary = {
+ icon_status_canceled: canceledSvg,
+ icon_status_created: createdSvg,
+ icon_status_failed: failedSvg,
+ icon_status_manual: manualSvg,
+ icon_status_pending: pendingSvg,
+ icon_status_running: runningSvg,
+ icon_status_skipped: skippedSvg,
+ icon_status_success: successSvg,
+ icon_status_warning: warningSvg,
+ };
+
+ return {
+ svg: svgsDictionary[this.pipeline.details.status.icon],
+ };
+ },
+
+ computed: {
+ cssClasses() {
+ return `ci-status ci-${this.pipeline.details.status.group}`;
+ },
+
+ detailsPath() {
+ const { status } = this.pipeline.details;
+ return status.has_details ? status.details_path : false;
+ },
+
+ content() {
+ return `${this.svg} ${this.pipeline.details.status.text}`;
+ },
+ },
+ template: `
+ <td class="commit-link">
+ <a
+ :class="cssClasses"
+ :href="detailsPath"
+ v-html="content">
+ </a>
+ </td>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/time_ago.js b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
new file mode 100644
index 00000000000..498d0715f54
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
@@ -0,0 +1,71 @@
+import iconTimerSvg from 'icons/_icon_timer.svg';
+import '../../lib/utils/datetime_utility';
+
+export default {
+ data() {
+ return {
+ currentTime: new Date(),
+ iconTimerSvg,
+ };
+ },
+ props: ['pipeline'],
+ computed: {
+ timeAgo() {
+ return gl.utils.getTimeago();
+ },
+ localTimeFinished() {
+ return gl.utils.formatDate(this.pipeline.details.finished_at);
+ },
+ timeStopped() {
+ const changeTime = this.currentTime;
+ const options = {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ };
+ options.timeZoneName = 'short';
+ const finished = this.pipeline.details.finished_at;
+ if (!finished && changeTime) return false;
+ return ({ words: this.timeAgo.format(finished) });
+ },
+ duration() {
+ const { duration } = this.pipeline.details;
+ const date = new Date(duration * 1000);
+
+ let hh = date.getUTCHours();
+ let mm = date.getUTCMinutes();
+ let ss = date.getSeconds();
+
+ if (hh < 10) hh = `0${hh}`;
+ if (mm < 10) mm = `0${mm}`;
+ if (ss < 10) ss = `0${ss}`;
+
+ if (duration !== null) return `${hh}:${mm}:${ss}`;
+ return false;
+ },
+ },
+ methods: {
+ changeTime() {
+ this.currentTime = new Date();
+ },
+ },
+ template: `
+ <td class="pipelines-time-ago">
+ <p class="duration" v-if='duration'>
+ <span v-html="iconTimerSvg"></span>
+ {{duration}}
+ </p>
+ <p class="finished-at" v-if='timeStopped'>
+ <i class="fa fa-calendar"></i>
+ <time
+ data-toggle="tooltip"
+ data-placement="top"
+ data-container="body"
+ :data-original-title='localTimeFinished'>
+ {{timeStopped.words}}
+ </time>
+ </p>
+ </td>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/event_hub.js b/app/assets/javascripts/vue_pipelines_index/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js b/app/assets/javascripts/vue_pipelines_index/index.js
index a90bd1518e9..48f9181a8d9 100644
--- a/app/assets/javascripts/vue_pipelines_index/index.js
+++ b/app/assets/javascripts/vue_pipelines_index/index.js
@@ -1,29 +1,22 @@
-/* eslint-disable no-param-reassign */
-/* global Vue, VueResource, gl */
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../lib/utils/common_utils');
-require('../vue_shared/vue_resource_interceptor');
-require('./pipelines');
+import Vue from 'vue';
+import PipelinesStore from './stores/pipelines_store';
+import PipelinesComponent from './pipelines';
+import '../vue_shared/vue_resource_interceptor';
$(() => new Vue({
- el: document.querySelector('.vue-pipelines-index'),
+ el: document.querySelector('#pipelines-list-vue'),
data() {
- const project = document.querySelector('.pipelines');
+ const store = new PipelinesStore();
return {
- scope: project.dataset.url,
- store: new gl.PipelineStore(),
+ store,
};
},
components: {
- 'vue-pipelines': gl.VuePipelines,
+ 'vue-pipelines': PipelinesComponent,
},
template: `
- <vue-pipelines
- :scope="scope"
- :store="store">
- </vue-pipelines>
+ <vue-pipelines :store="store" />
`,
}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js
deleted file mode 100644
index 583d6915a85..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js
+++ /dev/null
@@ -1,123 +0,0 @@
-/* global Vue, Flash, gl */
-/* eslint-disable no-param-reassign, no-alert */
-const playIconSvg = require('icons/_icon_play.svg');
-
-((gl) => {
- gl.VuePipelineActions = Vue.extend({
- props: ['pipeline'],
- computed: {
- actions() {
- return this.pipeline.details.manual_actions.length > 0;
- },
- artifacts() {
- return this.pipeline.details.artifacts.length > 0;
- },
- },
- methods: {
- download(name) {
- return `Download ${name} artifacts`;
- },
-
- /**
- * Shows a dialog when the user clicks in the cancel button.
- * We need to prevent the default behavior and stop propagation because the
- * link relies on UJS.
- *
- * @param {Event} event
- */
- confirmAction(event) {
- if (!confirm('Are you sure you want to cancel this pipeline?')) {
- event.preventDefault();
- event.stopPropagation();
- }
- },
- },
-
- data() {
- return { playIconSvg };
- },
-
- template: `
- <td class="pipeline-actions">
- <div class="pull-right">
- <div class="btn-group">
- <div class="btn-group" v-if="actions">
- <button
- class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
- data-toggle="dropdown"
- title="Manual job"
- data-placement="top"
- data-container="body"
- aria-label="Manual job">
- <span v-html="playIconSvg" aria-hidden="true"></span>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- </button>
- <ul class="dropdown-menu dropdown-menu-align-right">
- <li v-for='action in pipeline.details.manual_actions'>
- <a
- rel="nofollow"
- data-method="post"
- :href="action.path" >
- <span v-html="playIconSvg" aria-hidden="true"></span>
- <span>{{action.name}}</span>
- </a>
- </li>
- </ul>
- </div>
-
- <div class="btn-group" v-if="artifacts">
- <button
- class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
- title="Artifacts"
- data-placement="top"
- data-container="body"
- data-toggle="dropdown"
- aria-label="Artifacts">
- <i class="fa fa-download" aria-hidden="true"></i>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- </button>
- <ul class="dropdown-menu dropdown-menu-align-right">
- <li v-for='artifact in pipeline.details.artifacts'>
- <a
- rel="nofollow"
- :href="artifact.path">
- <i class="fa fa-download" aria-hidden="true"></i>
- <span>{{download(artifact.name)}}</span>
- </a>
- </li>
- </ul>
- </div>
- <div class="btn-group" v-if="pipeline.flags.retryable">
- <a
- class="btn btn-default btn-retry has-tooltip"
- title="Retry"
- rel="nofollow"
- data-method="post"
- data-placement="top"
- data-container="body"
- data-toggle="dropdown"
- :href='pipeline.retry_path'
- aria-label="Retry">
- <i class="fa fa-repeat" aria-hidden="true"></i>
- </a>
- </div>
- <div class="btn-group" v-if="pipeline.flags.cancelable">
- <a
- class="btn btn-remove has-tooltip"
- title="Cancel"
- rel="nofollow"
- data-method="post"
- data-placement="top"
- data-container="body"
- data-toggle="dropdown"
- :href='pipeline.cancel_path'
- aria-label="Cancel">
- <i class="fa fa-remove" aria-hidden="true"></i>
- </a>
- </div>
- </div>
- </div>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js
deleted file mode 100644
index ae5649f0519..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- gl.VuePipelineUrl = Vue.extend({
- props: [
- 'pipeline',
- ],
- computed: {
- user() {
- return !!this.pipeline.user;
- },
- },
- template: `
- <td>
- <a :href='pipeline.path'>
- <span class="pipeline-id">#{{pipeline.id}}</span>
- </a>
- <span>by</span>
- <a
- v-if='user'
- :href='pipeline.user.web_url'
- >
- <img
- v-if='user'
- class="avatar has-tooltip s20 "
- :title='pipeline.user.name'
- data-container="body"
- :src='pipeline.user.avatar_url'
- >
- </a>
- <span
- v-if='!user'
- class="api monospace"
- >
- API
- </span>
- <span
- v-if='pipeline.flags.latest'
- class="label label-success has-tooltip"
- title="Latest pipeline for this branch"
- data-original-title="Latest pipeline for this branch"
- >
- latest
- </span>
- <span
- v-if='pipeline.flags.yaml_errors'
- class="label label-danger has-tooltip"
- :title='pipeline.yaml_errors'
- :data-original-title='pipeline.yaml_errors'
- >
- yaml invalid
- </span>
- <span
- v-if='pipeline.flags.stuck'
- class="label label-warning"
- >
- stuck
- </span>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js
index 601ef41e917..9bdc232b7da 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js
@@ -1,87 +1,246 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
+import Vue from 'vue';
+import PipelinesService from './services/pipelines_service';
+import eventHub from './event_hub';
+import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
+import TablePaginationComponent from '../vue_shared/components/table_pagination';
+import EmptyState from './components/empty_state';
+import ErrorState from './components/error_state';
+import NavigationTabs from './components/navigation_tabs';
+import NavigationControls from './components/nav_controls';
-window.Vue = require('vue');
-require('../vue_shared/components/table_pagination');
-require('./store');
-require('../vue_shared/components/pipelines_table');
-const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store');
+export default {
+ props: {
+ store: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ components: {
+ 'gl-pagination': TablePaginationComponent,
+ 'pipelines-table-component': PipelinesTableComponent,
+ 'empty-state': EmptyState,
+ 'error-state': ErrorState,
+ 'navigation-tabs': NavigationTabs,
+ 'navigation-controls': NavigationControls,
+ },
+
+ data() {
+ const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
+
+ return {
+ endpoint: pipelinesData.endpoint,
+ cssClass: pipelinesData.cssClass,
+ helpPagePath: pipelinesData.helpPagePath,
+ newPipelinePath: pipelinesData.newPipelinePath,
+ canCreatePipeline: pipelinesData.canCreatePipeline,
+ allPath: pipelinesData.allPath,
+ pendingPath: pipelinesData.pendingPath,
+ runningPath: pipelinesData.runningPath,
+ finishedPath: pipelinesData.finishedPath,
+ branchesPath: pipelinesData.branchesPath,
+ tagsPath: pipelinesData.tagsPath,
+ hasCi: pipelinesData.hasCi,
+ ciLintPath: pipelinesData.ciLintPath,
+ state: this.store.state,
+ apiScope: 'all',
+ pagenum: 1,
+ isLoading: false,
+ hasError: false,
+ };
+ },
+
+ computed: {
+ canCreatePipelineParsed() {
+ return gl.utils.convertPermissionToBoolean(this.canCreatePipeline);
+ },
-((gl) => {
- gl.VuePipelines = Vue.extend({
+ scope() {
+ const scope = gl.utils.getParameterByName('scope');
+ return scope === null ? 'all' : scope;
+ },
+
+ shouldRenderErrorState() {
+ return this.hasError && !this.isLoading;
+ },
- components: {
- 'gl-pagination': gl.VueGlPagination,
- 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
+ /**
+ * The empty state should only be rendered when the request is made to fetch all pipelines
+ * and none is returned.
+ *
+ * @return {Boolean}
+ */
+ shouldRenderEmptyState() {
+ return !this.isLoading &&
+ !this.hasError &&
+ !this.state.pipelines.length &&
+ (this.scope === 'all' || this.scope === null);
},
- data() {
+ /**
+ * When a specific scope does not have pipelines we render a message.
+ *
+ * @return {Boolean}
+ */
+ shouldRenderNoPipelinesMessage() {
+ return !this.isLoading &&
+ !this.hasError &&
+ !this.state.pipelines.length &&
+ this.scope !== 'all' &&
+ this.scope !== null;
+ },
+
+ shouldRenderTable() {
+ return !this.hasError &&
+ !this.isLoading && this.state.pipelines.length;
+ },
+
+ /**
+ * Pagination should only be rendered when there is more than one page.
+ *
+ * @return {Boolean}
+ */
+ shouldRenderPagination() {
+ return !this.isLoading &&
+ this.state.pipelines.length &&
+ this.state.pageInfo.total > this.state.pageInfo.perPage;
+ },
+
+ hasCiEnabled() {
+ return this.hasCi !== undefined;
+ },
+
+ paths() {
return {
- pipelines: [],
- timeLoopInterval: '',
- intervalId: '',
- apiScope: 'all',
- pageInfo: {},
- pagenum: 1,
- count: {},
- pageRequest: false,
+ allPath: this.allPath,
+ pendingPath: this.pendingPath,
+ finishedPath: this.finishedPath,
+ runningPath: this.runningPath,
+ branchesPath: this.branchesPath,
+ tagsPath: this.tagsPath,
};
},
- props: ['scope', 'store'],
- created() {
- const pagenum = gl.utils.getParameterByName('page');
- const scope = gl.utils.getParameterByName('scope');
- if (pagenum) this.pagenum = pagenum;
- if (scope) this.apiScope = scope;
+ },
- this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
- },
+ created() {
+ this.service = new PipelinesService(this.endpoint);
+
+ this.fetchPipelines();
+
+ eventHub.$on('refreshPipelines', this.fetchPipelines);
+ },
+
+ beforeUpdate() {
+ if (this.state.pipelines.length && this.$children) {
+ this.store.startTimeAgoLoops.call(this, Vue);
+ }
+ },
+
+ beforeDestroyed() {
+ eventHub.$off('refreshPipelines');
+ },
- beforeUpdate() {
- if (this.pipelines.length && this.$children) {
- CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue);
- }
+ methods: {
+ /**
+ * Will change the page number and update the URL.
+ *
+ * @param {Number} pageNumber desired page to go to.
+ */
+ change(pageNumber) {
+ const param = gl.utils.setParamInURL('page', pageNumber);
+
+ gl.utils.visitUrl(param);
+ return param;
},
- methods: {
- /**
- * Will change the page number and update the URL.
- *
- * @param {Number} pageNumber desired page to go to.
- */
- change(pageNumber) {
- const param = gl.utils.setParamInURL('page', pageNumber);
-
- gl.utils.visitUrl(param);
- return param;
- },
+ fetchPipelines() {
+ const pageNumber = gl.utils.getParameterByName('page') || this.pagenum;
+ const scope = gl.utils.getParameterByName('scope') || this.apiScope;
+
+ this.isLoading = true;
+ return this.service.getPipelines(scope, pageNumber)
+ .then(resp => ({
+ headers: resp.headers,
+ body: resp.json(),
+ }))
+ .then((response) => {
+ this.store.storeCount(response.body.count);
+ this.store.storePipelines(response.body.pipelines);
+ this.store.storePagination(response.headers);
+ })
+ .then(() => {
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.hasError = true;
+ this.isLoading = false;
+ });
},
- template: `
- <div>
- <div class="pipelines realtime-loading" v-if='pageRequest'>
- <i class="fa fa-spinner fa-spin"></i>
+ },
+
+ template: `
+ <div :class="cssClass">
+
+ <div
+ class="top-area scrolling-tabs-container inner-page-scroll-tabs"
+ v-if="!isLoading && !shouldRenderEmptyState">
+ <div class="fade-left">
+ <i class="fa fa-angle-left" aria-hidden="true"></i>
</div>
+ <div class="fade-right">
+ <i class="fa fa-angle-right" aria-hidden="true"></i>
+ </div>
+ <navigation-tabs
+ :scope="scope"
+ :count="state.count"
+ :paths="paths" />
+
+ <navigation-controls
+ :new-pipeline-path="newPipelinePath"
+ :has-ci-enabled="hasCiEnabled"
+ :help-page-path="helpPagePath"
+ :ciLintPath="ciLintPath"
+ :can-create-pipeline="canCreatePipelineParsed " />
+ </div>
- <div class="blank-state blank-state-no-icon"
- v-if="!pageRequest && pipelines.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- No pipelines to show
- </h2>
+ <div class="content-list pipelines">
+
+ <div
+ class="realtime-loading"
+ v-if="isLoading">
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
</div>
- <div class="table-holder" v-if='!pageRequest && pipelines.length'>
- <pipelines-table-component :pipelines='pipelines'/>
+ <empty-state
+ v-if="shouldRenderEmptyState"
+ :help-page-path="helpPagePath" />
+
+ <error-state v-if="shouldRenderErrorState" />
+
+ <div
+ class="blank-state blank-state-no-icon"
+ v-if="shouldRenderNoPipelinesMessage">
+ <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
+ </div>
+
+ <div
+ class="table-holder"
+ v-if="shouldRenderTable">
+
+ <pipelines-table-component
+ :pipelines="state.pipelines"
+ :service="service"/>
</div>
<gl-pagination
- v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage'
- :pagenum='pagenum'
- :change='change'
- :count='count.all'
- :pageInfo='pageInfo'
- >
- </gl-pagination>
+ v-if="shouldRenderPagination"
+ :pagenum="pagenum"
+ :change="change"
+ :count="state.count.all"
+ :pageInfo="state.pageInfo"/>
</div>
- `,
- });
-})(window.gl || (window.gl = {}));
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js
new file mode 100644
index 00000000000..708f5068dd3
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js
@@ -0,0 +1,44 @@
+/* eslint-disable class-methods-use-this */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class PipelinesService {
+
+ /**
+ * Commits and merge request endpoints need to be requested with `.json`.
+ *
+ * The url provided to request the pipelines in the new merge request
+ * page already has `.json`.
+ *
+ * @param {String} root
+ */
+ constructor(root) {
+ let endpoint;
+
+ if (root.indexOf('.json') === -1) {
+ endpoint = `${root}.json`;
+ } else {
+ endpoint = root;
+ }
+
+ this.pipelines = Vue.resource(endpoint);
+ }
+
+ getPipelines(scope, page) {
+ return this.pipelines.get({ scope, page });
+ }
+
+ /**
+ * Post request for all pipelines actions.
+ * Endpoint content type needs to be:
+ * `Content-Type:application/x-www-form-urlencoded`
+ *
+ * @param {String} endpoint
+ * @return {Promise}
+ */
+ postAction(endpoint) {
+ return Vue.http.post(endpoint, {}, { emulateJSON: true });
+ }
+}
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js b/app/assets/javascripts/vue_pipelines_index/stage.js
deleted file mode 100644
index ae4f0b4a53b..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/stage.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/* global Vue, Flash, gl */
-/* eslint-disable no-param-reassign */
-import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
-import createdSvg from 'icons/_icon_status_created_borderless.svg';
-import failedSvg from 'icons/_icon_status_failed_borderless.svg';
-import manualSvg from 'icons/_icon_status_manual_borderless.svg';
-import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
-import runningSvg from 'icons/_icon_status_running_borderless.svg';
-import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
-import successSvg from 'icons/_icon_status_success_borderless.svg';
-import warningSvg from 'icons/_icon_status_warning_borderless.svg';
-
-((gl) => {
- gl.VueStage = Vue.extend({
- data() {
- const svgsDictionary = {
- icon_status_canceled: canceledSvg,
- icon_status_created: createdSvg,
- icon_status_failed: failedSvg,
- icon_status_manual: manualSvg,
- icon_status_pending: pendingSvg,
- icon_status_running: runningSvg,
- icon_status_skipped: skippedSvg,
- icon_status_success: successSvg,
- icon_status_warning: warningSvg,
- };
-
- return {
- builds: '',
- spinner: '<span class="fa fa-spinner fa-spin"></span>',
- svg: svgsDictionary[this.stage.status.icon],
- };
- },
-
- props: {
- stage: {
- type: Object,
- required: true,
- },
- },
-
- updated() {
- if (this.builds) {
- this.stopDropdownClickPropagation();
- }
- },
-
- methods: {
- fetchBuilds(e) {
- const areaExpanded = e.currentTarget.attributes['aria-expanded'];
-
- if (areaExpanded && (areaExpanded.textContent === 'true')) return null;
-
- return this.$http.get(this.stage.dropdown_path)
- .then((response) => {
- this.builds = JSON.parse(response.body).html;
- }, () => {
- const flash = new Flash('Something went wrong on our end.');
- return flash;
- });
- },
-
- /**
- * 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() {
- $(this.$el).on('click', '.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item', (e) => {
- e.stopPropagation();
- });
- },
- },
- computed: {
- buildsOrSpinner() {
- return this.builds ? this.builds : this.spinner;
- },
- dropdownClass() {
- if (this.builds) return 'js-builds-dropdown-container';
- return 'js-builds-dropdown-loading builds-dropdown-loading';
- },
- buildStatus() {
- return `Build: ${this.stage.status.label}`;
- },
- tooltip() {
- return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
- },
- triggerButtonClass() {
- return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
- },
- },
- template: `
- <div>
- <button
- @click="fetchBuilds($event)"
- :class="triggerButtonClass"
- :title="stage.title"
- data-placement="top"
- data-toggle="dropdown"
- type="button"
- :aria-label="stage.title">
- <span v-html="svg" aria-hidden="true"></span>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- </button>
- <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
- <div class="arrow-up" aria-hidden="true"></div>
- <div
- :class="dropdownClass"
- class="js-builds-dropdown-list scrollable-menu"
- v-html="buildsOrSpinner">
- </div>
- </ul>
- </div>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/status.js b/app/assets/javascripts/vue_pipelines_index/status.js
deleted file mode 100644
index 8d9f83ac113..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/status.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-import canceledSvg from 'icons/_icon_status_canceled.svg';
-import createdSvg from 'icons/_icon_status_created.svg';
-import failedSvg from 'icons/_icon_status_failed.svg';
-import manualSvg from 'icons/_icon_status_manual.svg';
-import pendingSvg from 'icons/_icon_status_pending.svg';
-import runningSvg from 'icons/_icon_status_running.svg';
-import skippedSvg from 'icons/_icon_status_skipped.svg';
-import successSvg from 'icons/_icon_status_success.svg';
-import warningSvg from 'icons/_icon_status_warning.svg';
-
-((gl) => {
- gl.VueStatusScope = Vue.extend({
- props: [
- 'pipeline',
- ],
-
- data() {
- const svgsDictionary = {
- icon_status_canceled: canceledSvg,
- icon_status_created: createdSvg,
- icon_status_failed: failedSvg,
- icon_status_manual: manualSvg,
- icon_status_pending: pendingSvg,
- icon_status_running: runningSvg,
- icon_status_skipped: skippedSvg,
- icon_status_success: successSvg,
- icon_status_warning: warningSvg,
- };
-
- return {
- svg: svgsDictionary[this.pipeline.details.status.icon],
- };
- },
-
- computed: {
- cssClasses() {
- const cssObject = { 'ci-status': true };
- cssObject[`ci-${this.pipeline.details.status.group}`] = true;
- return cssObject;
- },
-
- detailsPath() {
- const { status } = this.pipeline.details;
- return status.has_details ? status.details_path : false;
- },
-
- content() {
- return `${this.svg} ${this.pipeline.details.status.text}`;
- },
- },
- template: `
- <td class="commit-link">
- <a
- :class="cssClasses"
- :href="detailsPath"
- v-html="content">
- </a>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js b/app/assets/javascripts/vue_pipelines_index/store.js
deleted file mode 100644
index 909007267b9..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/store.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/* global gl, Flash */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- const pageValues = (headers) => {
- const normalized = gl.utils.normalizeHeaders(headers);
- const paginationInfo = gl.utils.parseIntPagination(normalized);
- return paginationInfo;
- };
-
- gl.PipelineStore = class {
- fetchDataLoop(Vue, pageNum, url, apiScope) {
- this.pageRequest = true;
-
- return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
- .then((response) => {
- const pageInfo = pageValues(response.headers);
- this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
-
- const res = JSON.parse(response.body);
- this.count = Object.assign({}, this.count, res.count);
- this.pipelines = Object.assign([], this.pipelines, res.pipelines);
-
- this.pageRequest = false;
- }, () => {
- this.pageRequest = false;
- return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
- });
- }
- };
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
index f1b80e45444..7ac10086a55 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_store.js
+++ b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
@@ -1,31 +1,46 @@
/* eslint-disable no-underscore-dangle*/
-/**
- * Pipelines' Store for commits view.
- *
- * Used to store the Pipelines rendered in the commit view in the pipelines table.
- */
-require('../../vue_realtime_listener');
-
-class PipelinesStore {
+import '../../vue_realtime_listener';
+
+export default class PipelinesStore {
constructor() {
this.state = {};
+
this.state.pipelines = [];
+ this.state.count = {};
+ this.state.pageInfo = {};
}
storePipelines(pipelines = []) {
this.state.pipelines = pipelines;
+ }
- return pipelines;
+ storeCount(count = {}) {
+ this.state.count = count;
+ }
+
+ storePagination(pagination = {}) {
+ let paginationInfo;
+
+ if (Object.keys(pagination).length) {
+ const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
+ paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
+ } else {
+ paginationInfo = pagination;
+ }
+
+ this.state.pageInfo = paginationInfo;
}
/**
+ * FIXME: Move this inside the component.
+ *
* Once the data is received we will start the time ago loops.
*
* Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
* update the time to show how long as passed.
*
*/
- static startTimeAgoLoops() {
+ startTimeAgoLoops() {
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => {
@@ -44,5 +59,3 @@ class PipelinesStore {
gl.VueRealtimeListener(removeIntervals, startIntervals);
}
}
-
-module.exports = PipelinesStore;
diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js b/app/assets/javascripts/vue_pipelines_index/time_ago.js
deleted file mode 100644
index a383570857d..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/time_ago.js
+++ /dev/null
@@ -1,78 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-window.Vue = require('vue');
-require('../lib/utils/datetime_utility');
-
-const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg');
-
-((gl) => {
- gl.VueTimeAgo = Vue.extend({
- data() {
- return {
- currentTime: new Date(),
- iconTimerSvg,
- };
- },
- props: ['pipeline'],
- computed: {
- timeAgo() {
- return gl.utils.getTimeago();
- },
- localTimeFinished() {
- return gl.utils.formatDate(this.pipeline.details.finished_at);
- },
- timeStopped() {
- const changeTime = this.currentTime;
- const options = {
- weekday: 'long',
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- };
- options.timeZoneName = 'short';
- const finished = this.pipeline.details.finished_at;
- if (!finished && changeTime) return false;
- return ({ words: this.timeAgo.format(finished) });
- },
- duration() {
- const { duration } = this.pipeline.details;
- const date = new Date(duration * 1000);
-
- let hh = date.getUTCHours();
- let mm = date.getUTCMinutes();
- let ss = date.getSeconds();
-
- if (hh < 10) hh = `0${hh}`;
- if (mm < 10) mm = `0${mm}`;
- if (ss < 10) ss = `0${ss}`;
-
- if (duration !== null) return `${hh}:${mm}:${ss}`;
- return false;
- },
- },
- methods: {
- changeTime() {
- this.currentTime = new Date();
- },
- },
- template: `
- <td class="pipelines-time-ago">
- <p class="duration" v-if='duration'>
- <span v-html="iconTimerSvg"></span>
- {{duration}}
- </p>
- <p class="finished-at" v-if='timeStopped'>
- <i class="fa fa-calendar"></i>
- <time
- data-toggle="tooltip"
- data-placement="top"
- data-container="body"
- :data-original-title='localTimeFinished'>
- {{timeStopped.words}}
- </time>
- </p>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_shared/common_vue.js b/app/assets/javascripts/vue_shared/common_vue.js
new file mode 100644
index 00000000000..eb2a6071fda
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/common_vue.js
@@ -0,0 +1,6 @@
+import Vue from 'vue';
+import './vue_resource_interceptor';
+
+if (process.env.NODE_ENV !== 'production') {
+ Vue.config.productionTip = false;
+}
diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js
index 4381487b79e..fb68abd95a2 100644
--- a/app/assets/javascripts/vue_shared/components/commit.js
+++ b/app/assets/javascripts/vue_shared/components/commit.js
@@ -1,164 +1,157 @@
-/* global Vue */
-window.Vue = require('vue');
-const commitIconSvg = require('icons/_icon_commit.svg');
-
-(() => {
- window.gl = window.gl || {};
-
- window.gl.CommitComponent = Vue.component('commit-component', {
-
- props: {
- /**
- * Indicates the existance of a tag.
- * Used to render the correct icon, if true will render `fa-tag` icon,
- * if false will render `fa-code-fork` icon.
- */
- tag: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- /**
- * If provided is used to render the branch name and url.
- * Should contain the following properties:
- * name
- * ref_url
- */
- commitRef: {
- type: Object,
- required: false,
- default: () => ({}),
- },
-
- /**
- * Used to link to the commit sha.
- */
- commitUrl: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * Used to show the commit short sha that links to the commit url.
- */
- shortSha: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * If provided shows the commit tile.
- */
- title: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * If provided renders information about the author of the commit.
- * When provided should include:
- * `avatar_url` to render the avatar icon
- * `web_url` to link to user profile
- * `username` to render alt and title tags
- */
- author: {
- type: Object,
- required: false,
- default: () => ({}),
- },
+import commitIconSvg from 'icons/_icon_commit.svg';
+
+export default {
+ props: {
+ /**
+ * Indicates the existance of a tag.
+ * Used to render the correct icon, if true will render `fa-tag` icon,
+ * if false will render `fa-code-fork` icon.
+ */
+ tag: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- computed: {
- /**
- * Used to verify if all the properties needed to render the commit
- * ref section were provided.
- *
- * TODO: Improve this! Use lodash _.has when we have it.
- *
- * @returns {Boolean}
- */
- hasCommitRef() {
- return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
- },
-
- /**
- * Used to verify if all the properties needed to render the commit
- * author section were provided.
- *
- * TODO: Improve this! Use lodash _.has when we have it.
- *
- * @returns {Boolean}
- */
- hasAuthor() {
- return this.author &&
- this.author.avatar_url &&
- this.author.web_url &&
- this.author.username;
- },
-
- /**
- * If information about the author is provided will return a string
- * to be rendered as the alt attribute of the img tag.
- *
- * @returns {String}
- */
- userImageAltDescription() {
- return this.author &&
- this.author.username ? `${this.author.username}'s avatar` : null;
- },
+ /**
+ * If provided is used to render the branch name and url.
+ * Should contain the following properties:
+ * name
+ * ref_url
+ */
+ commitRef: {
+ type: Object,
+ required: false,
+ default: () => ({}),
},
- data() {
- return { commitIconSvg };
+ /**
+ * Used to link to the commit sha.
+ */
+ commitUrl: {
+ type: String,
+ required: false,
+ default: '',
},
- template: `
- <div class="branch-commit">
-
- <div v-if="hasCommitRef" class="icon-container">
- <i v-if="tag" class="fa fa-tag"></i>
- <i v-if="!tag" class="fa fa-code-fork"></i>
- </div>
-
- <a v-if="hasCommitRef"
- class="monospace branch-name"
- :href="commitRef.ref_url">
- {{commitRef.name}}
- </a>
-
- <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
-
- <a class="commit-id monospace"
- :href="commitUrl">
- {{shortSha}}
- </a>
-
- <p class="commit-title">
- <span v-if="title">
- <a v-if="hasAuthor"
- class="avatar-image-container"
- :href="author.web_url">
- <img
- class="avatar has-tooltip s20"
- :src="author.avatar_url"
- :alt="userImageAltDescription"
- :title="author.username" />
- </a>
-
- <a class="commit-row-message"
- :href="commitUrl">
- {{title}}
- </a>
- </span>
- <span v-else>
- Cant find HEAD commit for this branch
- </span>
- </p>
+ /**
+ * Used to show the commit short sha that links to the commit url.
+ */
+ shortSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided shows the commit tile.
+ */
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided renders information about the author of the commit.
+ * When provided should include:
+ * `avatar_url` to render the avatar icon
+ * `web_url` to link to user profile
+ * `username` to render alt and title tags
+ */
+ author: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+
+ computed: {
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * ref section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasCommitRef() {
+ return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
+ },
+
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * author section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasAuthor() {
+ return this.author &&
+ this.author.avatar_url &&
+ this.author.web_url &&
+ this.author.username;
+ },
+
+ /**
+ * If information about the author is provided will return a string
+ * to be rendered as the alt attribute of the img tag.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ return this.author &&
+ this.author.username ? `${this.author.username}'s avatar` : null;
+ },
+ },
+
+ data() {
+ return { commitIconSvg };
+ },
+
+ template: `
+ <div class="branch-commit">
+
+ <div v-if="hasCommitRef" class="icon-container">
+ <i v-if="tag" class="fa fa-tag"></i>
+ <i v-if="!tag" class="fa fa-code-fork"></i>
</div>
- `,
- });
-})();
+
+ <a v-if="hasCommitRef"
+ class="monospace branch-name"
+ :href="commitRef.ref_url">
+ {{commitRef.name}}
+ </a>
+
+ <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
+
+ <a class="commit-id monospace"
+ :href="commitUrl">
+ {{shortSha}}
+ </a>
+
+ <p class="commit-title">
+ <span v-if="title">
+ <a v-if="hasAuthor"
+ class="avatar-image-container"
+ :href="author.web_url">
+ <img
+ class="avatar has-tooltip s20"
+ :src="author.avatar_url"
+ :alt="userImageAltDescription"
+ :title="author.username" />
+ </a>
+
+ <a class="commit-row-message"
+ :href="commitUrl">
+ {{title}}
+ </a>
+ </span>
+ <span v-else>
+ Cant find HEAD commit for this branch
+ </span>
+ </p>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js b/app/assets/javascripts/vue_shared/components/pipelines_table.js
index 0d8f85db965..afd8d7acf6b 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js
@@ -1,52 +1,48 @@
-/* eslint-disable no-param-reassign */
-/* global Vue */
+import PipelinesTableRowComponent from './pipelines_table_row';
-require('./pipelines_table_row');
/**
* Pipelines Table Component.
*
* Given an array of objects, renders a table.
*/
-
-(() => {
- window.gl = window.gl || {};
- gl.pipelines = gl.pipelines || {};
-
- gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', {
-
- props: {
- pipelines: {
- type: Array,
- required: true,
- default: () => ([]),
- },
-
+export default {
+ props: {
+ pipelines: {
+ type: Array,
+ required: true,
+ default: () => ([]),
},
- components: {
- 'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent,
+ service: {
+ type: Object,
+ required: true,
},
+ },
+
+ components: {
+ 'pipelines-table-row-component': PipelinesTableRowComponent,
+ },
- template: `
- <table class="table ci-table">
- <thead>
- <tr>
- <th class="js-pipeline-status pipeline-status">Status</th>
- <th class="js-pipeline-info pipeline-info">Pipeline</th>
- <th class="js-pipeline-commit pipeline-commit">Commit</th>
- <th class="js-pipeline-stages pipeline-stages">Stages</th>
- <th class="js-pipeline-date pipeline-date"></th>
- <th class="js-pipeline-actions pipeline-actions"></th>
- </tr>
- </thead>
- <tbody>
- <template v-for="model in pipelines"
- v-bind:model="model">
- <tr is="pipelines-table-row-component"
- :pipeline="model"></tr>
- </template>
- </tbody>
- </table>
- `,
- });
-})();
+ template: `
+ <table class="table ci-table">
+ <thead>
+ <tr>
+ <th class="js-pipeline-status pipeline-status">Status</th>
+ <th class="js-pipeline-info pipeline-info">Pipeline</th>
+ <th class="js-pipeline-commit pipeline-commit">Commit</th>
+ <th class="js-pipeline-stages pipeline-stages">Stages</th>
+ <th class="js-pipeline-date pipeline-date"></th>
+ <th class="js-pipeline-actions pipeline-actions"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template v-for="model in pipelines"
+ v-bind:model="model">
+ <tr is="pipelines-table-row-component"
+ :pipeline="model"
+ :service="service"></tr>
+ </template>
+ </tbody>
+ </table>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
index e5e88186a85..f5b3cb9214e 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
@@ -1,199 +1,228 @@
/* eslint-disable no-param-reassign */
-/* global Vue */
-
-require('../../vue_pipelines_index/status');
-require('../../vue_pipelines_index/pipeline_url');
-require('../../vue_pipelines_index/stage');
-require('../../vue_pipelines_index/pipeline_actions');
-require('../../vue_pipelines_index/time_ago');
-require('./commit');
+
+import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button';
+import PipelinesActionsComponent from '../../vue_pipelines_index/components/pipelines_actions';
+import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts';
+import PipelinesStatusComponent from '../../vue_pipelines_index/components/status';
+import PipelinesStageComponent from '../../vue_pipelines_index/components/stage';
+import PipelinesUrlComponent from '../../vue_pipelines_index/components/pipeline_url';
+import PipelinesTimeagoComponent from '../../vue_pipelines_index/components/time_ago';
+import CommitComponent from './commit';
+
/**
* Pipeline table row.
*
* Given the received object renders a table row in the pipelines' table.
*/
-(() => {
- window.gl = window.gl || {};
- gl.pipelines = gl.pipelines || {};
-
- gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', {
-
- props: {
- pipeline: {
- type: Object,
- required: true,
- default: () => ({}),
- },
+export default {
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ components: {
+ 'async-button-component': AsyncButtonComponent,
+ 'pipelines-actions-component': PipelinesActionsComponent,
+ 'pipelines-artifacts-component': PipelinesArtifactsComponent,
+ 'commit-component': CommitComponent,
+ 'dropdown-stage': PipelinesStageComponent,
+ 'pipeline-url': PipelinesUrlComponent,
+ 'status-scope': PipelinesStatusComponent,
+ 'time-ago': PipelinesTimeagoComponent,
+ },
+
+ computed: {
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * This field needs a lot of verification, because of different possible cases:
+ *
+ * 1. person who is an author of a commit might be a GitLab user
+ * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
+ * 3. If GitLab user does not have avatar he/she might have a Gravatar
+ * 4. If committer is not a GitLab User he/she can have a Gravatar
+ * 5. We do not have consistent API object in this case
+ * 6. We should improve API and the code
+ *
+ * @returns {Object|Undefined}
+ */
+ commitAuthor() {
+ let commitAuthorInformation;
+
+ // 1. person who is an author of a commit might be a GitLab user
+ if (this.pipeline &&
+ this.pipeline.commit &&
+ this.pipeline.commit.author) {
+ // 2. if person who is an author of a commit is a GitLab user
+ // he/she can have a GitLab avatar
+ if (this.pipeline.commit.author.avatar_url) {
+ commitAuthorInformation = this.pipeline.commit.author;
+
+ // 3. If GitLab user does not have avatar he/she might have a Gravatar
+ } else if (this.pipeline.commit.author_gravatar_url) {
+ commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ });
+ }
+ }
+
+ // 4. If committer is not a GitLab User he/she can have a Gravatar
+ if (this.pipeline &&
+ this.pipeline.commit) {
+ commitAuthorInformation = {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ web_url: `mailto:${this.pipeline.commit.author_email}`,
+ username: this.pipeline.commit.author_name,
+ };
+ }
+
+ return commitAuthorInformation;
},
- components: {
- 'commit-component': gl.CommitComponent,
- 'pipeline-actions': gl.VuePipelineActions,
- 'dropdown-stage': gl.VueStage,
- 'pipeline-url': gl.VuePipelineUrl,
- 'status-scope': gl.VueStatusScope,
- 'time-ago': gl.VueTimeAgo,
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTag() {
+ if (this.pipeline.ref &&
+ this.pipeline.ref.tag) {
+ return this.pipeline.ref.tag;
+ }
+ return undefined;
},
- computed: {
- /**
- * If provided, returns the commit tag.
- * Needed to render the commit component column.
- *
- * This field needs a lot of verification, because of different possible cases:
- *
- * 1. person who is an author of a commit might be a GitLab user
- * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
- * 3. If GitLab user does not have avatar he/she might have a Gravatar
- * 4. If committer is not a GitLab User he/she can have a Gravatar
- * 5. We do not have consistent API object in this case
- * 6. We should improve API and the code
- *
- * @returns {Object|Undefined}
- */
- commitAuthor() {
- let commitAuthorInformation;
-
- // 1. person who is an author of a commit might be a GitLab user
- if (this.pipeline &&
- this.pipeline.commit &&
- this.pipeline.commit.author) {
- // 2. if person who is an author of a commit is a GitLab user
- // he/she can have a GitLab avatar
- if (this.pipeline.commit.author.avatar_url) {
- commitAuthorInformation = this.pipeline.commit.author;
-
- // 3. If GitLab user does not have avatar he/she might have a Gravatar
- } else if (this.pipeline.commit.author_gravatar_url) {
- commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
- avatar_url: this.pipeline.commit.author_gravatar_url,
- });
+ /**
+ * If provided, returns the commit ref.
+ * Needed to render the commit component column.
+ *
+ * Matches `path` prop sent in the API to `ref_url` prop needed
+ * in the commit component.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitRef() {
+ if (this.pipeline.ref) {
+ return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
+ if (prop === 'path') {
+ accumulator.ref_url = this.pipeline.ref[prop];
+ } else {
+ accumulator[prop] = this.pipeline.ref[prop];
}
- }
+ return accumulator;
+ }, {});
+ }
- // 4. If committer is not a GitLab User he/she can have a Gravatar
- if (this.pipeline &&
- this.pipeline.commit) {
- commitAuthorInformation = {
- avatar_url: this.pipeline.commit.author_gravatar_url,
- web_url: `mailto:${this.pipeline.commit.author_email}`,
- username: this.pipeline.commit.author_name,
- };
- }
+ return undefined;
+ },
- return commitAuthorInformation;
- },
-
- /**
- * If provided, returns the commit tag.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitTag() {
- if (this.pipeline.ref &&
- this.pipeline.ref.tag) {
- return this.pipeline.ref.tag;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit ref.
- * Needed to render the commit component column.
- *
- * Matches `path` prop sent in the API to `ref_url` prop needed
- * in the commit component.
- *
- * @returns {Object|Undefined}
- */
- commitRef() {
- if (this.pipeline.ref) {
- return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
- if (prop === 'path') {
- accumulator.ref_url = this.pipeline.ref[prop];
- } else {
- accumulator[prop] = this.pipeline.ref[prop];
- }
- return accumulator;
- }, {});
- }
+ /**
+ * If provided, returns the commit url.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitUrl() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.commit_path) {
+ return this.pipeline.commit.commit_path;
+ }
+ return undefined;
+ },
- return undefined;
- },
-
- /**
- * If provided, returns the commit url.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitUrl() {
- if (this.pipeline.commit &&
- this.pipeline.commit.commit_path) {
- return this.pipeline.commit.commit_path;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit short sha.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitShortSha() {
- if (this.pipeline.commit &&
- this.pipeline.commit.short_id) {
- return this.pipeline.commit.short_id;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit title.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitTitle() {
- if (this.pipeline.commit &&
- this.pipeline.commit.title) {
- return this.pipeline.commit.title;
- }
- return undefined;
- },
+ /**
+ * If provided, returns the commit short sha.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitShortSha() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.short_id) {
+ return this.pipeline.commit.short_id;
+ }
+ return undefined;
},
- template: `
- <tr class="commit">
- <status-scope :pipeline="pipeline"/>
-
- <pipeline-url :pipeline="pipeline"></pipeline-url>
-
- <td>
- <commit-component
- :tag="commitTag"
- :commit-ref="commitRef"
- :commit-url="commitUrl"
- :short-sha="commitShortSha"
- :title="commitTitle"
- :author="commitAuthor"/>
- </td>
-
- <td class="stage-cell">
- <div class="stage-container dropdown js-mini-pipeline-graph"
- v-if="pipeline.details.stages.length > 0"
- v-for="stage in pipeline.details.stages">
- <dropdown-stage :stage="stage"/>
- </div>
- </td>
-
- <time-ago :pipeline="pipeline"/>
-
- <pipeline-actions :pipeline="pipeline" />
- </tr>
- `,
- });
-})();
+ /**
+ * If provided, returns the commit title.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTitle() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.title) {
+ return this.pipeline.commit.title;
+ }
+ return undefined;
+ },
+ },
+
+ template: `
+ <tr class="commit">
+ <status-scope :pipeline="pipeline"/>
+
+ <pipeline-url :pipeline="pipeline"></pipeline-url>
+
+ <td>
+ <commit-component
+ :tag="commitTag"
+ :commit-ref="commitRef"
+ :commit-url="commitUrl"
+ :short-sha="commitShortSha"
+ :title="commitTitle"
+ :author="commitAuthor"/>
+ </td>
+
+ <td class="stage-cell">
+ <div class="stage-container dropdown js-mini-pipeline-graph"
+ v-if="pipeline.details.stages.length > 0"
+ v-for="stage in pipeline.details.stages">
+ <dropdown-stage :stage="stage"/>
+ </div>
+ </td>
+
+ <time-ago :pipeline="pipeline"/>
+
+ <td class="pipeline-actions">
+ <div class="pull-right btn-group">
+ <pipelines-actions-component
+ v-if="pipeline.details.manual_actions.length"
+ :actions="pipeline.details.manual_actions"
+ :service="service" />
+
+ <pipelines-artifacts-component
+ v-if="pipeline.details.artifacts.length"
+ :artifacts="pipeline.details.artifacts" />
+
+ <async-button-component
+ v-if="pipeline.flags.retryable"
+ :service="service"
+ :endpoint="pipeline.retry_path"
+ css-class="js-pipelines-retry-button btn-default btn-retry"
+ title="Retry"
+ icon="repeat" />
+
+ <async-button-component
+ v-if="pipeline.flags.cancelable"
+ :service="service"
+ :endpoint="pipeline.cancel_path"
+ css-class="js-pipelines-cancel-button btn-remove"
+ title="Cancel"
+ icon="remove"
+ confirm-action-message="Are you sure you want to cancel this pipeline?" />
+ </div>
+ </td>
+ </tr>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js b/app/assets/javascripts/vue_shared/components/table_pagination.js
index 8943b850a72..ebb14912b00 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.js
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.js
@@ -1,147 +1,135 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign, no-plusplus */
-
-window.Vue = require('vue');
-
-((gl) => {
- const PAGINATION_UI_BUTTON_LIMIT = 4;
- const UI_LIMIT = 6;
- const SPREAD = '...';
- const PREV = 'Prev';
- const NEXT = 'Next';
- const FIRST = '<< First';
- const LAST = 'Last >>';
-
- gl.VueGlPagination = Vue.extend({
- props: {
-
- // TODO: Consider refactoring in light of turbolinks removal.
-
- /**
- This function will take the information given by the pagination component
-
- Here is an example `change` method:
-
- change(pagenum) {
- gl.utils.visitUrl(`?page=${pagenum}`);
- },
- */
-
- change: {
- type: Function,
- required: true,
+const PAGINATION_UI_BUTTON_LIMIT = 4;
+const UI_LIMIT = 6;
+const SPREAD = '...';
+const PREV = 'Prev';
+const NEXT = 'Next';
+const FIRST = '« First';
+const LAST = 'Last »';
+
+export default {
+ props: {
+ /**
+ This function will take the information given by the pagination component
+
+ Here is an example `change` method:
+
+ change(pagenum) {
+ gl.utils.visitUrl(`?page=${pagenum}`);
},
+ */
+ change: {
+ type: Function,
+ required: true,
+ },
- /**
- pageInfo will come from the headers of the API call
- in the `.then` clause of the VueResource API call
- there should be a function that contructs the pageInfo for this component
-
- This is an example:
-
- const pageInfo = headers => ({
- perPage: +headers['X-Per-Page'],
- page: +headers['X-Page'],
- total: +headers['X-Total'],
- totalPages: +headers['X-Total-Pages'],
- nextPage: +headers['X-Next-Page'],
- previousPage: +headers['X-Prev-Page'],
- });
- */
-
- pageInfo: {
- type: Object,
- required: true,
- },
+ /**
+ pageInfo will come from the headers of the API call
+ in the `.then` clause of the VueResource API call
+ there should be a function that contructs the pageInfo for this component
+
+ This is an example:
+
+ const pageInfo = headers => ({
+ perPage: +headers['X-Per-Page'],
+ page: +headers['X-Page'],
+ total: +headers['X-Total'],
+ totalPages: +headers['X-Total-Pages'],
+ nextPage: +headers['X-Next-Page'],
+ previousPage: +headers['X-Prev-Page'],
+ });
+ */
+ pageInfo: {
+ type: Object,
+ required: true,
},
- methods: {
- changePage(e) {
- const text = e.target.innerText;
- const { totalPages, nextPage, previousPage } = this.pageInfo;
-
- switch (text) {
- case SPREAD:
- break;
- case LAST:
- this.change(totalPages);
- break;
- case NEXT:
- this.change(nextPage);
- break;
- case PREV:
- this.change(previousPage);
- break;
- case FIRST:
- this.change(1);
- break;
- default:
- this.change(+text);
- break;
- }
- },
+ },
+ methods: {
+ changePage(e) {
+ const text = e.target.innerText;
+ const { totalPages, nextPage, previousPage } = this.pageInfo;
+
+ switch (text) {
+ case SPREAD:
+ break;
+ case LAST:
+ this.change(totalPages);
+ break;
+ case NEXT:
+ this.change(nextPage);
+ break;
+ case PREV:
+ this.change(previousPage);
+ break;
+ case FIRST:
+ this.change(1);
+ break;
+ default:
+ this.change(+text);
+ break;
+ }
},
- computed: {
- prev() {
- return this.pageInfo.previousPage;
- },
- next() {
- return this.pageInfo.nextPage;
- },
- getItems() {
- const total = this.pageInfo.totalPages;
- const page = this.pageInfo.page;
- const items = [];
+ },
+ computed: {
+ prev() {
+ return this.pageInfo.previousPage;
+ },
+ next() {
+ return this.pageInfo.nextPage;
+ },
+ getItems() {
+ const total = this.pageInfo.totalPages;
+ const page = this.pageInfo.page;
+ const items = [];
- if (page > 1) items.push({ title: FIRST });
+ if (page > 1) items.push({ title: FIRST });
- if (page > 1) {
- items.push({ title: PREV, prev: true });
- } else {
- items.push({ title: PREV, disabled: true, prev: true });
- }
+ if (page > 1) {
+ items.push({ title: PREV, prev: true });
+ } else {
+ items.push({ title: PREV, disabled: true, prev: true });
+ }
- if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
+ if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
- const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
- const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
+ const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
+ const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
- for (let i = start; i <= end; i++) {
- const isActive = i === page;
- items.push({ title: i, active: isActive, page: true });
- }
+ for (let i = start; i <= end; i += 1) {
+ const isActive = i === page;
+ items.push({ title: i, active: isActive, page: true });
+ }
- if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
- items.push({ title: SPREAD, separator: true, page: true });
- }
+ if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
+ items.push({ title: SPREAD, separator: true, page: true });
+ }
- if (page === total) {
- items.push({ title: NEXT, disabled: true, next: true });
- } else if (total - page >= 1) {
- items.push({ title: NEXT, next: true });
- }
+ if (page === total) {
+ items.push({ title: NEXT, disabled: true, next: true });
+ } else if (total - page >= 1) {
+ items.push({ title: NEXT, next: true });
+ }
- if (total - page >= 1) items.push({ title: LAST, last: true });
+ if (total - page >= 1) items.push({ title: LAST, last: true });
- return items;
- },
+ return items;
},
- template: `
- <div class="gl-pagination">
- <ul class="pagination clearfix">
- <li v-for='item in getItems'
- :class='{
- page: item.page,
- prev: item.prev,
- next: item.next,
- separator: item.separator,
- active: item.active,
- disabled: item.disabled
- }'
- >
- <a @click="changePage($event)">{{item.title}}</a>
- </li>
- </ul>
- </div>
- `,
- });
-})(window.gl || (window.gl = {}));
+ },
+ template: `
+ <div class="gl-pagination">
+ <ul class="pagination clearfix">
+ <li v-for='item in getItems'
+ :class='{
+ page: item.page,
+ prev: item.prev,
+ next: item.next,
+ separator: item.separator,
+ active: item.active,
+ disabled: item.disabled
+ }'
+ >
+ <a @click="changePage($event)">{{item.title}}</a>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
index 4157fefddc9..d5f87588c28 100644
--- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
@@ -1,18 +1,23 @@
-/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars,
-no-param-reassign, no-plusplus */
-/* global Vue */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+Vue.use(VueResource);
+
+// Maintain a global counter for active requests
+// see: spec/support/wait_for_vue_resource.rb
Vue.http.interceptors.push((request, next) => {
- Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
+ window.activeVueResources = window.activeVueResources || 0;
+ window.activeVueResources += 1;
- next((response) => {
- Vue.activeResources--;
+ next(() => {
+ window.activeVueResources -= 1;
});
});
+// Inject CSRF token so we don't break any tests.
Vue.http.interceptors.push((request, next) => {
- // needed in order to not break the tests.
if ($.rails) {
+ // eslint-disable-next-line no-param-reassign
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
}
next();