diff options
author | Phil Hughes <me@iamphill.com> | 2017-04-20 16:15:39 +0300 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2017-04-20 16:15:39 +0300 |
commit | b7b5bd4a49a212f41f08be98997c25c0ce530f97 (patch) | |
tree | 72e7b32423adf742a9d8dcbfe2608fec86be0378 /app/assets | |
parent | 7d16537cac7eb808d8d18cd0b89475db4e4eeaaa (diff) | |
parent | 7ceb0efc6c6016e055ec6862759ff5f442551c4a (diff) |
Merge remote-tracking branch 'origin/28433-internationalise-cycle-analytics-page' into js-translations
Diffstat (limited to 'app/assets')
85 files changed, 2159 insertions, 1905 deletions
diff --git a/app/assets/images/ci_favicons/icon_status_canceled.ico b/app/assets/images/ci_favicons/favicon_status_canceled.ico Binary files differindex 5a19458f2a2..5a19458f2a2 100755 --- a/app/assets/images/ci_favicons/icon_status_canceled.ico +++ b/app/assets/images/ci_favicons/favicon_status_canceled.ico diff --git a/app/assets/images/ci_favicons/icon_status_created.ico b/app/assets/images/ci_favicons/favicon_status_created.ico Binary files differindex 4dca9640cb3..4dca9640cb3 100755 --- a/app/assets/images/ci_favicons/icon_status_created.ico +++ b/app/assets/images/ci_favicons/favicon_status_created.ico diff --git a/app/assets/images/ci_favicons/icon_status_failed.ico b/app/assets/images/ci_favicons/favicon_status_failed.ico Binary files differindex c961ff9a69b..c961ff9a69b 100755 --- a/app/assets/images/ci_favicons/icon_status_failed.ico +++ b/app/assets/images/ci_favicons/favicon_status_failed.ico diff --git a/app/assets/images/ci_favicons/icon_status_manual.ico b/app/assets/images/ci_favicons/favicon_status_manual.ico Binary files differindex 5fbbc99ea7c..5fbbc99ea7c 100755 --- a/app/assets/images/ci_favicons/icon_status_manual.ico +++ b/app/assets/images/ci_favicons/favicon_status_manual.ico diff --git a/app/assets/images/ci_favicons/icon_status_not_found.ico b/app/assets/images/ci_favicons/favicon_status_not_found.ico Binary files differindex 21afa9c72e6..21afa9c72e6 100755 --- a/app/assets/images/ci_favicons/icon_status_not_found.ico +++ b/app/assets/images/ci_favicons/favicon_status_not_found.ico diff --git a/app/assets/images/ci_favicons/icon_status_pending.ico b/app/assets/images/ci_favicons/favicon_status_pending.ico Binary files differindex 8be32dab85a..8be32dab85a 100755 --- a/app/assets/images/ci_favicons/icon_status_pending.ico +++ b/app/assets/images/ci_favicons/favicon_status_pending.ico diff --git a/app/assets/images/ci_favicons/icon_status_running.ico b/app/assets/images/ci_favicons/favicon_status_running.ico Binary files differindex f328ff1a5ed..f328ff1a5ed 100755 --- a/app/assets/images/ci_favicons/icon_status_running.ico +++ b/app/assets/images/ci_favicons/favicon_status_running.ico diff --git a/app/assets/images/ci_favicons/icon_status_skipped.ico b/app/assets/images/ci_favicons/favicon_status_skipped.ico Binary files differindex b4394e1b4af..b4394e1b4af 100755 --- a/app/assets/images/ci_favicons/icon_status_skipped.ico +++ b/app/assets/images/ci_favicons/favicon_status_skipped.ico diff --git a/app/assets/images/ci_favicons/icon_status_success.ico b/app/assets/images/ci_favicons/favicon_status_success.ico Binary files differindex 4f436c95242..4f436c95242 100755 --- a/app/assets/images/ci_favicons/icon_status_success.ico +++ b/app/assets/images/ci_favicons/favicon_status_success.ico diff --git a/app/assets/images/ci_favicons/icon_status_warning.ico b/app/assets/images/ci_favicons/favicon_status_warning.ico Binary files differindex 805cc20cdec..805cc20cdec 100755 --- a/app/assets/images/ci_favicons/icon_status_warning.ico +++ b/app/assets/images/ci_favicons/favicon_status_warning.ico diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index ce426741637..f93208944a1 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,3 +1,5 @@ +/* global Flash */ + import Cookies from 'js-cookie'; import emojiMap from 'emojis/digests.json'; @@ -6,6 +8,7 @@ import { glEmojiTag } from './behaviors/gl_emoji'; import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; +const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; const requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || @@ -103,8 +106,9 @@ function AwardsHandler() { const $glEmojiElement = $target.find('gl-emoji'); const $spriteIconElement = $target.find('.icon'); const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name'); + $target.closest('.js-awards-block').addClass('current'); - return this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji); + this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji); }); } @@ -124,16 +128,18 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) { } const $menu = $('.emoji-menu'); + const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent(); + const $userAuthored = this.isUserAuthored($addBtn); if ($menu.length) { if ($menu.is('.is-visible')) { $addBtn.removeClass('is-active'); $menu.removeClass('is-visible'); - $('#emoji_search').blur(); + $('.js-emoji-menu-search').blur(); } else { $addBtn.addClass('is-active'); this.positionMenu($menu, $addBtn); $menu.addClass('is-visible'); - $('#emoji_search').focus(); + $('.js-emoji-menu-search').focus(); } } else { $addBtn.addClass('is-loading is-active'); @@ -143,10 +149,12 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) { this.positionMenu($createdMenu, $addBtn); return setTimeout(() => { $createdMenu.addClass('is-visible'); - $('#emoji_search').focus(); + $('.js-emoji-menu-search').focus(); }, 200); }); } + + $thumbsBtn.toggleClass('disabled', $userAuthored); }; // Create the emoji menu with the first category of emojis. @@ -174,7 +182,7 @@ AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) { const emojiMenuMarkup = ` <div class="emoji-menu"> - <input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" placeholder="Search emoji" /> + <input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" /> <div class="emoji-menu-content"> ${frequentlyUsedCatgegory} @@ -259,7 +267,8 @@ AwardsHandler.prototype.addAward = function addAward( callback, ) { const normalizedEmoji = this.normalizeEmojiName(emoji); - this.postEmoji(awardUrl, normalizedEmoji, () => { + const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); + this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => { this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); return typeof callback === 'function' ? callback() : undefined; }); @@ -324,6 +333,10 @@ AwardsHandler.prototype.isActive = function isActive($emojiButton) { return $emojiButton.hasClass('active'); }; +AwardsHandler.prototype.isUserAuthored = function isUserAuthored($button) { + return $button.hasClass('js-user-authored'); +}; + AwardsHandler.prototype.decrementCounter = function decrementCounter($emojiButton, emoji) { const counter = $('.js-counter', $emojiButton); const counterNumber = parseInt(counter.text(), 10); @@ -428,20 +441,35 @@ AwardsHandler.prototype.createEmoji = function createEmoji(votesBlock, emoji) { }); }; -AwardsHandler.prototype.postEmoji = function postEmoji(awardUrl, emoji, callback) { - return $.post(awardUrl, { - name: emoji, - }, (data) => { - if (data.ok) { - callback(); - } - }); +AwardsHandler.prototype.postEmoji = function postEmoji($emojiButton, awardUrl, emoji, callback) { + if (this.isUserAuthored($emojiButton)) { + this.userAuthored($emojiButton); + } else { + $.post(awardUrl, { + name: emoji, + }, (data) => { + if (data.ok) { + callback(); + } + }).fail(() => new Flash('Something went wrong on our end.')); + } }; AwardsHandler.prototype.findEmojiIcon = function findEmojiIcon(votesBlock, emoji) { return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`); }; +AwardsHandler.prototype.userAuthored = function userAuthored($emojiButton) { + const oldTitle = this.getAwardTooltip($emojiButton); + const newTitle = 'You cannot vote on your own issue, MR and note'; + gl.utils.updateTooltipTitle($emojiButton, newTitle).tooltip('show'); + // Restore tooltip back to award list + return setTimeout(() => { + $emojiButton.tooltip('hide'); + gl.utils.updateTooltipTitle($emojiButton, oldTitle); + }, 2800); +}; + AwardsHandler.prototype.scrollToAwards = function scrollToAwards() { const options = { scrollTop: $('.awards').offset().top - 110, @@ -474,24 +502,41 @@ AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmoj }; AwardsHandler.prototype.setupSearch = function setupSearch() { - this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => { + const $search = $('.js-emoji-menu-search'); + + this.registerEventListener('on', $search, 'input', (e) => { const term = $(e.target).val().trim(); - // Clean previous search results - $('ul.emoji-menu-search, h5.emoji-search-title').remove(); - if (term.length > 0) { - // Generate a search result block - const h5 = $('<h5 class="emoji-search-title"/>').text('Search results'); - const foundEmojis = this.searchEmojis(term).show(); - const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis); - $('.emoji-menu-content ul, .emoji-menu-content h5').hide(); - $('.emoji-menu-content').append(h5).append(ul); - } else { - $('.emoji-menu-content').children().show(); + this.searchEmojis(term); + }); + + const $menu = $('.emoji-menu'); + this.registerEventListener('on', $menu, transitionEndEventString, (e) => { + if (e.target === e.currentTarget) { + // Clear the search + this.searchEmojis(''); } }); }; AwardsHandler.prototype.searchEmojis = function searchEmojis(term) { + const $search = $('.js-emoji-menu-search'); + $search.val(term); + + // Clean previous search results + $('ul.emoji-menu-search, h5.emoji-search-title').remove(); + if (term.length > 0) { + // Generate a search result block + const h5 = $('<h5 class="emoji-search-title"/>').text('Search results'); + const foundEmojis = this.findMatchingEmojiElements(term).show(); + const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis); + $('.emoji-menu-content ul, .emoji-menu-content h5').hide(); + $('.emoji-menu-content').append(h5).append(ul); + } else { + $('.emoji-menu-content').children().show(); + } +}; + +AwardsHandler.prototype.findMatchingEmojiElements = function findMatchingEmojiElements(term) { const safeTerm = term.toLowerCase(); const namesMatchingAlias = []; diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 4c9ad128e6c..77e92ff8caf 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -22,6 +22,7 @@ $(() => { } $('body').on('click', '.js-toggle-button', function toggleButton(e) { + e.target.classList.toggle('open'); toggleContainer($(this).closest('.js-toggle-container')); const targetTag = e.currentTarget.tagName.toLowerCase(); diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index c9fe23aec75..4568b86f298 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -35,7 +35,7 @@ export default class BlobFileDropzone { this.removeFile(file); }); this.on('sending', function (file, xhr, formData) { - formData.append('target_branch', form.find('input[name="target_branch"]').val()); + formData.append('branch_name', form.find('input[name="branch_name"]').val()); formData.append('create_merge_request', form.find('.js-create-merge-request').val()); formData.append('commit_message', form.find('.js-commit-message').val()); }); diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 91e5fb2a666..f2b79a88a4a 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -3,6 +3,8 @@ /* global ListLabel */ import queryData from '../utils/query_data'; +const PER_PAGE = 20; + class List { constructor (obj) { this.id = obj.id; @@ -58,7 +60,9 @@ class List { nextPage () { if (this.issuesSize > this.issues.length) { - this.page += 1; + if (this.issues.length / PER_PAGE >= 1) { + this.page += 1; + } return this.getIssues(false); } @@ -145,10 +149,7 @@ class List { } updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) { - gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid) - .then(() => { - listFrom.getIssues(false); - }); + gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid); } findIssue (id) { diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 0aad95c2fe3..97f279e4be4 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -2,6 +2,8 @@ consistent-return, prefer-rest-params */ /* global Breakpoints */ +import { bytesToKiB } from './lib/utils/number_utils'; + const bind = function (fn, me) { return function () { return fn.apply(me, arguments); }; }; const AUTO_SCROLL_OFFSET = 75; const DOWN_BUILD_TRACE = '#down-build-trace'; @@ -20,6 +22,7 @@ window.Build = (function () { this.state = this.options.logState; this.buildStage = this.options.buildStage; this.$document = $(document); + this.logBytes = 0; this.updateDropdown = bind(this.updateDropdown, this); @@ -98,15 +101,22 @@ window.Build = (function () { if (log.append) { $buildContainer.append(log.html); + this.logBytes += log.size; } else { $buildContainer.html(log.html); - if (log.truncated) { - $('.js-truncated-info-size').html(` ${log.size} `); - this.$truncatedInfo.removeClass('hidden'); - this.initAffixTruncatedInfo(); - } else { - this.$truncatedInfo.addClass('hidden'); - } + this.logBytes = log.size; + } + + // if the incremental sum of logBytes we received is less than the total + // we need to show a message warning the user about that. + if (this.logBytes < log.total) { + // size is in bytes, we need to calculate KiB + const size = bytesToKiB(this.logBytes); + $('.js-truncated-info-size').html(`${size}`); + this.$truncatedInfo.removeClass('hidden'); + this.initAffixTruncatedInfo(); + } else { + this.$truncatedInfo.addClass('hidden'); } this.checkAutoscroll(); diff --git a/app/assets/javascripts/ci_status_icons.js b/app/assets/javascripts/ci_status_icons.js new file mode 100644 index 00000000000..f16616873b2 --- /dev/null +++ b/app/assets/javascripts/ci_status_icons.js @@ -0,0 +1,34 @@ +import CANCELED_SVG from 'icons/_icon_status_canceled_borderless.svg'; +import CREATED_SVG from 'icons/_icon_status_created_borderless.svg'; +import FAILED_SVG from 'icons/_icon_status_failed_borderless.svg'; +import MANUAL_SVG from 'icons/_icon_status_manual_borderless.svg'; +import PENDING_SVG from 'icons/_icon_status_pending_borderless.svg'; +import RUNNING_SVG from 'icons/_icon_status_running_borderless.svg'; +import SKIPPED_SVG from 'icons/_icon_status_skipped_borderless.svg'; +import SUCCESS_SVG from 'icons/_icon_status_success_borderless.svg'; +import WARNING_SVG from 'icons/_icon_status_warning_borderless.svg'; + +const StatusIconEntityMap = { + icon_status_canceled: CANCELED_SVG, + icon_status_created: CREATED_SVG, + icon_status_failed: FAILED_SVG, + icon_status_manual: MANUAL_SVG, + icon_status_pending: PENDING_SVG, + icon_status_running: RUNNING_SVG, + icon_status_skipped: SKIPPED_SVG, + icon_status_success: SUCCESS_SVG, + icon_status_warning: WARNING_SVG, +}; + +export { + CANCELED_SVG, + CREATED_SVG, + FAILED_SVG, + MANUAL_SVG, + PENDING_SVG, + RUNNING_SVG, + SKIPPED_SVG, + SUCCESS_SVG, + WARNING_SVG, + StatusIconEntityMap as default, +}; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js index 1d16c64e07e..7438faeadf4 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js @@ -1,11 +1,11 @@ import Vue from 'vue'; import Visibility from 'visibilityjs'; 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.vue'; -import ErrorState from '../../vue_pipelines_index/components/error_state.vue'; +import PipelinesService from '../../pipelines/services/pipelines_service'; +import PipelineStore from '../../pipelines/stores/pipelines_store'; +import eventHub from '../../pipelines/event_hub'; +import EmptyState from '../../pipelines/components/empty_state.vue'; +import ErrorState from '../../pipelines/components/error_state.vue'; import '../../lib/utils/common_utils'; import '../../vue_shared/vue_resource_interceptor'; import Poll from '../../lib/utils/poll'; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 95257e58e6b..c8e53cb554e 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -128,7 +128,7 @@ $(() => { }, dismissOverviewDialog() { this.isOverviewDialogDismissed = true; - Cookies.set(OVERVIEW_DIALOG_COOKIE, '1'); + Cookies.set(OVERVIEW_DIALOG_COOKIE, '1', { expires: 365 }); }, }, }); 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 eb76b7d15fd..aed7cac4e62 100644 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js @@ -3,65 +3,63 @@ import Vue from 'vue'; -(() => { - const CommentAndResolveBtn = Vue.extend({ - props: { - discussionId: String, +const CommentAndResolveBtn = Vue.extend({ + props: { + discussionId: String, + }, + data() { + return { + textareaIsEmpty: true, + discussion: {}, + }; + }, + computed: { + showButton: function () { + if (this.discussion) { + return this.discussion.isResolvable(); + } else { + return false; + } }, - data() { - return { - textareaIsEmpty: true, - discussion: {}, - }; + isDiscussionResolved: function () { + return this.discussion.isResolved(); }, - computed: { - showButton: function () { - if (this.discussion) { - return this.discussion.isResolvable(); + buttonText: function () { + if (this.isDiscussionResolved) { + if (this.textareaIsEmpty) { + return "Unresolve discussion"; } else { - return false; + return "Comment & unresolve discussion"; } - }, - isDiscussionResolved: function () { - return this.discussion.isResolved(); - }, - buttonText: function () { - if (this.isDiscussionResolved) { - if (this.textareaIsEmpty) { - return "Unresolve discussion"; - } else { - return "Comment & unresolve discussion"; - } + } else { + if (this.textareaIsEmpty) { + return "Resolve discussion"; } else { - if (this.textareaIsEmpty) { - return "Resolve discussion"; - } else { - return "Comment & resolve discussion"; - } + return "Comment & resolve discussion"; } } - }, - created() { - if (this.discussionId) { - this.discussion = CommentsStore.state[this.discussionId]; - } - }, - mounted: function () { - if (!this.discussionId) return; + } + }, + created() { + if (this.discussionId) { + this.discussion = CommentsStore.state[this.discussionId]; + } + }, + mounted: function () { + if (!this.discussionId) return; - const $textarea = $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`); - this.textareaIsEmpty = $textarea.val() === ''; + const $textarea = $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`); + this.textareaIsEmpty = $textarea.val() === ''; - $textarea.on('input.comment-and-resolve-btn', () => { - this.textareaIsEmpty = $textarea.val() === ''; - }); - }, - destroyed: function () { - if (!this.discussionId) return; + $textarea.on('input.comment-and-resolve-btn', () => { + this.textareaIsEmpty = $textarea.val() === ''; + }); + }, + destroyed: function () { + if (!this.discussionId) return; - $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off('input.comment-and-resolve-btn'); - } - }); + $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off('input.comment-and-resolve-btn'); + } +}); - Vue.component('comment-and-resolve-btn', CommentAndResolveBtn); -})(window); +Vue.component('comment-and-resolve-btn', CommentAndResolveBtn); 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 0297add94d5..f3a688fbf2f 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -4,155 +4,153 @@ import Vue from 'vue'; import collapseIcon from '../icons/collapse_icon.svg'; -(() => { - const DiffNoteAvatars = Vue.extend({ - props: ['discussionId'], - data() { - return { - isVisible: false, - lineType: '', - storeState: CommentsStore.state, - shownAvatars: 3, - collapseIcon, - }; - }, - template: ` - <div class="diff-comment-avatar-holders" - v-show="notesCount !== 0"> - <div v-if="!isVisible"> - <img v-for="note in notesSubset" - class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar" - width="19" - height="19" - role="button" - data-container="body" - data-placement="top" - data-html="true" - :data-line-type="lineType" - :title="note.authorName + ': ' + note.noteTruncated" - :src="note.authorAvatar" - @click="clickedAvatar($event)" /> - <span v-if="notesCount > shownAvatars" - class="diff-comments-more-count has-tooltip js-diff-comment-avatar" - data-container="body" - data-placement="top" - ref="extraComments" - role="button" - :data-line-type="lineType" - :title="extraNotesTitle" - @click="clickedAvatar($event)">{{ moreText }}</span> - </div> - <button class="diff-notes-collapse js-diff-comment-avatar" - type="button" - aria-label="Show comments" +const DiffNoteAvatars = Vue.extend({ + props: ['discussionId'], + data() { + return { + isVisible: false, + lineType: '', + storeState: CommentsStore.state, + shownAvatars: 3, + collapseIcon, + }; + }, + template: ` + <div class="diff-comment-avatar-holders" + v-show="notesCount !== 0"> + <div v-if="!isVisible"> + <img v-for="note in notesSubset" + class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar" + width="19" + height="19" + role="button" + data-container="body" + data-placement="top" + data-html="true" + :data-line-type="lineType" + :title="note.authorName + ': ' + note.noteTruncated" + :src="note.authorAvatar" + @click="clickedAvatar($event)" /> + <span v-if="notesCount > shownAvatars" + class="diff-comments-more-count has-tooltip js-diff-comment-avatar" + data-container="body" + data-placement="top" + ref="extraComments" + role="button" :data-line-type="lineType" - @click="clickedAvatar($event)" - v-if="isVisible" - v-html="collapseIcon"> - </button> + :title="extraNotesTitle" + @click="clickedAvatar($event)">{{ moreText }}</span> </div> - `, - mounted() { + <button class="diff-notes-collapse js-diff-comment-avatar" + type="button" + aria-label="Show comments" + :data-line-type="lineType" + @click="clickedAvatar($event)" + v-if="isVisible" + v-html="collapseIcon"> + </button> + </div> + `, + mounted() { + this.$nextTick(() => { + this.addNoCommentClass(); + this.setDiscussionVisible(); + + this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new'; + }); + + $(document).on('toggle.comments', () => { this.$nextTick(() => { - this.addNoCommentClass(); this.setDiscussionVisible(); - - this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new'; }); - - $(document).on('toggle.comments', () => { + }); + }, + destroyed() { + $(document).off('toggle.comments'); + }, + watch: { + storeState: { + handler() { this.$nextTick(() => { - this.setDiscussionVisible(); + $('.has-tooltip', this.$el).tooltip('fixTitle'); + + // We need to add/remove a class to an element that is outside the Vue instance + this.addNoCommentClass(); }); - }); - }, - destroyed() { - $(document).off('toggle.comments'); - }, - watch: { - storeState: { - handler() { - this.$nextTick(() => { - $('.has-tooltip', this.$el).tooltip('fixTitle'); - - // We need to add/remove a class to an element that is outside the Vue instance - this.addNoCommentClass(); - }); - }, - deep: true, }, + deep: true, }, - computed: { - notesSubset() { - let notes = []; - - if (this.discussion) { - notes = Object.keys(this.discussion.notes) - .slice(0, this.shownAvatars) - .map(noteId => this.discussion.notes[noteId]); - } - - return notes; - }, - extraNotesTitle() { - if (this.discussion) { - const extra = this.discussion.notesCount() - this.shownAvatars; + }, + computed: { + notesSubset() { + let notes = []; + + if (this.discussion) { + notes = Object.keys(this.discussion.notes) + .slice(0, this.shownAvatars) + .map(noteId => this.discussion.notes[noteId]); + } + + return notes; + }, + extraNotesTitle() { + if (this.discussion) { + const extra = this.discussion.notesCount() - this.shownAvatars; - return `${extra} more comment${extra > 1 ? 's' : ''}`; - } + return `${extra} more comment${extra > 1 ? 's' : ''}`; + } - return ''; - }, - discussion() { - return this.storeState[this.discussionId]; - }, - notesCount() { - if (this.discussion) { - return this.discussion.notesCount(); - } + return ''; + }, + discussion() { + return this.storeState[this.discussionId]; + }, + notesCount() { + if (this.discussion) { + return this.discussion.notesCount(); + } - return 0; - }, - moreText() { - const plusSign = this.notesCount < 100 ? '+' : ''; + return 0; + }, + moreText() { + const plusSign = this.notesCount < 100 ? '+' : ''; - return `${plusSign}${this.notesCount - this.shownAvatars}`; - }, + return `${plusSign}${this.notesCount - this.shownAvatars}`; }, - methods: { - clickedAvatar(e) { - notes.addDiffNote(e); + }, + methods: { + clickedAvatar(e) { + notes.addDiffNote(e); - // Toggle the active state of the toggle all button - this.toggleDiscussionsToggleState(); + // Toggle the active state of the toggle all button + this.toggleDiscussionsToggleState(); - this.$nextTick(() => { - this.setDiscussionVisible(); + this.$nextTick(() => { + this.setDiscussionVisible(); - $('.has-tooltip', this.$el).tooltip('fixTitle'); - $('.has-tooltip', this.$el).tooltip('hide'); - }); - }, - addNoCommentClass() { - const notesCount = this.notesCount; + $('.has-tooltip', this.$el).tooltip('fixTitle'); + $('.has-tooltip', this.$el).tooltip('hide'); + }); + }, + addNoCommentClass() { + const notesCount = this.notesCount; - $(this.$el).closest('.js-avatar-container') - .toggleClass('js-no-comment-btn', notesCount > 0) - .nextUntil('.js-avatar-container') - .toggleClass('js-no-comment-btn', notesCount > 0); - }, - toggleDiscussionsToggleState() { - const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); - const $visibleNotesHolders = $notesHolders.filter(':visible'); - const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments'); + $(this.$el).closest('.js-avatar-container') + .toggleClass('js-no-comment-btn', notesCount > 0) + .nextUntil('.js-avatar-container') + .toggleClass('js-no-comment-btn', notesCount > 0); + }, + toggleDiscussionsToggleState() { + const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); + const $visibleNotesHolders = $notesHolders.filter(':visible'); + const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments'); - $toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length); - }, - setDiscussionVisible() { - this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible'); - }, + $toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length); + }, + setDiscussionVisible() { + this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible'); }, - }); + }, +}); - Vue.component('diff-note-avatars', DiffNoteAvatars); -})(); +Vue.component('diff-note-avatars', DiffNoteAvatars); 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 8edc45130fc..8a0fd3bb4a7 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js @@ -4,192 +4,190 @@ import Vue from 'vue'; -(() => { - const JumpToDiscussion = Vue.extend({ - mixins: [DiscussionMixins], - props: { - discussionId: String +const JumpToDiscussion = Vue.extend({ + mixins: [DiscussionMixins], + props: { + discussionId: String + }, + data: function () { + return { + discussions: CommentsStore.state, + discussion: {}, + }; + }, + computed: { + allResolved: function () { + return this.unresolvedDiscussionCount === 0; }, - data: function () { - return { - discussions: CommentsStore.state, - discussion: {}, - }; - }, - computed: { - allResolved: function () { - return this.unresolvedDiscussionCount === 0; - }, - showButton: function () { - if (this.discussionId) { - if (this.unresolvedDiscussionCount > 1) { - return true; - } else { - return this.discussionId !== this.lastResolvedId; - } + showButton: function () { + if (this.discussionId) { + if (this.unresolvedDiscussionCount > 1) { + return true; } else { - return this.unresolvedDiscussionCount >= 1; + return this.discussionId !== this.lastResolvedId; } - }, - lastResolvedId: function () { - let lastId; - for (const discussionId in this.discussions) { - const discussion = this.discussions[discussionId]; - - if (!discussion.isResolved()) { - lastId = discussion.id; - } - } - return lastId; + } else { + return this.unresolvedDiscussionCount >= 1; } }, - methods: { - jumpToNextUnresolvedDiscussion: function () { - let discussionsSelector; - let discussionIdsInScope; - let firstUnresolvedDiscussionId; - let nextUnresolvedDiscussionId; - let activeTab = window.mrTabs.currentAction; - let hasDiscussionsToJumpTo = true; - let jumpToFirstDiscussion = !this.discussionId; - - const discussionIdsForElements = function(elements) { - return elements.map(function() { - return $(this).attr('data-discussion-id'); - }).toArray(); - }; - - const discussions = this.discussions; - - if (activeTab === 'diffs') { - discussionsSelector = '.diffs .notes[data-discussion-id]'; - discussionIdsInScope = discussionIdsForElements($(discussionsSelector)); - - let unresolvedDiscussionCount = 0; - - for (let i = 0; i < discussionIdsInScope.length; i += 1) { - const discussionId = discussionIdsInScope[i]; - const discussion = discussions[discussionId]; - if (discussion && !discussion.isResolved()) { - unresolvedDiscussionCount += 1; - } - } + lastResolvedId: function () { + let lastId; + for (const discussionId in this.discussions) { + const discussion = this.discussions[discussionId]; - if (this.discussionId && !this.discussion.isResolved()) { - // If this is the last unresolved discussion on the diffs tab, - // there are no discussions to jump to. - if (unresolvedDiscussionCount === 1) { - hasDiscussionsToJumpTo = false; - } - } else { - // If there are no unresolved discussions on the diffs tab at all, - // there are no discussions to jump to. - if (unresolvedDiscussionCount === 0) { - hasDiscussionsToJumpTo = false; - } - } - } else if (activeTab !== 'notes') { - // If we are on the commits or builds tabs, - // there are no discussions to jump to. - hasDiscussionsToJumpTo = false; + if (!discussion.isResolved()) { + lastId = discussion.id; } + } + return lastId; + } + }, + methods: { + jumpToNextUnresolvedDiscussion: function () { + let discussionsSelector; + let discussionIdsInScope; + let firstUnresolvedDiscussionId; + let nextUnresolvedDiscussionId; + let activeTab = window.mrTabs.currentAction; + let hasDiscussionsToJumpTo = true; + let jumpToFirstDiscussion = !this.discussionId; + + const discussionIdsForElements = function(elements) { + return elements.map(function() { + return $(this).attr('data-discussion-id'); + }).toArray(); + }; - if (!hasDiscussionsToJumpTo) { - // If there are no discussions to jump to on the current page, - // switch to the notes tab and jump to the first disucssion there. - window.mrTabs.activateTab('notes'); - activeTab = 'notes'; - jumpToFirstDiscussion = true; - } + const discussions = this.discussions; - if (activeTab === 'notes') { - discussionsSelector = '.discussion[data-discussion-id]'; - discussionIdsInScope = discussionIdsForElements($(discussionsSelector)); - } + if (activeTab === 'diffs') { + discussionsSelector = '.diffs .notes[data-discussion-id]'; + discussionIdsInScope = discussionIdsForElements($(discussionsSelector)); + + let unresolvedDiscussionCount = 0; - let currentDiscussionFound = false; for (let i = 0; i < discussionIdsInScope.length; i += 1) { const discussionId = discussionIdsInScope[i]; const discussion = discussions[discussionId]; + if (discussion && !discussion.isResolved()) { + unresolvedDiscussionCount += 1; + } + } - if (!discussion) { - // Discussions for comments on commits in this MR don't have a resolved status. - continue; + if (this.discussionId && !this.discussion.isResolved()) { + // If this is the last unresolved discussion on the diffs tab, + // there are no discussions to jump to. + if (unresolvedDiscussionCount === 1) { + hasDiscussionsToJumpTo = false; + } + } else { + // If there are no unresolved discussions on the diffs tab at all, + // there are no discussions to jump to. + if (unresolvedDiscussionCount === 0) { + hasDiscussionsToJumpTo = false; } + } + } else if (activeTab !== 'notes') { + // If we are on the commits or builds tabs, + // there are no discussions to jump to. + hasDiscussionsToJumpTo = false; + } - if (!firstUnresolvedDiscussionId && !discussion.isResolved()) { - firstUnresolvedDiscussionId = discussionId; + if (!hasDiscussionsToJumpTo) { + // If there are no discussions to jump to on the current page, + // switch to the notes tab and jump to the first disucssion there. + window.mrTabs.activateTab('notes'); + activeTab = 'notes'; + jumpToFirstDiscussion = true; + } - if (jumpToFirstDiscussion) { - break; - } + if (activeTab === 'notes') { + discussionsSelector = '.discussion[data-discussion-id]'; + discussionIdsInScope = discussionIdsForElements($(discussionsSelector)); + } + + let currentDiscussionFound = false; + for (let i = 0; i < discussionIdsInScope.length; i += 1) { + const discussionId = discussionIdsInScope[i]; + const discussion = discussions[discussionId]; + + if (!discussion) { + // Discussions for comments on commits in this MR don't have a resolved status. + continue; + } + + if (!firstUnresolvedDiscussionId && !discussion.isResolved()) { + firstUnresolvedDiscussionId = discussionId; + + if (jumpToFirstDiscussion) { + break; } + } - if (!jumpToFirstDiscussion) { - if (currentDiscussionFound) { - if (!discussion.isResolved()) { - nextUnresolvedDiscussionId = discussionId; - break; - } - else { - continue; - } + if (!jumpToFirstDiscussion) { + if (currentDiscussionFound) { + if (!discussion.isResolved()) { + nextUnresolvedDiscussionId = discussionId; + break; } - - if (discussionId === this.discussionId) { - currentDiscussionFound = true; + else { + continue; } } + + if (discussionId === this.discussionId) { + currentDiscussionFound = true; + } } + } - nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId; + nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId; - if (!nextUnresolvedDiscussionId) { - return; - } + if (!nextUnresolvedDiscussionId) { + return; + } - let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`); + let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`); - if (activeTab === 'notes') { - $target = $target.closest('.note-discussion'); + if (activeTab === 'notes') { + $target = $target.closest('.note-discussion'); - // If the next discussion is closed, toggle it open. - if ($target.find('.js-toggle-content').is(':hidden')) { - $target.find('.js-toggle-button i').trigger('click'); + // If the next discussion is closed, toggle it open. + if ($target.find('.js-toggle-content').is(':hidden')) { + $target.find('.js-toggle-button i').trigger('click'); + } + } else if (activeTab === 'diffs') { + // Resolved discussions are hidden in the diffs tab by default. + // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab. + // When jumping between unresolved discussions on the diffs tab, we show them. + $target.closest(".content").show(); + + $target = $target.closest("tr.notes_holder"); + $target.show(); + + // If we are on the diffs tab, we don't scroll to the discussion itself, but to + // 4 diff lines above it: the line the discussion was in response to + 3 context + let prevEl; + for (let i = 0; i < 4; i += 1) { + prevEl = $target.prev(); + + // If the discussion doesn't have 4 lines above it, we'll have to do with fewer. + if (!prevEl.hasClass("line_holder")) { + break; } - } else if (activeTab === 'diffs') { - // Resolved discussions are hidden in the diffs tab by default. - // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab. - // When jumping between unresolved discussions on the diffs tab, we show them. - $target.closest(".content").show(); - - $target = $target.closest("tr.notes_holder"); - $target.show(); - - // If we are on the diffs tab, we don't scroll to the discussion itself, but to - // 4 diff lines above it: the line the discussion was in response to + 3 context - let prevEl; - for (let i = 0; i < 4; i += 1) { - prevEl = $target.prev(); - - // If the discussion doesn't have 4 lines above it, we'll have to do with fewer. - if (!prevEl.hasClass("line_holder")) { - break; - } - $target = prevEl; - } + $target = prevEl; } - - $.scrollTo($target, { - offset: 0 - }); } - }, - created() { - this.discussion = this.discussions[this.discussionId]; - }, - }); - Vue.component('jump-to-discussion', JumpToDiscussion); -})(); + $.scrollTo($target, { + offset: 0 + }); + } + }, + created() { + this.discussion = this.discussions[this.discussionId]; + }, +}); + +Vue.component('jump-to-discussion', JumpToDiscussion); 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 8eb0e10b832..e0c09aa0eee 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 @@ -2,29 +2,27 @@ import Vue from 'vue'; -(() => { - const NewIssueForDiscussion = Vue.extend({ - props: { - discussionId: { - type: String, - required: true, - }, +const NewIssueForDiscussion = Vue.extend({ + props: { + discussionId: { + type: String, + required: true, }, - data() { - return { - discussions: CommentsStore.state, - }; + }, + data() { + return { + discussions: CommentsStore.state, + }; + }, + computed: { + discussion() { + return this.discussions[this.discussionId]; }, - computed: { - discussion() { - return this.discussions[this.discussionId]; - }, - showButton() { - if (this.discussion) return !this.discussion.isResolved(); - return false; - }, + showButton() { + if (this.discussion) return !this.discussion.isResolved(); + return false; }, - }); + }, +}); - Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion); -})(); +Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion); diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js index 312f38ce241..8fafd13c6c2 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js @@ -5,117 +5,115 @@ import Vue from 'vue'; -(() => { - const ResolveBtn = Vue.extend({ - props: { - noteId: Number, - discussionId: String, - resolved: Boolean, - canResolve: Boolean, - resolvedBy: String, - authorName: String, - authorAvatar: String, - noteTruncated: String, +const ResolveBtn = Vue.extend({ + props: { + noteId: Number, + discussionId: String, + resolved: Boolean, + canResolve: Boolean, + resolvedBy: String, + authorName: String, + authorAvatar: String, + noteTruncated: String, + }, + data: function () { + return { + discussions: CommentsStore.state, + loading: false + }; + }, + watch: { + 'discussions': { + handler: 'updateTooltip', + deep: true + } + }, + computed: { + discussion: function () { + return this.discussions[this.discussionId]; }, - data: function () { - return { - discussions: CommentsStore.state, - loading: false, - note: {}, - }; + note: function () { + return this.discussion ? this.discussion.getNote(this.noteId) : {}; }, - watch: { - 'discussions': { - handler: 'updateTooltip', - deep: true + buttonText: function () { + if (this.isResolved) { + return `Resolved by ${this.resolvedByName}`; + } else if (this.canResolve) { + return 'Mark as resolved'; + } else { + return 'Unable to resolve'; } }, - computed: { - discussion: function () { - return this.discussions[this.discussionId]; - }, - buttonText: function () { - if (this.isResolved) { - return `Resolved by ${this.resolvedByName}`; - } else if (this.canResolve) { - return 'Mark as resolved'; - } else { - return 'Unable to resolve'; - } - }, - isResolved: function () { - if (this.note) { - return this.note.resolved; - } else { - return false; - } - }, - resolvedByName: function () { - return this.note.resolved_by; - }, + isResolved: function () { + if (this.note) { + return this.note.resolved; + } else { + return false; + } + }, + resolvedByName: function () { + return this.note.resolved_by; }, - methods: { - updateTooltip: function () { - this.$nextTick(() => { - $(this.$refs.button) - .tooltip('hide') - .tooltip('fixTitle'); - }); - }, - resolve: function () { - if (!this.canResolve) return; + }, + methods: { + updateTooltip: function () { + this.$nextTick(() => { + $(this.$refs.button) + .tooltip('hide') + .tooltip('fixTitle'); + }); + }, + resolve: function () { + if (!this.canResolve) return; - let promise; - this.loading = true; + let promise; + this.loading = true; - if (this.isResolved) { - promise = ResolveService - .unresolve(this.noteId); - } else { - promise = ResolveService - .resolve(this.noteId); - } + if (this.isResolved) { + promise = ResolveService + .unresolve(this.noteId); + } else { + promise = ResolveService + .resolve(this.noteId); + } - promise.then((response) => { - this.loading = false; + promise.then((response) => { + this.loading = false; - if (response.status === 200) { - const data = response.json(); - const resolved_by = data ? data.resolved_by : null; + if (response.status === 200) { + const data = response.json(); + const resolved_by = data ? data.resolved_by : null; - CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by); - this.discussion.updateHeadline(data); - } else { - new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert'); - } + CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by); + this.discussion.updateHeadline(data); + } else { + new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert'); + } - this.updateTooltip(); - }); - } - }, - mounted: function () { - $(this.$refs.button).tooltip({ - container: 'body' + this.updateTooltip(); }); - }, - beforeDestroy: function () { - CommentsStore.delete(this.discussionId, this.noteId); - }, - created: function () { - CommentsStore.create({ - discussionId: this.discussionId, - noteId: this.noteId, - canResolve: this.canResolve, - resolved: this.resolved, - resolvedBy: this.resolvedBy, - authorName: this.authorName, - authorAvatar: this.authorAvatar, - noteTruncated: this.noteTruncated, - }); - - this.note = this.discussion.getNote(this.noteId); } - }); + }, + mounted: function () { + $(this.$refs.button).tooltip({ + container: 'body' + }); + }, + beforeDestroy: function () { + CommentsStore.delete(this.discussionId, this.noteId); + }, + created: function () { + CommentsStore.create({ + discussionId: this.discussionId, + noteId: this.noteId, + canResolve: this.canResolve, + resolved: this.resolved, + resolvedBy: this.resolvedBy, + authorName: this.authorName, + authorAvatar: this.authorAvatar, + noteTruncated: this.noteTruncated, + }); + } +}); - Vue.component('resolve-btn', ResolveBtn); -})(); +Vue.component('resolve-btn', ResolveBtn); diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js index 27147ac6b5c..96e5a440357 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_count.js +++ b/app/assets/javascripts/diff_notes/components/resolve_count.js @@ -4,24 +4,22 @@ import Vue from 'vue'; -((w) => { - w.ResolveCount = Vue.extend({ - mixins: [DiscussionMixins], - props: { - loggedOut: Boolean +window.ResolveCount = Vue.extend({ + mixins: [DiscussionMixins], + props: { + loggedOut: Boolean + }, + data: function () { + return { + discussions: CommentsStore.state + }; + }, + computed: { + allResolved: function () { + return this.resolvedDiscussionCount === this.discussionCount; }, - data: function () { - return { - discussions: CommentsStore.state - }; - }, - computed: { - allResolved: function () { - return this.resolvedDiscussionCount === this.discussionCount; - }, - resolvedCountText() { - return this.discussionCount === 1 ? 'discussion' : 'discussions'; - } + resolvedCountText() { + return this.discussionCount === 1 ? 'discussion' : 'discussions'; } - }); -})(window); + } +}); 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 a964b7d0c6b..6a036e96171 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js @@ -4,59 +4,57 @@ import Vue from 'vue'; -(() => { - const ResolveDiscussionBtn = Vue.extend({ - props: { - discussionId: String, - mergeRequestId: Number, - canResolve: Boolean, - }, - data: function() { - return { - discussion: {}, - }; +const ResolveDiscussionBtn = Vue.extend({ + props: { + discussionId: String, + mergeRequestId: Number, + canResolve: Boolean, + }, + data: function() { + return { + discussion: {}, + }; + }, + computed: { + showButton: function () { + if (this.discussion) { + return this.discussion.isResolvable(); + } else { + return false; + } }, - computed: { - showButton: function () { - if (this.discussion) { - return this.discussion.isResolvable(); - } else { - return false; - } - }, - isDiscussionResolved: function () { - if (this.discussion) { - return this.discussion.isResolved(); - } else { - return false; - } - }, - buttonText: function () { - if (this.isDiscussionResolved) { - return "Unresolve discussion"; - } else { - return "Resolve discussion"; - } - }, - loading: function () { - if (this.discussion) { - return this.discussion.loading; - } else { - return false; - } + isDiscussionResolved: function () { + if (this.discussion) { + return this.discussion.isResolved(); + } else { + return false; } }, - methods: { - resolve: function () { - ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId); + buttonText: function () { + if (this.isDiscussionResolved) { + return "Unresolve discussion"; + } else { + return "Resolve discussion"; } }, - created: function () { - CommentsStore.createDiscussion(this.discussionId, this.canResolve); - - this.discussion = CommentsStore.state[this.discussionId]; + loading: function () { + if (this.discussion) { + return this.discussion.loading; + } else { + return false; + } + } + }, + methods: { + resolve: function () { + ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId); } - }); + }, + created: function () { + CommentsStore.createDiscussion(this.discussionId, this.canResolve); + + this.discussion = CommentsStore.state[this.discussionId]; + } +}); - Vue.component('resolve-discussion-btn', ResolveDiscussionBtn); -})(); +Vue.component('resolve-discussion-btn', ResolveDiscussionBtn); diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js b/app/assets/javascripts/diff_notes/mixins/discussion.js index 3c08c222f46..36c4abf02cf 100644 --- a/app/assets/javascripts/diff_notes/mixins/discussion.js +++ b/app/assets/javascripts/diff_notes/mixins/discussion.js @@ -1,37 +1,35 @@ /* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */ -((w) => { - w.DiscussionMixins = { - computed: { - discussionCount: function () { - return Object.keys(this.discussions).length; - }, - resolvedDiscussionCount: function () { - let resolvedCount = 0; +window.DiscussionMixins = { + computed: { + discussionCount: function () { + return Object.keys(this.discussions).length; + }, + resolvedDiscussionCount: function () { + let resolvedCount = 0; - for (const discussionId in this.discussions) { - const discussion = this.discussions[discussionId]; + for (const discussionId in this.discussions) { + const discussion = this.discussions[discussionId]; - if (discussion.isResolved()) { - resolvedCount += 1; - } + if (discussion.isResolved()) { + resolvedCount += 1; } + } - return resolvedCount; - }, - unresolvedDiscussionCount: function () { - let unresolvedCount = 0; + return resolvedCount; + }, + unresolvedDiscussionCount: function () { + let unresolvedCount = 0; - for (const discussionId in this.discussions) { - const discussion = this.discussions[discussionId]; + for (const discussionId in this.discussions) { + const discussion = this.discussions[discussionId]; - if (!discussion.isResolved()) { - unresolvedCount += 1; - } + if (!discussion.isResolved()) { + unresolvedCount += 1; } - - return unresolvedCount; } + + return unresolvedCount; } - }; -})(window); + } +}; diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js index bfa4fc9037a..e1e2e3e93f9 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js +++ b/app/assets/javascripts/diff_notes/services/resolve.js @@ -9,76 +9,74 @@ require('../../vue_shared/vue_resource_interceptor'); Vue.use(VueResource); -(() => { - window.gl = window.gl || {}; +window.gl = window.gl || {}; - class ResolveServiceClass { - constructor(root) { - this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`); - this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`); - } - - resolve(noteId) { - return this.noteResource.save({ noteId }, {}); - } +class ResolveServiceClass { + constructor(root) { + this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`); + this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`); + } - unresolve(noteId) { - return this.noteResource.delete({ noteId }, {}); - } + resolve(noteId) { + return this.noteResource.save({ noteId }, {}); + } - toggleResolveForDiscussion(mergeRequestId, discussionId) { - const discussion = CommentsStore.state[discussionId]; - const isResolved = discussion.isResolved(); - let promise; + unresolve(noteId) { + return this.noteResource.delete({ noteId }, {}); + } - if (isResolved) { - promise = this.unResolveAll(mergeRequestId, discussionId); - } else { - promise = this.resolveAll(mergeRequestId, discussionId); - } + toggleResolveForDiscussion(mergeRequestId, discussionId) { + const discussion = CommentsStore.state[discussionId]; + const isResolved = discussion.isResolved(); + let promise; - promise.then((response) => { - discussion.loading = false; + if (isResolved) { + promise = this.unResolveAll(mergeRequestId, discussionId); + } else { + promise = this.resolveAll(mergeRequestId, discussionId); + } - if (response.status === 200) { - const data = response.json(); - const resolved_by = data ? data.resolved_by : null; + promise.then((response) => { + discussion.loading = false; - if (isResolved) { - discussion.unResolveAllNotes(); - } else { - discussion.resolveAllNotes(resolved_by); - } + if (response.status === 200) { + const data = response.json(); + const resolved_by = data ? data.resolved_by : null; - discussion.updateHeadline(data); + if (isResolved) { + discussion.unResolveAllNotes(); } else { - new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert'); + discussion.resolveAllNotes(resolved_by); } - }); - } - resolveAll(mergeRequestId, discussionId) { - const discussion = CommentsStore.state[discussionId]; + discussion.updateHeadline(data); + } else { + new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert'); + } + }); + } - discussion.loading = true; + resolveAll(mergeRequestId, discussionId) { + const discussion = CommentsStore.state[discussionId]; - return this.discussionResource.save({ - mergeRequestId, - discussionId - }, {}); - } + discussion.loading = true; + + return this.discussionResource.save({ + mergeRequestId, + discussionId + }, {}); + } - unResolveAll(mergeRequestId, discussionId) { - const discussion = CommentsStore.state[discussionId]; + unResolveAll(mergeRequestId, discussionId) { + const discussion = CommentsStore.state[discussionId]; - discussion.loading = true; + discussion.loading = true; - return this.discussionResource.delete({ - mergeRequestId, - discussionId - }, {}); - } + return this.discussionResource.delete({ + mergeRequestId, + discussionId + }, {}); } +} - gl.DiffNotesResolveServiceClass = ResolveServiceClass; -})(); +gl.DiffNotesResolveServiceClass = ResolveServiceClass; diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js index e6cbda56c91..d802db7d3af 100644 --- a/app/assets/javascripts/diff_notes/stores/comments.js +++ b/app/assets/javascripts/diff_notes/stores/comments.js @@ -3,56 +3,54 @@ import Vue from 'vue'; -((w) => { - w.CommentsStore = { - state: {}, - get: function (discussionId, noteId) { - return this.state[discussionId].getNote(noteId); - }, - createDiscussion: function (discussionId, canResolve) { - let discussion = this.state[discussionId]; - if (!this.state[discussionId]) { - discussion = new DiscussionModel(discussionId); - Vue.set(this.state, discussionId, discussion); - } +window.CommentsStore = { + state: {}, + get: function (discussionId, noteId) { + return this.state[discussionId].getNote(noteId); + }, + createDiscussion: function (discussionId, canResolve) { + let discussion = this.state[discussionId]; + if (!this.state[discussionId]) { + discussion = new DiscussionModel(discussionId); + Vue.set(this.state, discussionId, discussion); + } - if (canResolve !== undefined) { - discussion.canResolve = canResolve; - } + if (canResolve !== undefined) { + discussion.canResolve = canResolve; + } - return discussion; - }, - create: function (noteObj) { - const discussion = this.createDiscussion(noteObj.discussionId); + return discussion; + }, + create: function (noteObj) { + const discussion = this.createDiscussion(noteObj.discussionId); + + discussion.createNote(noteObj); + }, + update: function (discussionId, noteId, resolved, resolved_by) { + const discussion = this.state[discussionId]; + const note = discussion.getNote(noteId); + note.resolved = resolved; + note.resolved_by = resolved_by; + }, + delete: function (discussionId, noteId) { + const discussion = this.state[discussionId]; + discussion.deleteNote(noteId); + + if (discussion.notesCount() === 0) { + Vue.delete(this.state, discussionId); + } + }, + unresolvedDiscussionIds: function () { + const ids = []; - discussion.createNote(noteObj); - }, - update: function (discussionId, noteId, resolved, resolved_by) { - const discussion = this.state[discussionId]; - const note = discussion.getNote(noteId); - note.resolved = resolved; - note.resolved_by = resolved_by; - }, - delete: function (discussionId, noteId) { + for (const discussionId in this.state) { const discussion = this.state[discussionId]; - discussion.deleteNote(noteId); - if (discussion.notesCount() === 0) { - Vue.delete(this.state, discussionId); + if (!discussion.isResolved()) { + ids.push(discussion.id); } - }, - unresolvedDiscussionIds: function () { - const ids = []; - - for (const discussionId in this.state) { - const discussion = this.state[discussionId]; - - if (!discussion.isResolved()) { - ids.push(discussion.id); - } - } - - return ids; } - }; -})(window); + + return ids; + } +}; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index f277e1dddc7..02a7df9b2a0 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -33,6 +33,7 @@ /* global Labels */ /* global Shortcuts */ /* global Sidebar */ +/* global ShortcutsWiki */ import Issue from './issue'; @@ -46,6 +47,7 @@ import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; import UserCallout from './user_callout'; import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags'; +import ShortcutsWiki from './shortcuts_wiki'; const ShortcutsBlob = require('./shortcuts_blob'); @@ -148,13 +150,13 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'projects:milestones:new': case 'projects:milestones:edit': case 'projects:milestones:update': + case 'groups:milestones:new': + case 'groups:milestones:edit': + case 'groups:milestones:update': new ZenMode(); new gl.DueDateSelectors(); new gl.GLForm($('.milestone-form')); break; - case 'groups:milestones:new': - new ZenMode(); - break; case 'projects:compare:show': new gl.Diff(); break; @@ -365,6 +367,9 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'admin': new Admin(); switch (path[1]) { + case 'cohorts': + new gl.UsagePing(); + break; case 'groups': new UsersSelect(); break; @@ -416,7 +421,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); break; case 'wikis': new gl.Wikis(); - shortcut_handler = new ShortcutsNavigation(); + shortcut_handler = new ShortcutsWiki(); new ZenMode(); new gl.GLForm($('.wiki-form')); break; diff --git a/app/assets/javascripts/droplab/constants.js b/app/assets/javascripts/droplab/constants.js index a23d914772a..8883ed9aa14 100644 --- a/app/assets/javascripts/droplab/constants.js +++ b/app/assets/javascripts/droplab/constants.js @@ -2,10 +2,12 @@ const DATA_TRIGGER = 'data-dropdown-trigger'; const DATA_DROPDOWN = 'data-dropdown'; const SELECTED_CLASS = 'droplab-item-selected'; const ACTIVE_CLASS = 'droplab-item-active'; +const IGNORE_CLASS = 'droplab-item-ignore'; export { DATA_TRIGGER, DATA_DROPDOWN, SELECTED_CLASS, ACTIVE_CLASS, + IGNORE_CLASS, }; diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js index 9588921ebcd..1fb4d63923c 100644 --- a/app/assets/javascripts/droplab/drop_down.js +++ b/app/assets/javascripts/droplab/drop_down.js @@ -1,7 +1,7 @@ /* eslint-disable */ import utils from './utils'; -import { SELECTED_CLASS } from './constants'; +import { SELECTED_CLASS, IGNORE_CLASS } from './constants'; var DropDown = function(list) { this.currentIndex = 0; @@ -36,6 +36,7 @@ Object.assign(DropDown.prototype, { clickEvent: function(e) { if (e.target.tagName === 'UL') return; + if (e.target.classList.contains(IGNORE_CLASS)) return; var selected = utils.closest(e.target, 'LI'); if (!selected) return; diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index f2963a5eb19..b70d242269d 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -38,6 +38,9 @@ window.DropzoneInput = (function() { "opacity": 0, "display": "none" }); + + if (!project_uploads_path) return; + dropzone = form_dropzone.dropzone({ url: project_uploads_path, dictDefaultMessage: "", @@ -66,7 +69,10 @@ window.DropzoneInput = (function() { form_textarea.focus(); }, success: function(header, response) { - pasteText(response.link.markdown); + const processingFileCount = this.getQueuedFiles().length + this.getUploadingFiles().length; + const shouldPad = processingFileCount >= 1; + + pasteText(response.link.markdown, shouldPad); }, error: function(temp) { var checkIfMsgExists, errorAlert; @@ -123,16 +129,19 @@ window.DropzoneInput = (function() { } return false; }; - pasteText = function(text) { + pasteText = function(text, shouldPad) { var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; - var formattedText = text + "\n\n"; - caretStart = $(child)[0].selectionStart; - caretEnd = $(child)[0].selectionEnd; + var formattedText = text; + if (shouldPad) formattedText += "\n\n"; + const textarea = child.get(0); + caretStart = textarea.selectionStart; + caretEnd = textarea.selectionEnd; textEnd = $(child).val().length; beforeSelection = $(child).val().substring(0, caretStart); afterSelection = $(child).val().substring(caretEnd, textEnd); $(child).val(beforeSelection + formattedText + afterSelection); - child.get(0).setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); + textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); + textarea.style.height = `${textarea.scrollHeight}px`; return form_textarea.trigger("input"); }; getFilename = function(e) { @@ -176,7 +185,7 @@ window.DropzoneInput = (function() { }; insertToTextArea = function(filename, url) { return $(child).val(function(index, val) { - return val.replace("{{" + filename + "}}", url + "\n"); + return val.replace("{{" + filename + "}}", url); }); }; appendToTextArea = function(url) { @@ -211,6 +220,7 @@ window.DropzoneInput = (function() { form.find(".markdown-selector").click(function(e) { e.preventDefault(); $(this).closest('.gfm-form').find('.div-dropzone').click(); + form_textarea.focus(); }); } diff --git a/app/assets/javascripts/environments/components/environment_actions.js b/app/assets/javascripts/environments/components/environment_actions.js index 1418e8d86ee..313e78e573a 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js +++ b/app/assets/javascripts/environments/components/environment_actions.js @@ -35,6 +35,8 @@ export default { onClickAction(endpoint) { this.isLoading = true; + $(this.$refs.tooltip).tooltip('destroy'); + this.service.postAction(endpoint) .then(() => { this.isLoading = false; @@ -62,6 +64,7 @@ export default { class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip" data-container="body" data-toggle="dropdown" + ref="tooltip" :title="title" :aria-label="title" :disabled="isLoading"> diff --git a/app/assets/javascripts/environments/components/environment_monitoring.js b/app/assets/javascripts/environments/components/environment_monitoring.js index 064e2fc7434..8c37dd76ae7 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.js +++ b/app/assets/javascripts/environments/components/environment_monitoring.js @@ -21,7 +21,6 @@ export default { class="btn monitoring-url has-tooltip" data-container="body" :href="monitoringUrl" - target="_blank" rel="noopener noreferrer nofollow" :title="title" :aria-label="title"> diff --git a/app/assets/javascripts/environments/components/environment_rollback.js b/app/assets/javascripts/environments/components/environment_rollback.js index baa15d9e5b5..7cbfb651525 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.js +++ b/app/assets/javascripts/environments/components/environment_rollback.js @@ -36,6 +36,8 @@ export default { onClick() { this.isLoading = true; + $(this.$el).tooltip('destroy'); + this.service.postAction(this.retryUrl) .then(() => { this.isLoading = false; diff --git a/app/assets/javascripts/environments/components/environment_stop.js b/app/assets/javascripts/environments/components/environment_stop.js index 47102692024..9e5465c1785 100644 --- a/app/assets/javascripts/environments/components/environment_stop.js +++ b/app/assets/javascripts/environments/components/environment_stop.js @@ -36,6 +36,8 @@ export default { if (confirm('Are you sure you want to stop this environment?')) { this.isLoading = true; + $(this.$el).tooltip('destroy'); + this.service.postAction(this.retryUrl) .then(() => { this.isLoading = false; diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 381c40c03d8..3e7a892756c 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -2,82 +2,80 @@ import Filter from '~/droplab/plugins/filter'; require('./filtered_search_dropdown'); -(() => { - class DropdownHint extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter) { - super(droplab, dropdown, input, filter); - this.config = { - Filter: { - template: 'hint', - filterFunction: gl.DropdownUtils.filterHint.bind(null, input), - }, - }; - } - - itemClicked(e) { - const { selected } = e.detail; +class DropdownHint extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + super(droplab, dropdown, input, filter); + this.config = { + Filter: { + template: 'hint', + filterFunction: gl.DropdownUtils.filterHint.bind(null, input), + }, + }; + } - if (selected.tagName === 'LI') { - if (selected.hasAttribute('data-value')) { - this.dismissDropdown(); - } else if (selected.getAttribute('data-action') === 'submit') { - this.dismissDropdown(); - this.dispatchFormSubmitEvent(); - } else { - const token = selected.querySelector('.js-filter-hint').innerText.trim(); - const tag = selected.querySelector('.js-filter-tag').innerText.trim(); + itemClicked(e) { + const { selected } = e.detail; - if (tag.length) { - // Get previous input values in the input field and convert them into visual tokens - const previousInputValues = this.input.value.split(' '); - const searchTerms = []; + if (selected.tagName === 'LI') { + if (selected.hasAttribute('data-value')) { + this.dismissDropdown(); + } else if (selected.getAttribute('data-action') === 'submit') { + this.dismissDropdown(); + this.dispatchFormSubmitEvent(); + } else { + const token = selected.querySelector('.js-filter-hint').innerText.trim(); + const tag = selected.querySelector('.js-filter-tag').innerText.trim(); - previousInputValues.forEach((value, index) => { - searchTerms.push(value); + if (tag.length) { + // Get previous input values in the input field and convert them into visual tokens + const previousInputValues = this.input.value.split(' '); + const searchTerms = []; - if (index === previousInputValues.length - 1 - && token.indexOf(value.toLowerCase()) !== -1) { - searchTerms.pop(); - } - }); + previousInputValues.forEach((value, index) => { + searchTerms.push(value); - if (searchTerms.length > 0) { - gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' ')); + if (index === previousInputValues.length - 1 + && token.indexOf(value.toLowerCase()) !== -1) { + searchTerms.pop(); } + }); - gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container); + if (searchTerms.length > 0) { + gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' ')); } - this.dismissDropdown(); - this.dispatchInputEvent(); + + gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container); } + this.dismissDropdown(); + this.dispatchInputEvent(); } } + } - renderContent() { - const dropdownData = []; + renderContent() { + const dropdownData = []; - [].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => { - const { icon, hint, tag, type } = dropdownMenu.dataset; - if (icon && hint && tag) { - dropdownData.push( - Object.assign({ - icon: `fa-${icon}`, - hint, - tag: `<${tag}>`, - }, type && { type }), - ); - } - }); + [].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => { + const { icon, hint, tag, type } = dropdownMenu.dataset; + if (icon && hint && tag) { + dropdownData.push( + Object.assign({ + icon: `fa-${icon}`, + hint, + tag: `<${tag}>`, + }, type && { type }), + ); + } + }); - this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config); - this.droplab.setData(this.hookId, dropdownData); - } + this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config); + this.droplab.setData(this.hookId, dropdownData); + } - init() { - this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init(); - } + init() { + this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init(); } +} - window.gl = window.gl || {}; - gl.DropdownHint = DropdownHint; -})(); +window.gl = window.gl || {}; +gl.DropdownHint = DropdownHint; diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js index 6296965b911..982dc4b61be 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js @@ -5,48 +5,46 @@ import Filter from '~/droplab/plugins/filter'; require('./filtered_search_dropdown'); -(() => { - class DropdownNonUser extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter, endpoint, symbol) { - super(droplab, dropdown, input, filter); - this.symbol = symbol; - this.config = { - Ajax: { - endpoint, - method: 'setData', - loadingTemplate: this.loadingTemplate, - onError() { - /* eslint-disable no-new */ - new Flash('An error occured fetching the dropdown data.'); - /* eslint-enable no-new */ - }, +class DropdownNonUser extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter, endpoint, symbol) { + super(droplab, dropdown, input, filter); + this.symbol = symbol; + this.config = { + Ajax: { + endpoint, + method: 'setData', + loadingTemplate: this.loadingTemplate, + onError() { + /* eslint-disable no-new */ + new Flash('An error occured fetching the dropdown data.'); + /* eslint-enable no-new */ }, - Filter: { - filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input), - template: 'title', - }, - }; - } + }, + Filter: { + filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input), + template: 'title', + }, + }; + } - itemClicked(e) { - super.itemClicked(e, (selected) => { - const title = selected.querySelector('.js-data-value').innerText.trim(); - return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`; - }); - } + itemClicked(e) { + super.itemClicked(e, (selected) => { + const title = selected.querySelector('.js-data-value').innerText.trim(); + return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`; + }); + } - renderContent(forceShowList = false) { - this.droplab - .changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config); - super.renderContent(forceShowList); - } + renderContent(forceShowList = false) { + this.droplab + .changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config); + super.renderContent(forceShowList); + } - init() { - this.droplab - .addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init(); - } + init() { + this.droplab + .addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init(); } +} - window.gl = window.gl || {}; - gl.DropdownNonUser = DropdownNonUser; -})(); +window.gl = window.gl || {}; +gl.DropdownNonUser = DropdownNonUser; diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index 38b5d315bcf..74cec3d75fe 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -4,69 +4,67 @@ import AjaxFilter from '~/droplab/plugins/ajax_filter'; require('./filtered_search_dropdown'); -(() => { - class DropdownUser extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter) { - super(droplab, dropdown, input, filter); - this.config = { - AjaxFilter: { - endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, - searchKey: 'search', - params: { - per_page: 20, - active: true, - project_id: this.getProjectId(), - current_user: true, - }, - searchValueFunction: this.getSearchInput.bind(this), - loadingTemplate: this.loadingTemplate, - onError() { - /* eslint-disable no-new */ - new Flash('An error occured fetching the dropdown data.'); - /* eslint-enable no-new */ - }, +class DropdownUser extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + super(droplab, dropdown, input, filter); + this.config = { + AjaxFilter: { + endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, + searchKey: 'search', + params: { + per_page: 20, + active: true, + project_id: this.getProjectId(), + current_user: true, }, - }; - } - - itemClicked(e) { - super.itemClicked(e, - selected => selected.querySelector('.dropdown-light-content').innerText.trim()); - } - - renderContent(forceShowList = false) { - this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config); - super.renderContent(forceShowList); - } + searchValueFunction: this.getSearchInput.bind(this), + loadingTemplate: this.loadingTemplate, + onError() { + /* eslint-disable no-new */ + new Flash('An error occured fetching the dropdown data.'); + /* eslint-enable no-new */ + }, + }, + }; + } - getProjectId() { - return this.input.getAttribute('data-project-id'); - } + itemClicked(e) { + super.itemClicked(e, + selected => selected.querySelector('.dropdown-light-content').innerText.trim()); + } - getSearchInput() { - const query = gl.DropdownUtils.getSearchInput(this.input); - const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + renderContent(forceShowList = false) { + this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config); + super.renderContent(forceShowList); + } - let value = lastToken || ''; + getProjectId() { + return this.input.getAttribute('data-project-id'); + } - if (value[0] === '@') { - value = value.slice(1); - } + getSearchInput() { + const query = gl.DropdownUtils.getSearchInput(this.input); + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); - // Removes the first character if it is a quotation so that we can search - // with multiple words - if (value[0] === '"' || value[0] === '\'') { - value = value.slice(1); - } + let value = lastToken || ''; - return value; + if (value[0] === '@') { + value = value.slice(1); } - init() { - this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init(); + // Removes the first character if it is a quotation so that we can search + // with multiple words + if (value[0] === '"' || value[0] === '\'') { + value = value.slice(1); } + + return value; + } + + init() { + this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init(); } +} - window.gl = window.gl || {}; - gl.DropdownUser = DropdownUser; -})(); +window.gl = window.gl || {}; +gl.DropdownUser = DropdownUser; diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index 6c5c20447f7..bc7c1dffece 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -1,183 +1,181 @@ import FilteredSearchContainer from './container'; -(() => { - class DropdownUtils { - static getEscapedText(text) { - let escapedText = text; - const hasSpace = text.indexOf(' ') !== -1; - const hasDoubleQuote = text.indexOf('"') !== -1; - - // Encapsulate value with quotes if it has spaces - // Known side effect: values's with both single and double quotes - // won't escape properly - if (hasSpace) { - if (hasDoubleQuote) { - escapedText = `'${text}'`; - } else { - // Encapsulate singleQuotes or if it hasSpace - escapedText = `"${text}"`; - } +class DropdownUtils { + static getEscapedText(text) { + let escapedText = text; + const hasSpace = text.indexOf(' ') !== -1; + const hasDoubleQuote = text.indexOf('"') !== -1; + + // Encapsulate value with quotes if it has spaces + // Known side effect: values's with both single and double quotes + // won't escape properly + if (hasSpace) { + if (hasDoubleQuote) { + escapedText = `'${text}'`; + } else { + // Encapsulate singleQuotes or if it hasSpace + escapedText = `"${text}"`; } - - return escapedText; } - static filterWithSymbol(filterSymbol, input, item) { - const updatedItem = item; - const searchInput = gl.DropdownUtils.getSearchInput(input); + return escapedText; + } + + static filterWithSymbol(filterSymbol, input, item) { + const updatedItem = item; + const searchInput = gl.DropdownUtils.getSearchInput(input); - const title = updatedItem.title.toLowerCase(); - let value = searchInput.toLowerCase(); - let symbol = ''; + const title = updatedItem.title.toLowerCase(); + let value = searchInput.toLowerCase(); + let symbol = ''; - // Remove the symbol for filter - if (value[0] === filterSymbol) { - symbol = value[0]; - value = value.slice(1); - } + // Remove the symbol for filter + if (value[0] === filterSymbol) { + symbol = value[0]; + value = value.slice(1); + } - // Removes the first character if it is a quotation so that we can search - // with multiple words - if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { - value = value.slice(1); - } + // Removes the first character if it is a quotation so that we can search + // with multiple words + if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { + value = value.slice(1); + } + + // Eg. filterSymbol = ~ for labels + const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1; + const match = title.indexOf(`${symbol}${value}`) !== -1; - // Eg. filterSymbol = ~ for labels - const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1; - const match = title.indexOf(`${symbol}${value}`) !== -1; + updatedItem.droplab_hidden = !match && !matchWithoutSymbol; - updatedItem.droplab_hidden = !match && !matchWithoutSymbol; + return updatedItem; + } - return updatedItem; + static filterHint(input, item) { + const updatedItem = item; + const searchInput = gl.DropdownUtils.getSearchQuery(input); + const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput); + const lastKey = lastToken.key || lastToken || ''; + const allowMultiple = item.type === 'array'; + const itemInExistingTokens = tokens.some(t => t.key === item.hint); + + if (!allowMultiple && itemInExistingTokens) { + updatedItem.droplab_hidden = true; + } else if (!lastKey || searchInput.split('').last() === ' ') { + updatedItem.droplab_hidden = false; + } else if (lastKey) { + const split = lastKey.split(':'); + const tokenName = split[0].split(' ').last(); + + const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1; + updatedItem.droplab_hidden = tokenName ? match : false; } - static filterHint(input, item) { - const updatedItem = item; - const searchInput = gl.DropdownUtils.getSearchQuery(input); - const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput); - const lastKey = lastToken.key || lastToken || ''; - const allowMultiple = item.type === 'array'; - const itemInExistingTokens = tokens.some(t => t.key === item.hint); - - if (!allowMultiple && itemInExistingTokens) { - updatedItem.droplab_hidden = true; - } else if (!lastKey || searchInput.split('').last() === ' ') { - updatedItem.droplab_hidden = false; - } else if (lastKey) { - const split = lastKey.split(':'); - const tokenName = split[0].split(' ').last(); - - const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1; - updatedItem.droplab_hidden = tokenName ? match : false; - } + return updatedItem; + } + + static setDataValueIfSelected(filter, selected) { + const dataValue = selected.getAttribute('data-value'); - return updatedItem; + if (dataValue) { + gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true); } - static setDataValueIfSelected(filter, selected) { - const dataValue = selected.getAttribute('data-value'); + // Return boolean based on whether it was set + return dataValue !== null; + } - if (dataValue) { - gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true); - } + // Determines the full search query (visual tokens + input) + static getSearchQuery(untilInput = false) { + const container = FilteredSearchContainer.container; + const tokens = [].slice.call(container.querySelectorAll('.tokens-container li')); + const values = []; - // Return boolean based on whether it was set - return dataValue !== null; + if (untilInput) { + const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token')); + // Add one to include input-token to the tokens array + tokens.splice(inputIndex + 1); } - // Determines the full search query (visual tokens + input) - static getSearchQuery(untilInput = false) { - const container = FilteredSearchContainer.container; - const tokens = [].slice.call(container.querySelectorAll('.tokens-container li')); - const values = []; + tokens.forEach((token) => { + if (token.classList.contains('js-visual-token')) { + const name = token.querySelector('.name'); + const value = token.querySelector('.value'); + const symbol = value && value.dataset.symbol ? value.dataset.symbol : ''; + let valueText = ''; - if (untilInput) { - const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token')); - // Add one to include input-token to the tokens array - tokens.splice(inputIndex + 1); - } + if (value && value.innerText) { + valueText = value.innerText; + } - tokens.forEach((token) => { - if (token.classList.contains('js-visual-token')) { - const name = token.querySelector('.name'); - const value = token.querySelector('.value'); - const symbol = value && value.dataset.symbol ? value.dataset.symbol : ''; - let valueText = ''; - - if (value && value.innerText) { - valueText = value.innerText; - } - - if (token.className.indexOf('filtered-search-token') !== -1) { - values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`); - } else { - values.push(name.innerText); - } - } else if (token.classList.contains('input-token')) { - const { isLastVisualTokenValid } = - gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); - - const input = FilteredSearchContainer.container.querySelector('.filtered-search'); - const inputValue = input && input.value; - - if (isLastVisualTokenValid) { - values.push(inputValue); - } else { - const previous = values.pop(); - values.push(`${previous}${inputValue}`); - } + if (token.className.indexOf('filtered-search-token') !== -1) { + values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`); + } else { + values.push(name.innerText); } - }); + } else if (token.classList.contains('input-token')) { + const { isLastVisualTokenValid } = + gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); - return values - .map(value => value.trim()) - .join(' '); - } + const input = FilteredSearchContainer.container.querySelector('.filtered-search'); + const inputValue = input && input.value; - static getSearchInput(filteredSearchInput) { - const inputValue = filteredSearchInput.value; - const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput); + if (isLastVisualTokenValid) { + values.push(inputValue); + } else { + const previous = values.pop(); + values.push(`${previous}${inputValue}`); + } + } + }); - return inputValue.slice(0, right); - } + return values + .map(value => value.trim()) + .join(' '); + } - static getInputSelectionPosition(input) { - const selectionStart = input.selectionStart; - let inputValue = input.value; - // Replace all spaces inside quote marks with underscores - // (will continue to match entire string until an end quote is found if any) - // This helps with matching the beginning & end of a token:key - inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_')); - - // Get the right position for the word selected - // Regex matches first space - let right = inputValue.slice(selectionStart).search(/\s/); - - if (right >= 0) { - right += selectionStart; - } else if (right < 0) { - right = inputValue.length; - } + static getSearchInput(filteredSearchInput) { + const inputValue = filteredSearchInput.value; + const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput); - // Get the left position for the word selected - // Regex matches last non-whitespace character - let left = inputValue.slice(0, right).search(/\S+$/); + return inputValue.slice(0, right); + } - if (selectionStart === 0) { - left = 0; - } else if (selectionStart === inputValue.length && left < 0) { - left = inputValue.length; - } else if (left < 0) { - left = selectionStart; - } + static getInputSelectionPosition(input) { + const selectionStart = input.selectionStart; + let inputValue = input.value; + // Replace all spaces inside quote marks with underscores + // (will continue to match entire string until an end quote is found if any) + // This helps with matching the beginning & end of a token:key + inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_')); + + // Get the right position for the word selected + // Regex matches first space + let right = inputValue.slice(selectionStart).search(/\s/); + + if (right >= 0) { + right += selectionStart; + } else if (right < 0) { + right = inputValue.length; + } + + // Get the left position for the word selected + // Regex matches last non-whitespace character + let left = inputValue.slice(0, right).search(/\S+$/); - return { - left, - right, - }; + if (selectionStart === 0) { + left = 0; + } else if (selectionStart === inputValue.length && left < 0) { + left = inputValue.length; + } else if (left < 0) { + left = selectionStart; } + + return { + left, + right, + }; } +} - window.gl = window.gl || {}; - gl.DropdownUtils = DropdownUtils; -})(); +window.gl = window.gl || {}; +gl.DropdownUtils = DropdownUtils; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js index d58eeeebf81..4209ca0d6e2 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js @@ -1,124 +1,122 @@ -(() => { - const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; - - class FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter) { - this.droplab = droplab; - this.hookId = input && input.id; - this.input = input; - this.filter = filter; - this.dropdown = dropdown; - this.loadingTemplate = `<div class="filter-dropdown-loading"> - <i class="fa fa-spinner fa-spin"></i> - </div>`; - this.bindEvents(); - } - - bindEvents() { - this.itemClickedWrapper = this.itemClicked.bind(this); - this.dropdown.addEventListener('click.dl', this.itemClickedWrapper); - } +const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; + +class FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + this.droplab = droplab; + this.hookId = input && input.id; + this.input = input; + this.filter = filter; + this.dropdown = dropdown; + this.loadingTemplate = `<div class="filter-dropdown-loading"> + <i class="fa fa-spinner fa-spin"></i> + </div>`; + this.bindEvents(); + } - unbindEvents() { - this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); - } + bindEvents() { + this.itemClickedWrapper = this.itemClicked.bind(this); + this.dropdown.addEventListener('click.dl', this.itemClickedWrapper); + } - getCurrentHook() { - return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null; - } + unbindEvents() { + this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); + } - itemClicked(e, getValueFunction) { - const { selected } = e.detail; + getCurrentHook() { + return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null; + } - if (selected.tagName === 'LI' && selected.innerHTML) { - const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected); + itemClicked(e, getValueFunction) { + const { selected } = e.detail; - if (!dataValueSet) { - const value = getValueFunction(selected); - gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true); - } + if (selected.tagName === 'LI' && selected.innerHTML) { + const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected); - this.resetFilters(); - this.dismissDropdown(); - this.dispatchInputEvent(); + if (!dataValueSet) { + const value = getValueFunction(selected); + gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true); } - } - setAsDropdown() { - this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`); + this.resetFilters(); + this.dismissDropdown(); + this.dispatchInputEvent(); } + } - setOffset(offset = 0) { - if (window.innerWidth > 480) { - this.dropdown.style.left = `${offset}px`; - } else { - this.dropdown.style.left = '0px'; - } + setAsDropdown() { + this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`); + } + + setOffset(offset = 0) { + if (window.innerWidth > 480) { + this.dropdown.style.left = `${offset}px`; + } else { + this.dropdown.style.left = '0px'; } + } - renderContent(forceShowList = false) { - const currentHook = this.getCurrentHook(); - if (forceShowList && currentHook && currentHook.list.hidden) { - currentHook.list.show(); - } + renderContent(forceShowList = false) { + const currentHook = this.getCurrentHook(); + if (forceShowList && currentHook && currentHook.list.hidden) { + currentHook.list.show(); } + } - render(forceRenderContent = false, forceShowList = false) { - this.setAsDropdown(); + render(forceRenderContent = false, forceShowList = false) { + this.setAsDropdown(); - const currentHook = this.getCurrentHook(); - const firstTimeInitialized = currentHook === null; + const currentHook = this.getCurrentHook(); + const firstTimeInitialized = currentHook === null; - if (firstTimeInitialized || forceRenderContent) { - this.renderContent(forceShowList); - } else if (currentHook.list.list.id !== this.dropdown.id) { - this.renderContent(forceShowList); - } + if (firstTimeInitialized || forceRenderContent) { + this.renderContent(forceShowList); + } else if (currentHook.list.list.id !== this.dropdown.id) { + this.renderContent(forceShowList); } + } - dismissDropdown() { - // Focusing on the input will dismiss dropdown - // (default droplab functionality) - this.input.focus(); - } + dismissDropdown() { + // Focusing on the input will dismiss dropdown + // (default droplab functionality) + this.input.focus(); + } - dispatchInputEvent() { - // Propogate input change to FilteredSearchDropdownManager - // so that it can determine which dropdowns to open - this.input.dispatchEvent(new CustomEvent('input', { - bubbles: true, - cancelable: true, - })); - } + dispatchInputEvent() { + // Propogate input change to FilteredSearchDropdownManager + // so that it can determine which dropdowns to open + this.input.dispatchEvent(new CustomEvent('input', { + bubbles: true, + cancelable: true, + })); + } - dispatchFormSubmitEvent() { - // dispatchEvent() is necessary as form.submit() does not - // trigger event handlers - this.input.form.dispatchEvent(new Event('submit')); - } + dispatchFormSubmitEvent() { + // dispatchEvent() is necessary as form.submit() does not + // trigger event handlers + this.input.form.dispatchEvent(new Event('submit')); + } - hideDropdown() { - const currentHook = this.getCurrentHook(); - if (currentHook) { - currentHook.list.hide(); - } + hideDropdown() { + const currentHook = this.getCurrentHook(); + if (currentHook) { + currentHook.list.hide(); } + } - resetFilters() { - const hook = this.getCurrentHook(); - - if (hook) { - const data = hook.list.data || []; - const results = data.map((o) => { - const updated = o; - updated.droplab_hidden = false; - return updated; - }); - hook.list.render(results); - } + resetFilters() { + const hook = this.getCurrentHook(); + + if (hook) { + const data = hook.list.data || []; + const results = data.map((o) => { + const updated = o; + updated.droplab_hidden = false; + return updated; + }); + hook.list.render(results); } } +} - window.gl = window.gl || {}; - gl.FilteredSearchDropdown = FilteredSearchDropdown; -})(); +window.gl = window.gl || {}; +gl.FilteredSearchDropdown = FilteredSearchDropdown; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index ec481b9ef97..49a6cd1ac77 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -1,191 +1,189 @@ import DropLab from '~/droplab/drop_lab'; import FilteredSearchContainer from './container'; -(() => { - class FilteredSearchDropdownManager { - constructor(baseEndpoint = '', page) { - this.container = FilteredSearchContainer.container; - this.baseEndpoint = baseEndpoint.replace(/\/$/, ''); - this.tokenizer = gl.FilteredSearchTokenizer; - this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; - this.filteredSearchInput = this.container.querySelector('.filtered-search'); - this.page = page; - - this.setupMapping(); - - this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('beforeunload', this.cleanupWrapper); +class FilteredSearchDropdownManager { + constructor(baseEndpoint = '', page) { + this.container = FilteredSearchContainer.container; + this.baseEndpoint = baseEndpoint.replace(/\/$/, ''); + this.tokenizer = gl.FilteredSearchTokenizer; + this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; + this.filteredSearchInput = this.container.querySelector('.filtered-search'); + this.page = page; + + this.setupMapping(); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('beforeunload', this.cleanupWrapper); + } + + cleanup() { + if (this.droplab) { + this.droplab.destroy(); + this.droplab = null; } - cleanup() { - if (this.droplab) { - this.droplab.destroy(); - this.droplab = null; - } + this.setupMapping(); - this.setupMapping(); + document.removeEventListener('beforeunload', this.cleanupWrapper); + } - document.removeEventListener('beforeunload', this.cleanupWrapper); - } + setupMapping() { + this.mapping = { + author: { + reference: null, + gl: 'DropdownUser', + element: this.container.querySelector('#js-dropdown-author'), + }, + assignee: { + reference: null, + gl: 'DropdownUser', + element: this.container.querySelector('#js-dropdown-assignee'), + }, + milestone: { + reference: null, + gl: 'DropdownNonUser', + extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'], + element: this.container.querySelector('#js-dropdown-milestone'), + }, + label: { + reference: null, + gl: 'DropdownNonUser', + extraArguments: [`${this.baseEndpoint}/labels.json`, '~'], + element: this.container.querySelector('#js-dropdown-label'), + }, + hint: { + reference: null, + gl: 'DropdownHint', + element: this.container.querySelector('#js-dropdown-hint'), + }, + }; + } - setupMapping() { - this.mapping = { - author: { - reference: null, - gl: 'DropdownUser', - element: this.container.querySelector('#js-dropdown-author'), - }, - assignee: { - reference: null, - gl: 'DropdownUser', - element: this.container.querySelector('#js-dropdown-assignee'), - }, - milestone: { - reference: null, - gl: 'DropdownNonUser', - extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'], - element: this.container.querySelector('#js-dropdown-milestone'), - }, - label: { - reference: null, - gl: 'DropdownNonUser', - extraArguments: [`${this.baseEndpoint}/labels.json`, '~'], - element: this.container.querySelector('#js-dropdown-label'), - }, - hint: { - reference: null, - gl: 'DropdownHint', - element: this.container.querySelector('#js-dropdown-hint'), - }, - }; + static addWordToInput(tokenName, tokenValue = '', clicked = false) { + const input = FilteredSearchContainer.container.querySelector('.filtered-search'); + + gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue); + input.value = ''; + + if (clicked) { + gl.FilteredSearchVisualTokens.moveInputToTheRight(); } + } - static addWordToInput(tokenName, tokenValue = '', clicked = false) { - const input = FilteredSearchContainer.container.querySelector('.filtered-search'); + updateCurrentDropdownOffset() { + this.updateDropdownOffset(this.currentDropdown); + } - gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue); - input.value = ''; + updateDropdownOffset(key) { + // Always align dropdown with the input field + let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left; - if (clicked) { - gl.FilteredSearchVisualTokens.moveInputToTheRight(); - } - } + const maxInputWidth = 240; + const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth; - updateCurrentDropdownOffset() { - this.updateDropdownOffset(this.currentDropdown); + // Make sure offset never exceeds the input container + const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth; + if (offsetMaxWidth < offset) { + offset = offsetMaxWidth; } - updateDropdownOffset(key) { - // Always align dropdown with the input field - let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left; + this.mapping[key].reference.setOffset(offset); + } - const maxInputWidth = 240; - const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth; + load(key, firstLoad = false) { + const mappingKey = this.mapping[key]; + const glClass = mappingKey.gl; + const element = mappingKey.element; + let forceShowList = false; - // Make sure offset never exceeds the input container - const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth; - if (offsetMaxWidth < offset) { - offset = offsetMaxWidth; - } + if (!mappingKey.reference) { + const dl = this.droplab; + const defaultArguments = [null, dl, element, this.filteredSearchInput, key]; + const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); - this.mapping[key].reference.setOffset(offset); + // Passing glArguments to `new gl[glClass](<arguments>)` + mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))(); } - load(key, firstLoad = false) { - const mappingKey = this.mapping[key]; - const glClass = mappingKey.gl; - const element = mappingKey.element; - let forceShowList = false; - - if (!mappingKey.reference) { - const dl = this.droplab; - const defaultArguments = [null, dl, element, this.filteredSearchInput, key]; - const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); + if (firstLoad) { + mappingKey.reference.init(); + } - // Passing glArguments to `new gl[glClass](<arguments>)` - mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))(); - } + if (this.currentDropdown === 'hint') { + // Force the dropdown to show if it was clicked from the hint dropdown + forceShowList = true; + } - if (firstLoad) { - mappingKey.reference.init(); - } + this.updateDropdownOffset(key); + mappingKey.reference.render(firstLoad, forceShowList); - if (this.currentDropdown === 'hint') { - // Force the dropdown to show if it was clicked from the hint dropdown - forceShowList = true; - } + this.currentDropdown = key; + } - this.updateDropdownOffset(key); - mappingKey.reference.render(firstLoad, forceShowList); + loadDropdown(dropdownName = '') { + let firstLoad = false; - this.currentDropdown = key; + if (!this.droplab) { + firstLoad = true; + this.droplab = new DropLab(); } - loadDropdown(dropdownName = '') { - let firstLoad = false; + const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); + const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key + && this.mapping[match.key]; + const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; - if (!this.droplab) { - firstLoad = true; - this.droplab = new DropLab(); - } + if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { + const key = match && match.key ? match.key : 'hint'; + this.load(key, firstLoad); + } + } - const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); - const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key - && this.mapping[match.key]; - const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; + setDropdown() { + const query = gl.DropdownUtils.getSearchQuery(true); + const { lastToken, searchToken } = this.tokenizer.processTokens(query); - if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { - const key = match && match.key ? match.key : 'hint'; - this.load(key, firstLoad); - } + if (this.currentDropdown) { + this.updateCurrentDropdownOffset(); } - setDropdown() { - const query = gl.DropdownUtils.getSearchQuery(true); - const { lastToken, searchToken } = this.tokenizer.processTokens(query); - - if (this.currentDropdown) { - this.updateCurrentDropdownOffset(); - } - - if (lastToken === searchToken && lastToken !== null) { - // Token is not fully initialized yet because it has no value - // Eg. token = 'label:' - - const split = lastToken.split(':'); - const dropdownName = split[0].split(' ').last(); - this.loadDropdown(split.length > 1 ? dropdownName : ''); - } else if (lastToken) { - // Token has been initialized into an object because it has a value - this.loadDropdown(lastToken.key); - } else { - this.loadDropdown('hint'); - } + if (lastToken === searchToken && lastToken !== null) { + // Token is not fully initialized yet because it has no value + // Eg. token = 'label:' + + const split = lastToken.split(':'); + const dropdownName = split[0].split(' ').last(); + this.loadDropdown(split.length > 1 ? dropdownName : ''); + } else if (lastToken) { + // Token has been initialized into an object because it has a value + this.loadDropdown(lastToken.key); + } else { + this.loadDropdown('hint'); } + } - resetDropdowns() { - if (!this.currentDropdown) { - return; - } + resetDropdowns() { + if (!this.currentDropdown) { + return; + } - // Force current dropdown to hide - this.mapping[this.currentDropdown].reference.hideDropdown(); + // Force current dropdown to hide + this.mapping[this.currentDropdown].reference.hideDropdown(); - // Re-Load dropdown - this.setDropdown(); + // Re-Load dropdown + this.setDropdown(); - // Reset filters for current dropdown - this.mapping[this.currentDropdown].reference.resetFilters(); + // Reset filters for current dropdown + this.mapping[this.currentDropdown].reference.resetFilters(); - // Reposition dropdown so that it is aligned with cursor - this.updateDropdownOffset(this.currentDropdown); - } + // Reposition dropdown so that it is aligned with cursor + this.updateDropdownOffset(this.currentDropdown); + } - destroyDroplab() { - this.droplab.destroy(); - } + destroyDroplab() { + this.droplab.destroy(); } +} - window.gl = window.gl || {}; - gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager; -})(); +window.gl = window.gl || {}; +gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index b93a8f1d322..a5eb33dd9de 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -6,489 +6,487 @@ import RecentSearchesStore from './stores/recent_searches_store'; import RecentSearchesService from './services/recent_searches_service'; import eventHub from './event_hub'; -(() => { - class FilteredSearchManager { - constructor(page) { - this.container = FilteredSearchContainer.container; - this.filteredSearchInput = this.container.querySelector('.filtered-search'); - this.filteredSearchInputForm = this.filteredSearchInput.form; - this.clearSearchButton = this.container.querySelector('.clear-search'); - this.tokensContainer = this.container.querySelector('.tokens-container'); - this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; - - this.recentSearchesStore = new RecentSearchesStore(); - let recentSearchesKey = 'issue-recent-searches'; - if (page === 'merge_requests') { - recentSearchesKey = 'merge-request-recent-searches'; - } - this.recentSearchesService = new RecentSearchesService(recentSearchesKey); - - // Fetch recent searches from localStorage - this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch() - .catch(() => { - // eslint-disable-next-line no-new - new Flash('An error occured while parsing recent searches'); - // Gracefully fail to empty array - return []; - }) - .then((searches) => { - // Put any searches that may have come in before - // we fetched the saved searches ahead of the already saved ones - const resultantSearches = this.recentSearchesStore.setRecentSearches( - this.recentSearchesStore.state.recentSearches.concat(searches), - ); - this.recentSearchesService.save(resultantSearches); - }); - - if (this.filteredSearchInput) { - this.tokenizer = gl.FilteredSearchTokenizer; - this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page); - - this.recentSearchesRoot = new RecentSearchesRoot( - this.recentSearchesStore, - this.recentSearchesService, - document.querySelector('.js-filtered-search-history-dropdown'), +class FilteredSearchManager { + constructor(page) { + this.container = FilteredSearchContainer.container; + this.filteredSearchInput = this.container.querySelector('.filtered-search'); + this.filteredSearchInputForm = this.filteredSearchInput.form; + this.clearSearchButton = this.container.querySelector('.clear-search'); + this.tokensContainer = this.container.querySelector('.tokens-container'); + this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; + + this.recentSearchesStore = new RecentSearchesStore(); + let recentSearchesKey = 'issue-recent-searches'; + if (page === 'merge_requests') { + recentSearchesKey = 'merge-request-recent-searches'; + } + this.recentSearchesService = new RecentSearchesService(recentSearchesKey); + + // Fetch recent searches from localStorage + this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch() + .catch(() => { + // eslint-disable-next-line no-new + new Flash('An error occured while parsing recent searches'); + // Gracefully fail to empty array + return []; + }) + .then((searches) => { + // Put any searches that may have come in before + // we fetched the saved searches ahead of the already saved ones + const resultantSearches = this.recentSearchesStore.setRecentSearches( + this.recentSearchesStore.state.recentSearches.concat(searches), ); - this.recentSearchesRoot.init(); + this.recentSearchesService.save(resultantSearches); + }); - this.bindEvents(); - this.loadSearchParamsFromURL(); - this.dropdownManager.setDropdown(); + if (this.filteredSearchInput) { + this.tokenizer = gl.FilteredSearchTokenizer; + this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page); - this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('beforeunload', this.cleanupWrapper); - } - } + this.recentSearchesRoot = new RecentSearchesRoot( + this.recentSearchesStore, + this.recentSearchesService, + document.querySelector('.js-filtered-search-history-dropdown'), + ); + this.recentSearchesRoot.init(); - cleanup() { - this.unbindEvents(); - document.removeEventListener('beforeunload', this.cleanupWrapper); + this.bindEvents(); + this.loadSearchParamsFromURL(); + this.dropdownManager.setDropdown(); - if (this.recentSearchesRoot) { - this.recentSearchesRoot.destroy(); - } + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('beforeunload', this.cleanupWrapper); } + } - bindEvents() { - this.handleFormSubmit = this.handleFormSubmit.bind(this); - this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); - this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); - this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this); - this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this); - this.checkForEnterWrapper = this.checkForEnter.bind(this); - this.onClearSearchWrapper = this.onClearSearch.bind(this); - this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); - this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this); - 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.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this); - - this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit); - this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); - this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); - this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper); - this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper); - this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); - 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.onClearSearchWrapper); - document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); - document.addEventListener('click', this.unselectEditTokensWrapper); - document.addEventListener('click', this.removeInputContainerFocusWrapper); - document.addEventListener('keydown', this.removeSelectedTokenWrapper); - eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); - } + cleanup() { + this.unbindEvents(); + document.removeEventListener('beforeunload', this.cleanupWrapper); - unbindEvents() { - this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit); - this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); - this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); - this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper); - this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper); - this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); - 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.onClearSearchWrapper); - document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); - document.removeEventListener('click', this.unselectEditTokensWrapper); - document.removeEventListener('click', this.removeInputContainerFocusWrapper); - document.removeEventListener('keydown', this.removeSelectedTokenWrapper); - eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); + if (this.recentSearchesRoot) { + this.recentSearchesRoot.destroy(); } + } - checkForBackspace(e) { - // 8 = Backspace Key - // 46 = Delete Key - if (e.keyCode === 8 || e.keyCode === 46) { - const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + bindEvents() { + this.handleFormSubmit = this.handleFormSubmit.bind(this); + this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); + this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); + this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this); + this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this); + this.checkForEnterWrapper = this.checkForEnter.bind(this); + this.onClearSearchWrapper = this.onClearSearch.bind(this); + this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); + this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this); + 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.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this); + + this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit); + this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); + this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); + this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper); + this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper); + this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); + 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.onClearSearchWrapper); + document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); + document.addEventListener('click', this.unselectEditTokensWrapper); + document.addEventListener('click', this.removeInputContainerFocusWrapper); + document.addEventListener('keydown', this.removeSelectedTokenWrapper); + eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); + } - if (this.filteredSearchInput.value === '' && lastVisualToken) { - this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); - gl.FilteredSearchVisualTokens.removeLastTokenPartial(); - } + unbindEvents() { + this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit); + this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); + this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); + this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper); + this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper); + this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); + 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.onClearSearchWrapper); + document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); + document.removeEventListener('click', this.unselectEditTokensWrapper); + document.removeEventListener('click', this.removeInputContainerFocusWrapper); + document.removeEventListener('keydown', this.removeSelectedTokenWrapper); + eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); + } + + checkForBackspace(e) { + // 8 = Backspace Key + // 46 = Delete Key + if (e.keyCode === 8 || e.keyCode === 46) { + const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); - // Reposition dropdown so that it is aligned with cursor - this.dropdownManager.updateCurrentDropdownOffset(); + if (this.filteredSearchInput.value === '' && lastVisualToken) { + this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); } - } - checkForEnter(e) { - if (e.keyCode === 38 || e.keyCode === 40) { - const selectionStart = this.filteredSearchInput.selectionStart; + // Reposition dropdown so that it is aligned with cursor + this.dropdownManager.updateCurrentDropdownOffset(); + } + } - e.preventDefault(); - this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart); - } + checkForEnter(e) { + if (e.keyCode === 38 || e.keyCode === 40) { + const selectionStart = this.filteredSearchInput.selectionStart; - if (e.keyCode === 13) { - const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; - const dropdownEl = dropdown.element; - const activeElements = dropdownEl.querySelectorAll('.droplab-item-active'); + e.preventDefault(); + this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart); + } - e.preventDefault(); + if (e.keyCode === 13) { + const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; + const dropdownEl = dropdown.element; + const activeElements = dropdownEl.querySelectorAll('.droplab-item-active'); - if (!activeElements.length) { - if (this.isHandledAsync) { - e.stopImmediatePropagation(); + e.preventDefault(); - this.filteredSearchInput.blur(); - this.dropdownManager.resetDropdowns(); - } else { - // Prevent droplab from opening dropdown - this.dropdownManager.destroyDroplab(); - } + if (!activeElements.length) { + if (this.isHandledAsync) { + e.stopImmediatePropagation(); - this.search(); + this.filteredSearchInput.blur(); + this.dropdownManager.resetDropdowns(); + } else { + // Prevent droplab from opening dropdown + this.dropdownManager.destroyDroplab(); } + + this.search(); } } + } - addInputContainerFocus() { - const inputContainer = this.filteredSearchInput.closest('.filtered-search-box'); + addInputContainerFocus() { + const inputContainer = this.filteredSearchInput.closest('.filtered-search-box'); - if (inputContainer) { - inputContainer.classList.add('focus'); - } + if (inputContainer) { + inputContainer.classList.add('focus'); } + } - removeInputContainerFocus(e) { - const inputContainer = this.filteredSearchInput.closest('.filtered-search-box'); - const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); - const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null; - const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null; + removeInputContainerFocus(e) { + const inputContainer = this.filteredSearchInput.closest('.filtered-search-box'); + 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'); - } + if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown && + !isElementInStaticFilterDropdown && inputContainer) { + inputContainer.classList.remove('focus'); } + } - static selectToken(e) { - const button = e.target.closest('.selectable'); + static selectToken(e) { + const button = e.target.closest('.selectable'); - if (button) { - e.preventDefault(); - e.stopPropagation(); - gl.FilteredSearchVisualTokens.selectToken(button); - } + if (button) { + e.preventDefault(); + e.stopPropagation(); + gl.FilteredSearchVisualTokens.selectToken(button); } + } - unselectEditTokens(e) { - const inputContainer = this.container.querySelector('.filtered-search-box'); - const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); - const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null; - const isElementTokensContainer = e.target.classList.contains('tokens-container'); + unselectEditTokens(e) { + const inputContainer = this.container.querySelector('.filtered-search-box'); + const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); + const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null; + const isElementTokensContainer = e.target.classList.contains('tokens-container'); - if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) { - gl.FilteredSearchVisualTokens.moveInputToTheRight(); - this.dropdownManager.resetDropdowns(); - } + if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) { + gl.FilteredSearchVisualTokens.moveInputToTheRight(); + this.dropdownManager.resetDropdowns(); } + } - editToken(e) { - const token = e.target.closest('.js-visual-token'); + editToken(e) { + const token = e.target.closest('.js-visual-token'); - if (token) { - gl.FilteredSearchVisualTokens.editToken(token); - this.tokenChange(); - } + if (token) { + gl.FilteredSearchVisualTokens.editToken(token); + this.tokenChange(); } + } - toggleClearSearchButton() { - const query = gl.DropdownUtils.getSearchQuery(); - const hidden = 'hidden'; - const hasHidden = this.clearSearchButton.classList.contains(hidden); + toggleClearSearchButton() { + const query = gl.DropdownUtils.getSearchQuery(); + const hidden = 'hidden'; + const hasHidden = this.clearSearchButton.classList.contains(hidden); - if (query.length === 0 && !hasHidden) { - this.clearSearchButton.classList.add(hidden); - } else if (query.length && hasHidden) { - this.clearSearchButton.classList.remove(hidden); - } + if (query.length === 0 && !hasHidden) { + this.clearSearchButton.classList.add(hidden); + } else if (query.length && hasHidden) { + this.clearSearchButton.classList.remove(hidden); } + } - handleInputPlaceholder() { - const query = gl.DropdownUtils.getSearchQuery(); - const placeholder = 'Search or filter results...'; - const currentPlaceholder = this.filteredSearchInput.placeholder; + handleInputPlaceholder() { + const query = gl.DropdownUtils.getSearchQuery(); + const placeholder = 'Search or filter results...'; + const currentPlaceholder = this.filteredSearchInput.placeholder; - if (query.length === 0 && currentPlaceholder !== placeholder) { - this.filteredSearchInput.placeholder = placeholder; - } else if (query.length > 0 && currentPlaceholder !== '') { - this.filteredSearchInput.placeholder = ''; - } + if (query.length === 0 && currentPlaceholder !== placeholder) { + this.filteredSearchInput.placeholder = placeholder; + } else if (query.length > 0 && currentPlaceholder !== '') { + this.filteredSearchInput.placeholder = ''; } + } - removeSelectedToken(e) { - // 8 = Backspace Key - // 46 = Delete Key - if (e.keyCode === 8 || e.keyCode === 46) { - gl.FilteredSearchVisualTokens.removeSelectedToken(); - this.handleInputPlaceholder(); - this.toggleClearSearchButton(); - } + removeSelectedToken(e) { + // 8 = Backspace Key + // 46 = Delete Key + if (e.keyCode === 8 || e.keyCode === 46) { + gl.FilteredSearchVisualTokens.removeSelectedToken(); + this.handleInputPlaceholder(); + this.toggleClearSearchButton(); } + } - onClearSearch(e) { - e.preventDefault(); - this.clearSearch(); - } + onClearSearch(e) { + e.preventDefault(); + this.clearSearch(); + } - clearSearch() { - this.filteredSearchInput.value = ''; + clearSearch() { + this.filteredSearchInput.value = ''; - const removeElements = []; + const removeElements = []; - [].forEach.call(this.tokensContainer.children, (t) => { - if (t.classList.contains('js-visual-token')) { - removeElements.push(t); - } - }); + [].forEach.call(this.tokensContainer.children, (t) => { + if (t.classList.contains('js-visual-token')) { + removeElements.push(t); + } + }); - removeElements.forEach((el) => { - el.parentElement.removeChild(el); - }); + removeElements.forEach((el) => { + el.parentElement.removeChild(el); + }); - this.clearSearchButton.classList.add('hidden'); - this.handleInputPlaceholder(); + this.clearSearchButton.classList.add('hidden'); + this.handleInputPlaceholder(); - this.dropdownManager.resetDropdowns(); + this.dropdownManager.resetDropdowns(); - if (this.isHandledAsync) { - this.search(); - } + if (this.isHandledAsync) { + this.search(); } + } - handleInputVisualToken() { - const input = this.filteredSearchInput; - const { tokens, searchToken } - = gl.FilteredSearchTokenizer.processTokens(input.value); - const { isLastVisualTokenValid } - = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); - - if (isLastVisualTokenValid) { - tokens.forEach((t) => { - input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, ''); - gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`); - }); - - const fragments = searchToken.split(':'); - if (fragments.length > 1) { - const inputValues = fragments[0].split(' '); - const tokenKey = inputValues.last(); - - if (inputValues.length > 1) { - inputValues.pop(); - const searchTerms = inputValues.join(' '); - - input.value = input.value.replace(searchTerms, ''); - gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms); - } + handleInputVisualToken() { + const input = this.filteredSearchInput; + const { tokens, searchToken } + = gl.FilteredSearchTokenizer.processTokens(input.value); + const { isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (isLastVisualTokenValid) { + tokens.forEach((t) => { + input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, ''); + gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`); + }); - gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey); - input.value = input.value.replace(`${tokenKey}:`, ''); - } - } else { - // Keep listening to token until we determine that the user is done typing the token value - const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g; + const fragments = searchToken.split(':'); + if (fragments.length > 1) { + const inputValues = fragments[0].split(' '); + const tokenKey = inputValues.last(); - if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') { - gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken); + if (inputValues.length > 1) { + inputValues.pop(); + const searchTerms = inputValues.join(' '); - // Trim the last space as seen in the if statement above - input.value = input.value.replace(searchToken, '').trim(); + input.value = input.value.replace(searchTerms, ''); + gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms); } + + gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey); + input.value = input.value.replace(`${tokenKey}:`, ''); } - } + } else { + // Keep listening to token until we determine that the user is done typing the token value + const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g; - handleFormSubmit(e) { - e.preventDefault(); - this.search(); - } + if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') { + gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken); - saveCurrentSearchQuery() { - // Don't save before we have fetched the already saved searches - this.fetchingRecentSearchesPromise.then(() => { - const searchQuery = gl.DropdownUtils.getSearchQuery(); - if (searchQuery.length > 0) { - const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery); - this.recentSearchesService.save(resultantSearches); - } - }); + // Trim the last space as seen in the if statement above + input.value = input.value.replace(searchToken, '').trim(); + } } + } - loadSearchParamsFromURL() { - const params = gl.utils.getUrlParamsArray(); - const usernameParams = this.getUsernameParams(); - let hasFilteredSearch = false; + handleFormSubmit(e) { + e.preventDefault(); + this.search(); + } - params.forEach((p) => { - const split = p.split('='); - const keyParam = decodeURIComponent(split[0]); - const value = split[1]; + saveCurrentSearchQuery() { + // Don't save before we have fetched the already saved searches + this.fetchingRecentSearchesPromise.then(() => { + const searchQuery = gl.DropdownUtils.getSearchQuery(); + if (searchQuery.length > 0) { + const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery); + this.recentSearchesService.save(resultantSearches); + } + }); + } - // Check if it matches edge conditions listed in this.filteredSearchTokenKeys - const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p); + loadSearchParamsFromURL() { + const params = gl.utils.getUrlParamsArray(); + const usernameParams = this.getUsernameParams(); + let hasFilteredSearch = false; - if (condition) { - hasFilteredSearch = true; - gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value); - } else { - // Sanitize value since URL converts spaces into + - // Replace before decode so that we know what was originally + versus the encoded + - const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; - const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam); - - if (match) { - const indexOf = keyParam.indexOf('_'); - const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam; - const symbol = match.symbol; - let quotationsToUse = ''; - - if (sanitizedValue.indexOf(' ') !== -1) { - // Prefer ", but use ' if required - quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; - } + params.forEach((p) => { + const split = p.split('='); + const keyParam = decodeURIComponent(split[0]); + const value = split[1]; + + // Check if it matches edge conditions listed in this.filteredSearchTokenKeys + const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p); + if (condition) { + hasFilteredSearch = true; + gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value); + } else { + // Sanitize value since URL converts spaces into + + // Replace before decode so that we know what was originally + versus the encoded + + const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; + const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam); + + if (match) { + const indexOf = keyParam.indexOf('_'); + const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam; + const symbol = match.symbol; + let quotationsToUse = ''; + + if (sanitizedValue.indexOf(' ') !== -1) { + // Prefer ", but use ' if required + quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; + } + + hasFilteredSearch = true; + gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); + } else if (!match && keyParam === 'assignee_id') { + const id = parseInt(value, 10); + if (usernameParams[id]) { hasFilteredSearch = true; - gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); - } else if (!match && keyParam === 'assignee_id') { - const id = parseInt(value, 10); - if (usernameParams[id]) { - hasFilteredSearch = true; - gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`); - } - } else if (!match && keyParam === 'author_id') { - const id = parseInt(value, 10); - if (usernameParams[id]) { - hasFilteredSearch = true; - gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`); - } - } else if (!match && keyParam === 'search') { + gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`); + } + } else if (!match && keyParam === 'author_id') { + const id = parseInt(value, 10); + if (usernameParams[id]) { hasFilteredSearch = true; - this.filteredSearchInput.value = sanitizedValue; + gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`); } + } else if (!match && keyParam === 'search') { + hasFilteredSearch = true; + this.filteredSearchInput.value = sanitizedValue; } - }); + } + }); - this.saveCurrentSearchQuery(); + this.saveCurrentSearchQuery(); - if (hasFilteredSearch) { - this.clearSearchButton.classList.remove('hidden'); - this.handleInputPlaceholder(); - } + if (hasFilteredSearch) { + this.clearSearchButton.classList.remove('hidden'); + this.handleInputPlaceholder(); } + } - search() { - const paths = []; - const searchQuery = gl.DropdownUtils.getSearchQuery(); - - this.saveCurrentSearchQuery(); + search() { + const paths = []; + const searchQuery = gl.DropdownUtils.getSearchQuery(); - const { tokens, searchToken } - = this.tokenizer.processTokens(searchQuery); - const currentState = gl.utils.getParameterByName('state') || 'opened'; - paths.push(`state=${currentState}`); + this.saveCurrentSearchQuery(); - tokens.forEach((token) => { - const condition = this.filteredSearchTokenKeys - .searchByConditionKeyValue(token.key, token.value.toLowerCase()); - const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {}; - const keyParam = param ? `${token.key}_${param}` : token.key; - let tokenPath = ''; + const { tokens, searchToken } + = this.tokenizer.processTokens(searchQuery); + const currentState = gl.utils.getParameterByName('state') || 'opened'; + paths.push(`state=${currentState}`); - if (condition) { - tokenPath = condition.url; - } else { - let tokenValue = token.value; + tokens.forEach((token) => { + const condition = this.filteredSearchTokenKeys + .searchByConditionKeyValue(token.key, token.value.toLowerCase()); + const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {}; + const keyParam = param ? `${token.key}_${param}` : token.key; + let tokenPath = ''; - if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || - (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { - tokenValue = tokenValue.slice(1, tokenValue.length - 1); - } + if (condition) { + tokenPath = condition.url; + } else { + let tokenValue = token.value; - tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`; + if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || + (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { + tokenValue = tokenValue.slice(1, tokenValue.length - 1); } - paths.push(tokenPath); - }); - - if (searchToken) { - const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+'); - paths.push(`search=${sanitized}`); + tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`; } - const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`; + paths.push(tokenPath); + }); - if (this.updateObject) { - this.updateObject(parameterizedUrl); - } else { - gl.utils.visitUrl(parameterizedUrl); - } + if (searchToken) { + const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+'); + paths.push(`search=${sanitized}`); } - getUsernameParams() { - const usernamesById = {}; - try { - const attribute = this.filteredSearchInput.getAttribute('data-username-params'); - JSON.parse(attribute).forEach((user) => { - usernamesById[user.id] = user.username; - }); - } catch (e) { - // do nothing - } - return usernamesById; + const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`; + + if (this.updateObject) { + this.updateObject(parameterizedUrl); + } else { + gl.utils.visitUrl(parameterizedUrl); } + } - tokenChange() { - const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; + getUsernameParams() { + const usernamesById = {}; + try { + const attribute = this.filteredSearchInput.getAttribute('data-username-params'); + JSON.parse(attribute).forEach((user) => { + usernamesById[user.id] = user.username; + }); + } catch (e) { + // do nothing + } + return usernamesById; + } - if (dropdown) { - const currentDropdownRef = dropdown.reference; + tokenChange() { + const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; - this.setDropdownWrapper(); - currentDropdownRef.dispatchInputEvent(); - } - } + if (dropdown) { + const currentDropdownRef = dropdown.reference; - onrecentSearchesItemSelected(text) { - this.clearSearch(); - this.filteredSearchInput.value = text; - this.filteredSearchInput.dispatchEvent(new CustomEvent('input')); - this.search(); + this.setDropdownWrapper(); + currentDropdownRef.dispatchInputEvent(); } } - window.gl = window.gl || {}; - gl.FilteredSearchManager = FilteredSearchManager; -})(); + onrecentSearchesItemSelected(text) { + this.clearSearch(); + this.filteredSearchInput.value = text; + this.filteredSearchInput.dispatchEvent(new CustomEvent('input')); + this.search(); + } +} + +window.gl = window.gl || {}; +gl.FilteredSearchManager = FilteredSearchManager; diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js index 6d5df86f2a5..1abad9d1b73 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -1,100 +1,98 @@ -(() => { - const tokenKeys = [{ - key: 'author', - type: 'string', - param: 'username', - symbol: '@', - }, { - key: 'assignee', - type: 'string', - param: 'username', - symbol: '@', - }, { - key: 'milestone', - type: 'string', - param: 'title', - symbol: '%', - }, { - key: 'label', - type: 'array', - param: 'name[]', - symbol: '~', - }]; +const tokenKeys = [{ + key: 'author', + type: 'string', + param: 'username', + symbol: '@', +}, { + key: 'assignee', + type: 'string', + param: 'username', + symbol: '@', +}, { + key: 'milestone', + type: 'string', + param: 'title', + symbol: '%', +}, { + key: 'label', + type: 'array', + param: 'name[]', + symbol: '~', +}]; - const alternativeTokenKeys = [{ - key: 'label', - type: 'string', - param: 'name', - symbol: '~', - }]; +const alternativeTokenKeys = [{ + key: 'label', + type: 'string', + param: 'name', + symbol: '~', +}]; - const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys); +const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys); - const conditions = [{ - url: 'assignee_id=0', - tokenKey: 'assignee', - value: 'none', - }, { - url: 'milestone_title=No+Milestone', - tokenKey: 'milestone', - value: 'none', - }, { - url: 'milestone_title=%23upcoming', - tokenKey: 'milestone', - value: 'upcoming', - }, { - url: 'milestone_title=%23started', - tokenKey: 'milestone', - value: 'started', - }, { - url: 'label_name[]=No+Label', - tokenKey: 'label', - value: 'none', - }]; +const conditions = [{ + url: 'assignee_id=0', + tokenKey: 'assignee', + value: 'none', +}, { + url: 'milestone_title=No+Milestone', + tokenKey: 'milestone', + value: 'none', +}, { + url: 'milestone_title=%23upcoming', + tokenKey: 'milestone', + value: 'upcoming', +}, { + url: 'milestone_title=%23started', + tokenKey: 'milestone', + value: 'started', +}, { + url: 'label_name[]=No+Label', + tokenKey: 'label', + value: 'none', +}]; - class FilteredSearchTokenKeys { - static get() { - return tokenKeys; - } +class FilteredSearchTokenKeys { + static get() { + return tokenKeys; + } - static getAlternatives() { - return alternativeTokenKeys; - } + static getAlternatives() { + return alternativeTokenKeys; + } - static getConditions() { - return conditions; - } + static getConditions() { + return conditions; + } - static searchByKey(key) { - return tokenKeys.find(tokenKey => tokenKey.key === key) || null; - } + static searchByKey(key) { + return tokenKeys.find(tokenKey => tokenKey.key === key) || null; + } - static searchBySymbol(symbol) { - return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; - } + static searchBySymbol(symbol) { + return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; + } - static searchByKeyParam(keyParam) { - return tokenKeysWithAlternative.find((tokenKey) => { - let tokenKeyParam = tokenKey.key; + static searchByKeyParam(keyParam) { + return tokenKeysWithAlternative.find((tokenKey) => { + let tokenKeyParam = tokenKey.key; - if (tokenKey.param) { - tokenKeyParam += `_${tokenKey.param}`; - } + if (tokenKey.param) { + tokenKeyParam += `_${tokenKey.param}`; + } - return keyParam === tokenKeyParam; - }) || null; - } + return keyParam === tokenKeyParam; + }) || null; + } - static searchByConditionUrl(url) { - return conditions.find(condition => condition.url === url) || null; - } + static searchByConditionUrl(url) { + return conditions.find(condition => condition.url === url) || null; + } - static searchByConditionKeyValue(key, value) { - return conditions - .find(condition => condition.tokenKey === key && condition.value === value) || null; - } + static searchByConditionKeyValue(key, value) { + return conditions + .find(condition => condition.tokenKey === key && condition.value === value) || null; } +} - window.gl = window.gl || {}; - gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys; -})(); +window.gl = window.gl || {}; +gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys; diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js index a2729dc0e95..2808e4b238a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js @@ -1,58 +1,56 @@ require('./filtered_search_token_keys'); -(() => { - class FilteredSearchTokenizer { - static processTokens(input) { - const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key); - // Regex extracts `(token):(symbol)(value)` - // 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 = ''; - } - - 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() || ''; - - if (tokens.length > 0) { - const last = tokens[tokens.length - 1]; - const lastString = `${last.key}:${last.symbol}${last.value}`; - lastToken = input.lastIndexOf(lastString) === - input.length - lastString.length ? last : searchToken; - } else { - lastToken = searchToken; +class FilteredSearchTokenizer { + static processTokens(input) { + const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key); + // Regex extracts `(token):(symbol)(value)` + // 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 = ''; } - return { - tokens, - lastToken, - searchToken, - }; + 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() || ''; + + if (tokens.length > 0) { + const last = tokens[tokens.length - 1]; + const lastString = `${last.key}:${last.symbol}${last.value}`; + lastToken = input.lastIndexOf(lastString) === + input.length - lastString.length ? last : searchToken; + } else { + lastToken = searchToken; } + + return { + tokens, + lastToken, + searchToken, + }; } +} - window.gl = window.gl || {}; - gl.FilteredSearchTokenizer = FilteredSearchTokenizer; -})(); +window.gl = window.gl || {}; +gl.FilteredSearchTokenizer = FilteredSearchTokenizer; diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index b6ce8e83729..4d491e70d83 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -1,26 +1,20 @@ import Vue from 'vue'; -import IssueTitle from './issue_title'; +import IssueTitle from './issue_title.vue'; import '../vue_shared/vue_resource_interceptor'; -const vueOptions = () => ({ - el: '.issue-title-entrypoint', - components: { - IssueTitle, - }, - data() { - const issueTitleData = document.querySelector('.issue-title-data').dataset; +(() => { + const issueTitleData = document.querySelector('.issue-title-data').dataset; + const { initialTitle, endpoint } = issueTitleData; - return { - initialTitle: issueTitleData.initialTitle, - endpoint: issueTitleData.endpoint, - }; - }, - template: ` - <IssueTitle - :initialTitle="initialTitle" - :endpoint="endpoint" - /> - `, -}); + const vm = new Vue({ + el: '.issue-title-entrypoint', + render: createElement => createElement(IssueTitle, { + props: { + initialTitle, + endpoint, + }, + }), + }); -(() => new Vue(vueOptions()))(); + return vm; +})(); diff --git a/app/assets/javascripts/issue_show/issue_title.js b/app/assets/javascripts/issue_show/issue_title.vue index 1184c8956dc..ba54178a310 100644 --- a/app/assets/javascripts/issue_show/issue_title.js +++ b/app/assets/javascripts/issue_show/issue_title.vue @@ -1,3 +1,4 @@ +<script> import Visibility from 'visibilityjs'; import Poll from './../lib/utils/poll'; import Service from './services/index'; @@ -72,7 +73,9 @@ export default { created() { this.fetch(); }, - template: ` - <h2 class='title' v-html='title'></h2> - `, }; +</script> + +<template> + <h2 class="title" v-html="title"></h2> +</template> diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index e1e6ca25446..01c4b9821d3 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -47,6 +47,10 @@ } }; + gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) { + return $tooltipEl.attr('title', newTitle).tooltip('fixTitle'); + }; + w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) { event_name = event_name || 'input'; var closest_submit, field, that; @@ -364,9 +368,9 @@ }); }; - w.gl.utils.setFavicon = (iconName) => { - if (faviconEl && iconName) { - faviconEl.setAttribute('href', `/assets/${iconName}.ico`); + w.gl.utils.setFavicon = (faviconPath) => { + if (faviconEl && faviconPath) { + faviconEl.setAttribute('href', faviconPath); } }; @@ -381,8 +385,8 @@ url: pageUrl, dataType: 'json', success: function(data) { - if (data && data.icon) { - gl.utils.setFavicon(`ci_favicons/${data.icon}`); + if (data && data.favicon) { + gl.utils.setFavicon(data.favicon); } else { gl.utils.resetFavicon(); } diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js new file mode 100644 index 00000000000..1e96c7ab5cd --- /dev/null +++ b/app/assets/javascripts/lib/utils/constants.js @@ -0,0 +1,2 @@ +/* eslint-disable import/prefer-default-export */ +export const BYTES_IN_KIB = 1024; diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js index e2bf69ee52e..f1b07408671 100644 --- a/app/assets/javascripts/lib/utils/number_utils.js +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -1,4 +1,4 @@ -/* eslint-disable import/prefer-default-export */ +import { BYTES_IN_KIB } from './constants'; /** * Function that allows a number with an X amount of decimals @@ -32,3 +32,13 @@ export function formatRelevantDigits(number) { } return formattedNumber; } + +/** + * Utility function that calculates KiB of the given bytes. + * + * @param {Number} number bytes + * @return {Number} KiB + */ +export function bytesToKiB(number) { + return number / BYTES_IN_KIB; +} diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index c50ec24c818..be3c2c9fbb1 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -165,6 +165,7 @@ import './syntax_highlight'; import './task_list'; import './todos'; import './tree'; +import './usage_ping'; import './user'; import './user_tabs'; import './username_validator'; @@ -210,6 +211,14 @@ $(function () { } }); + if (bootstrapBreakpoint === 'xs') { + const $rightSidebar = $('aside.right-sidebar, .page-with-sidebar'); + + $rightSidebar + .removeClass('right-sidebar-expanded') + .addClass('right-sidebar-collapsed'); + } + // prevent default action for disabled buttons $('.btn').click(function(e) { if ($(this).hasClass('disabled')) { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 15f7a813626..974fb0d83da 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -308,8 +308,10 @@ require('./task_list'); if (this.isNewNote(note)) { this.note_ids.push(note.id); - $notesList = $('ul.main-notes-list'); - $notesList.append(note.html).syntaxHighlight(); + + $notesList = window.$('ul.main-notes-list'); + Notes.animateAppendNote(note.html, $notesList); + // Update datetime format on the recent note gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false); this.collapseLongCommitList(); @@ -348,7 +350,7 @@ require('./task_list'); lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); // is this the first note of discussion? - discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); + discussionContainer = window.$(`.notes[data-discussion-id="${note.discussion_id}"]`); if (!discussionContainer.length) { discussionContainer = form.closest('.discussion').find('.notes'); } @@ -370,14 +372,13 @@ require('./task_list'); row.find(contentContainerClass + ' .content').append($notes.closest('.content').children()); } } - // Init discussion on 'Discussion' page if it is merge request page - if ($('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) { - $('ul.main-notes-list').append($(note.discussion_html).renderGFM()); + if (window.$('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) { + Notes.animateAppendNote(note.discussion_html, window.$('ul.main-notes-list')); } } else { // append new note to all matching discussions - discussionContainer.append($(note.html).renderGFM()); + Notes.animateAppendNote(note.html, discussionContainer); } if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_resolvable) { @@ -1063,6 +1064,13 @@ require('./task_list'); return $form; }; + Notes.animateAppendNote = function(noteHTML, $notesList) { + const $note = window.$(noteHTML); + + $note.addClass('fade-in').renderGFM(); + $notesList.append($note); + }; + return Notes; })(); }).call(window); diff --git a/app/assets/javascripts/vue_pipelines_index/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue index 11da6e908b7..d1c60b570de 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/async_button.vue +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -65,6 +65,8 @@ export default { makeRequest() { this.isLoading = true; + $(this.$el).tooltip('destroy'); + this.service.postAction(this.endpoint) .then(() => { this.isLoading = false; @@ -88,9 +90,13 @@ export default { :aria-label="title" data-container="body" data-placement="top" - :disabled="isLoading" - > - <i :class="iconClass" aria-hidden="true"></i> - <i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading"></i> + :disabled="isLoading"> + <i + :class="iconClass" + aria-hidden="true" /> + <i + class="fa fa-spinner fa-spin" + aria-hidden="true" + v-if="isLoading" /> </button> </template> diff --git a/app/assets/javascripts/vue_pipelines_index/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue index ba158bc4a1e..ba158bc4a1e 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/empty_state.vue diff --git a/app/assets/javascripts/vue_pipelines_index/components/error_state.vue b/app/assets/javascripts/pipelines/components/error_state.vue index 90cee68163e..90cee68163e 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/error_state.vue +++ b/app/assets/javascripts/pipelines/components/error_state.vue diff --git a/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js b/app/assets/javascripts/pipelines/components/nav_controls.js index 6aa10531034..6aa10531034 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js +++ b/app/assets/javascripts/pipelines/components/nav_controls.js diff --git a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js b/app/assets/javascripts/pipelines/components/navigation_tabs.js index 1626ae17a30..1626ae17a30 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js +++ b/app/assets/javascripts/pipelines/components/navigation_tabs.js diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js b/app/assets/javascripts/pipelines/components/pipeline_url.js index 4e183d5c8ec..4e183d5c8ec 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js +++ b/app/assets/javascripts/pipelines/components/pipeline_url.js diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js b/app/assets/javascripts/pipelines/components/pipelines_actions.js index 12d80768646..ffda18d2e0f 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.js @@ -28,6 +28,8 @@ export default { onClickAction(endpoint) { this.isLoading = true; + $(this.$refs.tooltip).tooltip('destroy'); + this.service.postAction(endpoint) .then(() => { this.isLoading = false; @@ -57,6 +59,7 @@ export default { data-toggle="dropdown" data-placement="top" aria-label="Manual job" + ref="tooltip" :disabled="isLoading"> ${playIconSvg} <i diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js b/app/assets/javascripts/pipelines/components/pipelines_artifacts.js index f18e2dfadaf..f18e2dfadaf 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.js diff --git a/app/assets/javascripts/vue_pipelines_index/components/stage.js b/app/assets/javascripts/pipelines/components/stage.js index a2c29002707..b8cc3630611 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/stage.js +++ b/app/assets/javascripts/pipelines/components/stage.js @@ -1,32 +1,11 @@ /* 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'; +import StatusIconEntityMap from '../../ci_status_icons'; 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], }; }, @@ -89,6 +68,9 @@ export default { triggerButtonClass() { return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`; }, + svgHTML() { + return StatusIconEntityMap[this.stage.status.icon]; + }, }, template: ` <div> @@ -100,7 +82,7 @@ export default { data-toggle="dropdown" type="button" :aria-label="stage.title"> - <span v-html="svg" aria-hidden="true"></span> + <span v-html="svgHTML" 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"> diff --git a/app/assets/javascripts/vue_pipelines_index/components/status.js b/app/assets/javascripts/pipelines/components/status.js index 21a281af438..21a281af438 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/status.js +++ b/app/assets/javascripts/pipelines/components/status.js diff --git a/app/assets/javascripts/vue_pipelines_index/components/time_ago.js b/app/assets/javascripts/pipelines/components/time_ago.js index 498d0715f54..498d0715f54 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/time_ago.js +++ b/app/assets/javascripts/pipelines/components/time_ago.js diff --git a/app/assets/javascripts/vue_pipelines_index/event_hub.js b/app/assets/javascripts/pipelines/event_hub.js index 0948c2e5352..0948c2e5352 100644 --- a/app/assets/javascripts/vue_pipelines_index/event_hub.js +++ b/app/assets/javascripts/pipelines/event_hub.js diff --git a/app/assets/javascripts/vue_pipelines_index/index.js b/app/assets/javascripts/pipelines/index.js index 48f9181a8d9..48f9181a8d9 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js +++ b/app/assets/javascripts/pipelines/index.js diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/pipelines/pipelines.js index 6eea4812f33..6eea4812f33 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js +++ b/app/assets/javascripts/pipelines/pipelines.js diff --git a/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js index 255cd513490..255cd513490 100644 --- a/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js +++ b/app/assets/javascripts/pipelines/services/pipelines_service.js diff --git a/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js index 377ec8ba2cc..377ec8ba2cc 100644 --- a/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js +++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js diff --git a/app/assets/javascripts/shortcuts_wiki.js b/app/assets/javascripts/shortcuts_wiki.js new file mode 100644 index 00000000000..8a075062a48 --- /dev/null +++ b/app/assets/javascripts/shortcuts_wiki.js @@ -0,0 +1,16 @@ +/* eslint-disable class-methods-use-this */ +/* global Mousetrap */ +/* global ShortcutsNavigation */ + +import findAndFollowLink from './shortcuts_dashboard_navigation'; + +export default class ShortcutsWiki extends ShortcutsNavigation { + constructor() { + super(); + Mousetrap.bind('e', this.editWiki); + } + + editWiki() { + findAndFollowLink('.js-wiki-edit'); + } +} diff --git a/app/assets/javascripts/usage_ping.js b/app/assets/javascripts/usage_ping.js new file mode 100644 index 00000000000..fd3af7d7ab6 --- /dev/null +++ b/app/assets/javascripts/usage_ping.js @@ -0,0 +1,15 @@ +function UsagePing() { + const usageDataUrl = $('.usage-data').data('endpoint'); + + $.ajax({ + type: 'GET', + url: usageDataUrl, + dataType: 'html', + success(html) { + $('.usage-data').html(html); + }, + }); +} + +window.gl = window.gl || {}; +window.gl.UsagePing = UsagePing; diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index fa078b48bf8..b9d57cbcad4 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -18,7 +18,7 @@ export default class UserCallout { dismissCallout(e) { const $currentTarget = $(e.currentTarget); - Cookies.set(USER_CALLOUT_COOKIE, 'true'); + Cookies.set(USER_CALLOUT_COOKIE, 'true', { expires: 365 }); if ($currentTarget.hasClass('close')) { this.userCalloutBody.remove(); 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 8ebe12cb1c5..62b7131de51 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js @@ -1,12 +1,12 @@ /* eslint-disable no-param-reassign */ -import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button.vue'; -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 AsyncButtonComponent from '../../pipelines/components/async_button.vue'; +import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions'; +import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; +import PipelinesStatusComponent from '../../pipelines/components/status'; +import PipelinesStageComponent from '../../pipelines/components/stage'; +import PipelinesUrlComponent from '../../pipelines/components/pipeline_url'; +import PipelinesTimeagoComponent from '../../pipelines/components/time_ago'; import CommitComponent from './commit'; /** diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 90935b9616b..7c50b80fd2b 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -145,3 +145,17 @@ a { .dropdown-menu-nav a { transition: none; } + +@keyframes fadeIn { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +.fade-in { + animation: fadeIn $fade-in-duration 1; +} diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index b849cc2d853..f614f262316 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -38,6 +38,15 @@ height: 300px; overflow-y: scroll; } + + .disabled { + cursor: default; + opacity: 0.5; + + &:hover { + transform: none; + } + } } .emoji-search { @@ -154,6 +163,17 @@ } } + &.user-authored { + cursor: default; + opacity: 0.65; + + &:hover, + &:active { + background-color: $white-light; + border-color: $border-color; + } + } + &.btn { &:focus { outline: 0; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 2c33b235980..0fd7203e72b 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -40,6 +40,10 @@ line-height: 24px; } +.bold { + font-weight: 600; +} + .tab-content { overflow: visible; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 7767826b033..b87e712c763 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -564,3 +564,7 @@ color: $gl-text-color-secondary; } } + +.droplab-item-ignore { + pointer-events: none; +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index abb092623c0..0077ea41d3b 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -155,7 +155,7 @@ header { .header-logo { display: inline-block; - margin: 0 7px 0 2px; + margin: 0 12px 0 2px; position: relative; top: 10px; transition-duration: .3s; @@ -186,7 +186,7 @@ header { display: flex; align-items: flex-start; flex: 1 1 auto; - padding-top: (($header-height - 19) / 2); + padding-top: 14px; overflow: hidden; } @@ -331,6 +331,14 @@ header { .dropdown-menu-nav { min-width: 140px; margin-top: -5px; + + .current-user { + padding: 5px 18px; + + .user-name { + display: block; + } + } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 20ef9a774e4..3ef6ec3f912 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -458,6 +458,11 @@ $label-remove-border: rgba(0, 0, 0, .1); $label-border-radius: 100px; /* +* Animation +*/ +$fade-in-duration: 200ms; + +/* * Lint */ $lint-incorrect-color: $red-500; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 144adbcdaef..411f1c4442b 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -61,8 +61,9 @@ .truncated-info { text-align: center; border-bottom: 1px solid; - background-color: $black-transparent; + background-color: $black; height: 45px; + padding: 15px; &.affix { top: 0; @@ -87,6 +88,16 @@ right: 5px; left: 5px; } + + .truncated-info-size { + margin: 0 5px; + } + + .raw-link { + color: inherit; + margin-left: 5px; + text-decoration: underline; + } } } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 0dad91ba128..9e3142c8aa3 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -135,7 +135,7 @@ .text-expander { display: inline-block; - background: $gray-light; + background: $white-light; color: $gl-text-color-secondary; padding: 0 5px; cursor: pointer; @@ -146,6 +146,11 @@ line-height: $gl-font-size; outline: none; + &.open { + background: $gray-light; + box-shadow: inset 0 0 2px rgba($black, 0.2); + } + &:hover { background-color: darken($gray-light, 10%); text-decoration: none; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 1aa1079903c..1b4694377b3 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -106,6 +106,10 @@ span { white-space: pre-wrap; } + + .line { + word-wrap: break-word; + } } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 0bca3e93e4c..8d3d1a72b9b 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -210,10 +210,6 @@ } } - .bold { - font-weight: 600; - } - .light { font-weight: normal; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index c78fb8ede79..2ea2ff8362b 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -123,6 +123,9 @@ ul.notes { } .note-emoji-button { + position: relative; + line-height: 1; + .fa-spinner { display: none; } @@ -352,6 +355,15 @@ ul.notes { font-size: 14px; } +.note-header { + display: flex; + justify-content: space-between; +} + +.note-header-info { + min-width: 0; +} + .note-headline-light { display: inline; @@ -371,21 +383,27 @@ ul.notes { } } +.note-headline-meta { + display: inline-block; + white-space: nowrap; +} + /** * Actions for Discussions/Notes */ -.discussion-actions, -.note-actions { +.discussion-actions { float: right; margin-left: 10px; color: $gray-darkest; } .note-actions { - position: absolute; - right: 0; - top: 0; + flex-shrink: 0; + // For PhantomJS that does not support flex + float: right; + margin-left: 10px; + color: $gray-darkest; .note-action-button { margin-left: 8px; @@ -428,7 +446,8 @@ ul.notes { .award-control-icon-positive, .award-control-icon-super-positive { position: absolute; - margin-left: -20px; + top: 0; + left: 0; opacity: 0; } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 8c6dd392865..fe084eb9397 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -289,8 +289,12 @@ table.u2f-registrations { margin: 0 auto; .bordered-box { - border: 1px solid $border-color; + border: 1px solid $blue-300; border-radius: $border-radius-default; + background-color: $blue-25; + position: relative; + display: flex; + justify-content: center; } .landing { @@ -298,28 +302,59 @@ table.u2f-registrations { margin-bottom: $gl-padding; .close { - margin-right: 20px; - } + position: absolute; + right: 20px; + opacity: 1; + + .dismiss-icon { + float: right; + cursor: pointer; + color: $blue-300; + } - .dismiss-icon { - float: right; - cursor: pointer; - color: $cycle-analytics-dismiss-icon-color; + &:hover { + background-color: transparent; + border: 0; + + .dismiss-icon { + color: $blue-400; + } + } } .svg-container { - text-align: center; + margin-right: 30px; + display: inline-block; svg { - width: 136px; - height: 136px; + height: 110px; + vertical-align: top; } } + + .user-callout-copy { + display: inline-block; + vertical-align: top; + } } @media(max-width: $screen-xs-max) { - .inner-content { - padding-left: 30px; + text-align: center; + + .bordered-box { + display: block; + } + + .landing { + .svg-container, + .user-callout-copy { + margin: 0; + display: block; + + svg { + height: 75px; + } + } } } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 717ebb44a23..28a8f9cb335 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -596,6 +596,10 @@ pre.light-well { .avatar-container { align-self: flex-start; + + > a { + width: 100%; + } } .project-details { @@ -929,27 +933,23 @@ pre.light-well { } .variable-key { - width: 300px; - max-width: 300px; + max-width: 120px; overflow: hidden; word-wrap: break-word; - - // override bootstrap - white-space: normal!important; - - @media (max-width: $screen-sm-max) { - width: 150px; - max-width: 150px; - } + white-space: nowrap; + text-overflow: ellipsis; } .variable-value { - @media(max-width: $screen-xs-max) { - width: 150px; - max-width: 150px; - overflow: hidden; - word-wrap: break-word; - } + max-width: 150px; + overflow: hidden; + word-wrap: break-word; + white-space: nowrap; + text-overflow: ellipsis; + } + + .variable-menu { + text-align: right; } } |