diff options
Diffstat (limited to 'app/assets')
66 files changed, 832 insertions, 917 deletions
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 22fa1f2a609..ec5be8664b2 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -2,6 +2,7 @@ /* global Flash */ import _ from 'underscore'; import Cookies from 'js-cookie'; +import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; @@ -237,7 +238,7 @@ class AwardsHandler { addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { const isMainAwardsBlock = votesBlock.closest('.js-issue-note-awards').length; - if (gl.utils.isInIssuePage() && !isMainAwardsBlock) { + if (isInIssuePage() && !isMainAwardsBlock) { const id = votesBlock.attr('id').replace('note_', ''); $('.emoji-menu').removeClass('is-visible'); @@ -288,7 +289,7 @@ class AwardsHandler { } getVotesBlock() { - if (gl.utils.isInIssuePage()) { + if (isInIssuePage()) { const $el = $('.js-add-award.is-active').closest('.note.timeline-entry'); if ($el.length) { @@ -452,11 +453,11 @@ class AwardsHandler { 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'); + updateTooltipTitle($emojiButton, newTitle).tooltip('show'); // Restore tooltip back to award list return setTimeout(() => { $emojiButton.tooltip('hide'); - gl.utils.updateTooltipTitle($emojiButton, oldTitle); + updateTooltipTitle($emojiButton, oldTitle); }, 2800); } diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 79702c54852..2cf8f4fa935 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -1,4 +1,5 @@ import '../commons/bootstrap'; +import { isInIssuePage } from '../lib/utils/common_utils'; // Quick Submit behavior // @@ -45,7 +46,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => { if (!$submitButton.attr('disabled')) { $submitButton.trigger('click', [e]); - if (!gl.utils.isInIssuePage()) { + if (!isInIssuePage()) { $submitButton.disable(); } } diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index 26d3419a162..ddd1fea3aca 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -3,6 +3,7 @@ import '../lib/utils/url_utility'; import { HIDDEN_CLASS } from '../lib/utils/constants'; +import csrf from '../lib/utils/csrf'; function toggleLoading($el, $icon, loading) { if (loading) { @@ -36,9 +37,7 @@ export default class BlobFileDropzone { maxFiles: 1, addRemoveLinks: true, previewsContainer: '.dropzone-previews', - headers: { - 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content'), - }, + headers: csrf.headers, init: function () { this.on('addedfile', function () { toggleLoading(submitButton, submitButtonLoadingIcon, false); diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 187fab084fd..e0b73f13d36 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -1,4 +1,6 @@ /* global Flash */ +import { handleLocationHash } from '../../lib/utils/common_utils'; + export default class BlobViewer { constructor() { BlobViewer.initAuxiliaryViewer(); @@ -114,7 +116,7 @@ export default class BlobViewer { $(viewer).renderGFM(); this.$fileHolder.trigger('highlight:line'); - gl.utils.handleLocationHash(); + handleLocationHash(); this.toggleCopyButtonState(); }) diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 43928e602d6..ea82958e80d 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -2,6 +2,7 @@ /* global List */ import _ from 'underscore'; import Cookies from 'js-cookie'; +import { getUrlParamsArray } from '../../lib/utils/common_utils'; window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; @@ -21,7 +22,7 @@ gl.issueBoards.BoardsStore = { }, create () { this.state.lists = []; - this.filter.path = gl.utils.getUrlParamsArray().join('&'); + this.filter.path = getUrlParamsArray().join('&'); this.detail = { issue: {} }; }, addList (listObj, defaultAvatar) { diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index ae1a23132a7..286a758b8a9 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -3,6 +3,7 @@ consistent-return, prefer-rest-params */ import _ from 'underscore'; import bp from './breakpoints'; import { bytesToKiB } from './lib/utils/number_utils'; +import { setCiStatusFavicon } from './lib/utils/common_utils'; window.Build = (function () { Build.timeout = null; @@ -169,7 +170,7 @@ window.Build = (function () { data: this.state, }) .done((log) => { - gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`); + setCiStatusFavicon(`${this.pageUrl}/status.json`); if (log.state) { this.state = log.state; diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 17d14dc1e79..4763985c802 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -11,14 +11,22 @@ function ImageFile(file) { this.file = file; this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) { - // Determine if old and new file has same dimensions, if not show 'two-up' view return function(deletedWidth, deletedHeight) { return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) { - if (width === deletedWidth && height === deletedHeight) { - return _this.initViewModes(); - } else { - return _this.initView('two-up'); - } + _this.initViewModes(); + + // Load two-up view after images are loaded + // so that we can display the correct width and height information + const images = $('.two-up.view img', _this.file); + let loadedCount = 0; + + images.on('load', () => { + loadedCount += 1; + + if (loadedCount === images.length) { + _this.initView('two-up'); + } + }); }); }; })(this)); @@ -134,8 +142,9 @@ width: maxWidth + 1, height: maxHeight + 2 }); + // Set swipeBar left position to match image frame $swipeBar.css({ - left: 0 + left: 1 }); wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10); diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js index b375b61202e..eae4a7eab55 100644 --- a/app/assets/javascripts/confirm_danger_modal.js +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -1,4 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */ +import { rstrip } from './lib/utils/common_utils'; window.ConfirmDangerModal = (function() { function ConfirmDangerModal(form, text) { @@ -12,7 +13,7 @@ window.ConfirmDangerModal = (function() { submit.disable(); $('.js-confirm-danger-input').off('input'); $('.js-confirm-danger-input').on('input', function() { - if (gl.utils.rstrip($(this).val()) === project_path) { + if (rstrip($(this).val()) === project_path) { return submit.enable(); } else { return submit.disable(); diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js index 13ba4a57293..e3e2c798570 100644 --- a/app/assets/javascripts/copy_as_gfm.js +++ b/app/assets/javascripts/copy_as_gfm.js @@ -1,6 +1,6 @@ /* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ import _ from 'underscore'; -import './lib/utils/common_utils'; +import { insertText, getSelectedFragment, nodeMatchesSelector } from './lib/utils/common_utils'; import { placeholderImage } from './lazy_loader'; const gfmRules = { @@ -295,7 +295,7 @@ class CopyAsGFM { const clipboardData = e.originalEvent.clipboardData; if (!clipboardData) return; - const documentFragment = window.gl.utils.getSelectedFragment(); + const documentFragment = getSelectedFragment(); if (!documentFragment) return; const el = transformer(documentFragment.cloneNode(true)); @@ -412,7 +412,7 @@ class CopyAsGFM { for (const selector in rules) { const func = rules[selector]; - if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue; + if (!nodeMatchesSelector(node, selector)) continue; let result; if (func.length === 2) { diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index 904f7f64fa8..b41d464475f 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -73,7 +73,7 @@ </span> <a v-if="deployKey.can_edit" - class="btn btn-small" + class="btn btn-sm" :href="editDeployKeyPath" > Edit diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index f3b537c83e2..31214818496 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -77,6 +77,7 @@ import initProjectVisibilitySelector from './project_visibility'; import GpgBadges from './gpg_badges'; import UserFeatureHelper from './helpers/user_feature_helper'; import initChangesDropdown from './init_changes_dropdown'; +import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; (function() { var Dispatcher; @@ -100,7 +101,7 @@ import initChangesDropdown from './init_changes_dropdown'; $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); - const enableGFM = gl.utils.convertPermissionToBoolean(el.dataset.supportsAutocomplete); + const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete); gfm.setup($(el), { emojis: true, members: enableGFM, @@ -351,7 +352,7 @@ import initChangesDropdown from './init_changes_dropdown'; if ($('.blob-viewer').length) new BlobViewer(); if ($('.project-show-activity').length) new gl.Activities(); $('#tree-slider').waitForImages(function() { - gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); + ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); }); break; case 'projects:edit': @@ -427,7 +428,7 @@ import initChangesDropdown from './init_changes_dropdown'; new NewCommitForm($('.js-create-dir-form')); new UserCallout({ setCalloutPerProject: true }); $('#tree-slider').waitForImages(function() { - gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); + ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); }); break; case 'projects:find_file:show': diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 975903159be..1cba65d17cd 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -2,6 +2,7 @@ /* global Dropzone */ import _ from 'underscore'; import './preview_markdown'; +import csrf from './lib/utils/csrf'; window.DropzoneInput = (function() { function DropzoneInput(form) { @@ -50,9 +51,7 @@ window.DropzoneInput = (function() { paramName: 'file', maxFilesize: maxFileSize, uploadMultiple: false, - headers: { - 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') - }, + headers: csrf.headers, previewContainer: false, processing: function() { return $('.div-dropzone-alert').alert('close'); @@ -260,9 +259,7 @@ window.DropzoneInput = (function() { dataType: 'json', processData: false, contentType: false, - headers: { - 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') - }, + headers: csrf.headers, beforeSend: function() { showSpinner(); return closeAlertMessage(); diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue index f54d573db6e..14fde1afb16 100644 --- a/app/assets/javascripts/environments/components/environment.vue +++ b/app/assets/javascripts/environments/components/environment.vue @@ -6,7 +6,7 @@ import environmentTable from './environments_table.vue'; import EnvironmentsStore from '../stores/environments_store'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue'; -import '../../lib/utils/common_utils'; +import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils'; import eventHub from '../event_hub'; import Poll from '../../lib/utils/poll'; import environmentsMixin from '../mixins/environments_mixin'; @@ -51,19 +51,19 @@ export default { computed: { scope() { - return gl.utils.getParameterByName('scope'); + return getParameterByName('scope'); }, canReadEnvironmentParsed() { - return gl.utils.convertPermissionToBoolean(this.canReadEnvironment); + return convertPermissionToBoolean(this.canReadEnvironment); }, canCreateDeploymentParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreateDeployment); + return convertPermissionToBoolean(this.canCreateDeployment); }, canCreateEnvironmentParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment); + return convertPermissionToBoolean(this.canCreateEnvironment); }, }, @@ -72,8 +72,8 @@ export default { * Toggles loading property. */ created() { - const scope = gl.utils.getParameterByName('scope') || this.visibility; - const page = gl.utils.getParameterByName('page') || this.pageNumber; + const scope = getParameterByName('scope') || this.visibility; + const page = getParameterByName('page') || this.pageNumber; this.service = new EnvironmentsService(this.endpoint); @@ -126,15 +126,15 @@ export default { * @return {String} */ changePage(pageNumber) { - const param = gl.utils.setParamInURL('page', pageNumber); + const param = setParamInURL('page', pageNumber); gl.utils.visitUrl(param); return param; }, fetchEnvironments() { - const scope = gl.utils.getParameterByName('scope') || this.visibility; - const page = gl.utils.getParameterByName('page') || this.pageNumber; + const scope = getParameterByName('scope') || this.visibility; + const page = getParameterByName('page') || this.pageNumber; this.isLoading = true; diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 925503a01c4..35891240239 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -9,7 +9,7 @@ import tablePagination from '../../vue_shared/components/table_pagination.vue'; import Poll from '../../lib/utils/poll'; import eventHub from '../event_hub'; import environmentsMixin from '../mixins/environments_mixin'; -import '../../lib/utils/common_utils'; +import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils'; export default { components: { @@ -47,15 +47,15 @@ export default { computed: { scope() { - return gl.utils.getParameterByName('scope'); + return getParameterByName('scope'); }, canReadEnvironmentParsed() { - return gl.utils.convertPermissionToBoolean(this.canReadEnvironment); + return convertPermissionToBoolean(this.canReadEnvironment); }, canCreateDeploymentParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreateDeployment); + return convertPermissionToBoolean(this.canCreateDeployment); }, /** @@ -82,8 +82,8 @@ export default { * Toggles loading property. */ created() { - const scope = gl.utils.getParameterByName('scope') || this.visibility; - const page = gl.utils.getParameterByName('page') || this.pageNumber; + const scope = getParameterByName('scope') || this.visibility; + const page = getParameterByName('page') || this.pageNumber; this.service = new EnvironmentsService(this.endpoint); @@ -125,15 +125,15 @@ export default { * @param {Number} pageNumber desired page to go to. */ changePage(pageNumber) { - const param = gl.utils.setParamInURL('page', pageNumber); + const param = setParamInURL('page', pageNumber); gl.utils.visitUrl(param); return param; }, fetchEnvironments() { - const scope = gl.utils.getParameterByName('scope') || this.visibility; - const page = gl.utils.getParameterByName('page') || this.pageNumber; + const scope = getParameterByName('scope') || this.visibility; + const page = getParameterByName('page') || this.pageNumber; this.isLoading = true; diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index 038c149be2d..aff8227c38c 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -1,4 +1,4 @@ -import '~/lib/utils/common_utils'; +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; /** * Environments Store. * @@ -66,8 +66,8 @@ export default class EnvironmentsStore { } setPagination(pagination = {}) { - const normalizedHeaders = gl.utils.normalizeHeaders(pagination); - const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders); + const normalizedHeaders = normalizeHeaders(pagination); + const paginationInformation = parseIntPagination(normalizedHeaders); this.state.paginationInformation = paginationInformation; return paginationInformation; diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js deleted file mode 100644 index 800ca05cd11..00000000000 --- a/app/assets/javascripts/feature_highlight/feature_highlight.js +++ /dev/null @@ -1,61 +0,0 @@ -import Cookies from 'js-cookie'; -import _ from 'underscore'; -import { - getCookieName, - getSelector, - hidePopover, - setupDismissButton, - mouseenter, - mouseleave, -} from './feature_highlight_helper'; - -export const setupFeatureHighlightPopover = (id, debounceTimeout = 300) => { - const $selector = $(getSelector(id)); - const $parent = $selector.parent(); - const $popoverContent = $parent.siblings('.feature-highlight-popover-content'); - const hideOnScroll = hidePopover.bind($selector); - const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout); - - $selector - // Setup popover - .data('content', $popoverContent.prop('outerHTML')) - .popover({ - html: true, - // Override the existing template to add custom CSS classes - template: ` - <div class="popover feature-highlight-popover" role="tooltip"> - <div class="arrow"></div> - <div class="popover-content"></div> - </div> - `, - }) - .on('mouseenter', mouseenter) - .on('mouseleave', debouncedMouseleave) - .on('inserted.bs.popover', setupDismissButton) - .on('show.bs.popover', () => { - window.addEventListener('scroll', hideOnScroll); - }) - .on('hide.bs.popover', () => { - window.removeEventListener('scroll', hideOnScroll); - }) - // Display feature highlight - .removeAttr('disabled'); -}; - -export const shouldHighlightFeature = (id) => { - const element = document.querySelector(getSelector(id)); - const previouslyDismissed = Cookies.get(getCookieName(id)) === 'true'; - - return element && !previouslyDismissed; -}; - -export const highlightFeatures = (highlightOrder) => { - const featureId = highlightOrder.find(shouldHighlightFeature); - - if (featureId) { - setupFeatureHighlightPopover(featureId); - return true; - } - - return false; -}; diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js deleted file mode 100644 index 9f741355cd7..00000000000 --- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js +++ /dev/null @@ -1,57 +0,0 @@ -import Cookies from 'js-cookie'; - -export const getCookieName = cookieId => `feature-highlighted-${cookieId}`; -export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`; - -export const showPopover = function showPopover() { - if (this.hasClass('js-popover-show')) { - return false; - } - this.popover('show'); - this.addClass('disable-animation js-popover-show'); - - return true; -}; - -export const hidePopover = function hidePopover() { - if (!this.hasClass('js-popover-show')) { - return false; - } - this.popover('hide'); - this.removeClass('disable-animation js-popover-show'); - - return true; -}; - -export const dismiss = function dismiss(cookieId) { - Cookies.set(getCookieName(cookieId), true); - hidePopover.call(this); - this.hide(); -}; - -export const mouseleave = function mouseleave() { - if (!$('.popover:hover').length > 0) { - const $featureHighlight = $(this); - hidePopover.call($featureHighlight); - } -}; - -export const mouseenter = function mouseenter() { - const $featureHighlight = $(this); - - const showedPopover = showPopover.call($featureHighlight); - if (showedPopover) { - $('.popover') - .on('mouseleave', mouseleave.bind($featureHighlight)); - } -}; - -export const setupDismissButton = function setupDismissButton() { - const popoverId = this.getAttribute('aria-describedby'); - const cookieId = this.dataset.highlight; - const $popover = $(this); - const dismissWrapper = dismiss.bind($popover, cookieId); - - $(`#${popoverId} .dismiss-feature-highlight`) - .on('click', dismissWrapper); -}; diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_options.js b/app/assets/javascripts/feature_highlight/feature_highlight_options.js deleted file mode 100644 index fd48f2e87cc..00000000000 --- a/app/assets/javascripts/feature_highlight/feature_highlight_options.js +++ /dev/null @@ -1,12 +0,0 @@ -import { highlightFeatures } from './feature_highlight'; -import bp from '../breakpoints'; - -const highlightOrder = ['issue-boards']; - -export default function domContentLoaded(order) { - if (bp.getBreakpointSize() === 'lg') { - highlightFeatures(order); - } -} - -document.addEventListener('DOMContentLoaded', domContentLoaded.bind(this, highlightOrder)); diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 36a04d4202f..d17a43b048a 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -1,6 +1,7 @@ <script> import tablePagination from '~/vue_shared/components/table_pagination.vue'; import eventHub from '../event_hub'; +import { getParameterByName } from '../../lib/utils/common_utils'; export default { props: { @@ -18,8 +19,8 @@ export default { }, methods: { change(page) { - const filterGroupsParam = gl.utils.getParameterByName('filter_groups'); - const sortParam = gl.utils.getParameterByName('sort'); + const filterGroupsParam = getParameterByName('filter_groups'); + const sortParam = getParameterByName('sort'); eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam); }, }, diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index 439a931ddad..83b102764ba 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -1,5 +1,6 @@ import FilterableList from '~/filterable_list'; import eventHub from './event_hub'; +import { getParameterByName } from '../lib/utils/common_utils'; export default class GroupFilterableList extends FilterableList { constructor({ form, filter, holder, filterEndpoint, pagePath }) { @@ -54,7 +55,7 @@ export default class GroupFilterableList extends FilterableList { e.preventDefault(); const queryData = {}; - const sortParam = gl.utils.getParameterByName('sort', e.currentTarget.href); + const sortParam = getParameterByName('sort', e.currentTarget.href); if (sortParam) { queryData.sort = sortParam; diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 00e1bd94c9c..9ad8e5c6052 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -8,6 +8,7 @@ import GroupItem from './components/group_item.vue'; import GroupsStore from './stores/groups_store'; import GroupsService from './services/groups_service'; import eventHub from './event_hub'; +import { getParameterByName } from '../lib/utils/common_utils'; document.addEventListener('DOMContentLoaded', () => { const el = document.getElementById('dashboard-group-app'); @@ -58,17 +59,17 @@ document.addEventListener('DOMContentLoaded', () => { this.isLoading = true; } - pageParam = gl.utils.getParameterByName('page'); + pageParam = getParameterByName('page'); if (pageParam) { page = pageParam; } - filterGroupsParam = gl.utils.getParameterByName('filter_groups'); + filterGroupsParam = getParameterByName('filter_groups'); if (filterGroupsParam) { filterGroups = filterGroupsParam; } - sortParam = gl.utils.getParameterByName('sort'); + sortParam = getParameterByName('sort'); if (sortParam) { sort = sortParam; } diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index 6eab6083e8f..f59ec677603 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils'; export default class GroupsStore { constructor() { @@ -30,8 +31,8 @@ export default class GroupsStore { let paginationInfo; if (Object.keys(pagination).length) { - const normalizedHeaders = gl.utils.normalizeHeaders(pagination); - paginationInfo = gl.utils.parseIntPagination(normalizedHeaders); + const normalizedHeaders = normalizeHeaders(pagination); + paginationInfo = parseIntPagination(normalizedHeaders); } else { paginationInfo = pagination; } diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 4d629bc6326..90ca70289ab 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -4,6 +4,7 @@ prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, promise/catch-or-return */ import Api from './api'; +import { normalizeCRLFHeaders } from './lib/utils/common_utils'; var slice = [].slice; @@ -30,7 +31,7 @@ window.GroupsSelect = (function() { $.ajax(params).then((data, status, xhr) => { const results = data || []; - const headers = gl.utils.normalizeCRLFHeaders(xhr.getAllResponseHeaders()); + const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders()); const currentPage = parseInt(headers['X-PAGE'], 10) || 0; const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; const more = currentPage < totalPages; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index b8bebe1894f..ead1b8f99d3 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,437 +1,437 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, max-len, prefer-template */ -(function() { - (function(w) { - var base; - const faviconEl = document.getElementById('favicon'); - const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null; - w.gl || (w.gl = {}); - (base = w.gl).utils || (base.utils = {}); - w.gl.utils.isInGroupsPage = function() { - return gl.utils.getPagePath() === 'groups'; - }; - w.gl.utils.isInProjectPage = function() { - return gl.utils.getPagePath() === 'projects'; - }; - w.gl.utils.getProjectSlug = function() { - if (this.isInProjectPage()) { - return $('body').data('project'); - } else { - return null; - } - }; - w.gl.utils.getGroupSlug = function() { - if (this.isInGroupsPage()) { - return $('body').data('group'); - } else { - return null; - } - }; - - w.gl.utils.isInIssuePage = () => { - const page = gl.utils.getPagePath(1); - const action = gl.utils.getPagePath(2); - - return page === 'issues' && action === 'show'; - }; - w.gl.utils.ajaxGet = function(url) { - return $.ajax({ - type: "GET", - url: url, - dataType: "script" - }); - }; - - w.gl.utils.ajaxPost = function(url, data) { - return $.ajax({ - type: 'POST', - url: url, - data: data, - }); - }; - - w.gl.utils.extractLast = function(term) { - return this.split(term).pop(); - }; - - w.gl.utils.rstrip = function rstrip(val) { - if (val) { - return val.replace(/\s+$/, ''); +export const getPagePath = (index = 0) => $('body').data('page').split(':')[index]; + +export const isInGroupsPage = () => getPagePath() === 'groups'; + +export const isInProjectPage = () => getPagePath() === 'projects'; + +export const getProjectSlug = () => { + if (isInProjectPage()) { + return $('body').data('project'); + } + return null; +}; + +export const getGroupSlug = () => { + if (isInGroupsPage()) { + return $('body').data('group'); + } + return null; +}; + +export const isInIssuePage = () => { + const page = getPagePath(1); + const action = getPagePath(2); + + return page === 'issues' && action === 'show'; +}; + +export const ajaxGet = url => $.ajax({ + type: 'GET', + url, + dataType: 'script', +}); + +export const ajaxPost = (url, data) => $.ajax({ + type: 'POST', + url, + data, +}); + +export const rstrip = (val) => { + if (val) { + return val.replace(/\s+$/, ''); + } + return val; +}; + +export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('fixTitle'); + +export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => { + const field = $(fieldSelector); + const closestSubmit = field.closest('form').find(buttonSelector); + if (rstrip(field.val()) === '') { + closestSubmit.disable(); + } + // eslint-disable-next-line func-names + return field.on(eventName, function () { + if (rstrip($(this).val()) === '') { + return closestSubmit.disable(); + } + return closestSubmit.enable(); + }); +}; + +// automatically adjust scroll position for hash urls taking the height of the navbar into account +// https://github.com/twitter/bootstrap/issues/1768 +export const handleLocationHash = () => { + let hash = window.gl.utils.getLocationHash(); + if (!hash) return; + + // This is required to handle non-unicode characters in hash + hash = decodeURIComponent(hash); + + const fixedTabs = document.querySelector('.js-tabs-affix'); + const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck'); + const fixedNav = document.querySelector('.navbar-gitlab'); + + let adjustment = 0; + if (fixedNav) adjustment -= fixedNav.offsetHeight; + + // scroll to user-generated markdown anchor if we cannot find a match + if (document.getElementById(hash) === null) { + const target = document.getElementById(`user-content-${hash}`); + if (target && target.scrollIntoView) { + target.scrollIntoView(true); + window.scrollBy(0, adjustment); + } + } else { + // only adjust for fixedTabs when not targeting user-generated content + if (fixedTabs) { + adjustment -= fixedTabs.offsetHeight; + } + + if (fixedDiffStats) { + adjustment -= fixedDiffStats.offsetHeight; + } + + window.scrollBy(0, adjustment); + } +}; + +// Check if element scrolled into viewport from above or below +// Courtesy http://stackoverflow.com/a/7557433/414749 +export const isInViewport = (el) => { + const rect = el.getBoundingClientRect(); + + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= window.innerHeight && + rect.right <= window.innerWidth + ); +}; + +export const parseUrl = (url) => { + const parser = document.createElement('a'); + parser.href = url; + return parser; +}; + +export const parseUrlPathname = (url) => { + const parsedUrl = parseUrl(url); + // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11 + // We have to make sure we always have an absolute path. + return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`; +}; + +// We can trust that each param has one & since values containing & will be encoded +// Remove the first character of search as it is always ? +export const getUrlParamsArray = () => window.location.search.slice(1).split('&').map((param) => { + const split = param.split('='); + return [decodeURI(split[0]), split[1]].join('='); +}); + +export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; + +// Identify following special clicks +// 1) Cmd + Click on Mac (e.metaKey) +// 2) Ctrl + Click on PC (e.ctrlKey) +// 3) Middle-click or Mouse Wheel Click (e.which is 2) +export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2; + +export const scrollToElement = ($el) => { + const top = $el.offset().top; + const mrTabsHeight = $('.merge-request-tabs').height() || 0; + const headerHeight = $('.navbar-gitlab').height() || 0; + + return $('body, html').animate({ + scrollTop: top - mrTabsHeight - headerHeight, + }, 200); +}; + +/** + this will take in the `name` of the param you want to parse in the url + if the name does not exist this function will return `null` + otherwise it will return the value of the param key provided +*/ +export const getParameterByName = (name, urlToParse) => { + const url = urlToParse || window.location.href; + const parsedName = name.replace(/[[\]]/g, '\\$&'); + const regex = new RegExp(`[?&]${parsedName}(=([^&#]*)|&|#|$)`); + const results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, ' ')); +}; + +export const getSelectedFragment = () => { + const selection = window.getSelection(); + if (selection.rangeCount === 0) return null; + const documentFragment = document.createDocumentFragment(); + for (let i = 0; i < selection.rangeCount; i += 1) { + documentFragment.appendChild(selection.getRangeAt(i).cloneContents()); + } + if (documentFragment.textContent.length === 0) return null; + + return documentFragment; +}; + +// TODO: Update this name, there is a gl.text.insertText function. +export const insertText = (target, text) => { + // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas + const selectionStart = target.selectionStart; + const selectionEnd = target.selectionEnd; + const value = target.value; + + const textBefore = value.substring(0, selectionStart); + const textAfter = value.substring(selectionEnd, value.length); + + const insertedText = text instanceof Function ? text(textBefore, textAfter) : text; + const newText = textBefore + insertedText + textAfter; + + // eslint-disable-next-line no-param-reassign + target.value = newText; + // eslint-disable-next-line no-param-reassign + target.selectionStart = target.selectionEnd = selectionStart + insertedText.length; + + // Trigger autosave + $(target).trigger('input'); + + // Trigger autosize + const event = document.createEvent('Event'); + event.initEvent('autosize:update', true, false); + target.dispatchEvent(event); +}; + +export const nodeMatchesSelector = (node, selector) => { + const matches = Element.prototype.matches || + Element.prototype.matchesSelector || + Element.prototype.mozMatchesSelector || + Element.prototype.msMatchesSelector || + Element.prototype.oMatchesSelector || + Element.prototype.webkitMatchesSelector; + + if (matches) { + return matches.call(node, selector); + } + + // IE11 doesn't support `node.matches(selector)` + + let parentNode = node.parentNode; + if (!parentNode) { + parentNode = document.createElement('div'); + // eslint-disable-next-line no-param-reassign + node = node.cloneNode(true); + parentNode.appendChild(node); + } + + const matchingNodes = parentNode.querySelectorAll(selector); + return Array.prototype.indexOf.call(matchingNodes, node) !== -1; +}; + +/** + this will take in the headers from an API response and normalize them + this way we don't run into production issues when nginx gives us lowercased header keys +*/ +export const normalizeHeaders = (headers) => { + const upperCaseHeaders = {}; + + Object.keys(headers).forEach((e) => { + upperCaseHeaders[e.toUpperCase()] = headers[e]; + }); + + return upperCaseHeaders; +}; + +/** + this will take in the getAllResponseHeaders result and normalize them + this way we don't run into production issues when nginx gives us lowercased header keys +*/ +export const normalizeCRLFHeaders = (headers) => { + const headersObject = {}; + const headersArray = headers.split('\n'); + + headersArray.forEach((header) => { + const keyValue = header.split(': '); + headersObject[keyValue[0]] = keyValue[1]; + }); + + return normalizeHeaders(headersObject); +}; + +/** + * Parses pagination object string values into numbers. + * + * @param {Object} paginationInformation + * @returns {Object} + */ +export const parseIntPagination = paginationInformation => ({ + perPage: parseInt(paginationInformation['X-PER-PAGE'], 10), + page: parseInt(paginationInformation['X-PAGE'], 10), + total: parseInt(paginationInformation['X-TOTAL'], 10), + totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10), + nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10), + previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10), +}); + +/** + * Updates the search parameter of a URL given the parameter and value provided. + * + * If no search params are present we'll add it. + * If param for page is already present, we'll update it + * If there are params but not for the given one, we'll add it at the end. + * Returns the new search parameters. + * + * @param {String} param + * @param {Number|String|Undefined|Null} value + * @return {String} + */ +export const setParamInURL = (param, value) => { + let search; + const locationSearch = window.location.search; + + if (locationSearch.length) { + const parameters = locationSearch.substring(1, locationSearch.length) + .split('&') + .reduce((acc, element) => { + const val = element.split('='); + // eslint-disable-next-line no-param-reassign + acc[val[0]] = decodeURIComponent(val[1]); + return acc; + }, {}); + + parameters[param] = value; + + const toString = Object.keys(parameters) + .map(val => `${val}=${encodeURIComponent(parameters[val])}`) + .join('&'); + + search = `?${toString}`; + } else { + search = `?${param}=${value}`; + } + + return search; +}; + +/** + * Converts permission provided as strings to booleans. + * + * @param {String} string + * @returns {Boolean} + */ +export const convertPermissionToBoolean = permission => permission === 'true'; + +/** + * Back Off exponential algorithm + * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error> + * + * @param {Function<next, stop>} fn function to be called + * @param {Number} timeout + * @return {Promise<Any, Error>} + * @example + * ``` + * backOff(function (next, stop) { + * // Let's perform this function repeatedly for 60s or for the timeout provided. + * + * ourFunction() + * .then(function (result) { + * // continue if result is not what we need + * next(); + * + * // when result is what we need let's stop with the repetions and jump out of the cycle + * stop(result); + * }) + * .catch(function (error) { + * // if there is an error, we need to stop this with an error. + * stop(error); + * }) + * }, 60000) + * .then(function (result) {}) + * .catch(function (error) { + * // deal with errors passed to stop() + * }) + * ``` + */ +export const backOff = (fn, timeout = 60000) => { + const maxInterval = 32000; + let nextInterval = 2000; + let timeElapsed = 0; + + return new Promise((resolve, reject) => { + const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg)); + + const next = () => { + if (timeElapsed < timeout) { + setTimeout(() => fn(next, stop), nextInterval); + timeElapsed += nextInterval; + nextInterval = Math.min(nextInterval + nextInterval, maxInterval); } else { - return val; - } - }; - - 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; - that = this; - field = $(field_selector); - closest_submit = field.closest('form').find(button_selector); - if (this.rstrip(field.val()) === "") { - closest_submit.disable(); + reject(new Error('BACKOFF_TIMEOUT')); } - return field.on(event_name, function() { - if (that.rstrip($(this).val()) === "") { - return closest_submit.disable(); - } else { - return closest_submit.enable(); - } - }); }; - // automatically adjust scroll position for hash urls taking the height of the navbar into account - // https://github.com/twitter/bootstrap/issues/1768 - w.gl.utils.handleLocationHash = function() { - var hash = w.gl.utils.getLocationHash(); - if (!hash) return; - - // This is required to handle non-unicode characters in hash - hash = decodeURIComponent(hash); - - const fixedTabs = document.querySelector('.js-tabs-affix'); - const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck'); - const fixedNav = document.querySelector('.navbar-gitlab'); - - var adjustment = 0; - if (fixedNav) adjustment -= fixedNav.offsetHeight; - - // scroll to user-generated markdown anchor if we cannot find a match - if (document.getElementById(hash) === null) { - var target = document.getElementById('user-content-' + hash); - if (target && target.scrollIntoView) { - target.scrollIntoView(true); - window.scrollBy(0, adjustment); - } + fn(next, stop); + }); +}; + +export const setFavicon = (faviconPath) => { + const faviconEl = document.getElementById('favicon'); + if (faviconEl && faviconPath) { + faviconEl.setAttribute('href', faviconPath); + } +}; + +export const resetFavicon = () => { + const faviconEl = document.getElementById('favicon'); + const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null; + if (faviconEl) { + faviconEl.setAttribute('href', originalFavicon); + } +}; + +export const setCiStatusFavicon = (pageUrl) => { + $.ajax({ + url: pageUrl, + dataType: 'json', + success: (data) => { + if (data && data.favicon) { + setFavicon(data.favicon); } else { - // only adjust for fixedTabs when not targeting user-generated content - if (fixedTabs) { - adjustment -= fixedTabs.offsetHeight; - } - - if (fixedDiffStats) { - adjustment -= fixedDiffStats.offsetHeight; - } - - window.scrollBy(0, adjustment); + resetFavicon(); } - }; - - // Check if element scrolled into viewport from above or below - // Courtesy http://stackoverflow.com/a/7557433/414749 - w.gl.utils.isInViewport = function(el) { - var rect = el.getBoundingClientRect(); - - return ( - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= window.innerHeight && - rect.right <= window.innerWidth - ); - }; - - gl.utils.getPagePath = function(index) { - index = index || 0; - return $('body').data('page').split(':')[index]; - }; - - gl.utils.parseUrl = function (url) { - var parser = document.createElement('a'); - parser.href = url; - return parser; - }; - - gl.utils.parseUrlPathname = function (url) { - var parsedUrl = gl.utils.parseUrl(url); - // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11 - // We have to make sure we always have an absolute path. - return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname; - }; - - gl.utils.getUrlParamsArray = function () { - // We can trust that each param has one & since values containing & will be encoded - // Remove the first character of search as it is always ? - return window.location.search.slice(1).split('&').map((param) => { - const split = param.split('='); - return [decodeURI(split[0]), split[1]].join('='); - }); - }; - - gl.utils.isMetaKey = function(e) { - return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; - }; - - gl.utils.isMetaClick = function(e) { - // Identify following special clicks - // 1) Cmd + Click on Mac (e.metaKey) - // 2) Ctrl + Click on PC (e.ctrlKey) - // 3) Middle-click or Mouse Wheel Click (e.which is 2) - return e.metaKey || e.ctrlKey || e.which === 2; - }; - - gl.utils.scrollToElement = function($el) { - const top = $el.offset().top; - const mrTabsHeight = $('.merge-request-tabs').height() || 0; - const headerHeight = $('.navbar-gitlab').height() || 0; - - return $('body, html').animate({ - scrollTop: top - mrTabsHeight - headerHeight, - }, 200); - }; - - /** - this will take in the `name` of the param you want to parse in the url - if the name does not exist this function will return `null` - otherwise it will return the value of the param key provided - */ - w.gl.utils.getParameterByName = (name, parseUrl) => { - const url = parseUrl || window.location.href; - name = name.replace(/[[\]]/g, '\\$&'); - const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); - const results = regex.exec(url); - if (!results) return null; - if (!results[2]) return ''; - return decodeURIComponent(results[2].replace(/\+/g, ' ')); - }; - - w.gl.utils.getSelectedFragment = () => { - const selection = window.getSelection(); - if (selection.rangeCount === 0) return null; - const documentFragment = document.createDocumentFragment(); - for (let i = 0; i < selection.rangeCount; i += 1) { - documentFragment.appendChild(selection.getRangeAt(i).cloneContents()); - } - if (documentFragment.textContent.length === 0) return null; - - return documentFragment; - }; - - w.gl.utils.insertText = (target, text) => { - // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas - - const selectionStart = target.selectionStart; - const selectionEnd = target.selectionEnd; - const value = target.value; - - const textBefore = value.substring(0, selectionStart); - const textAfter = value.substring(selectionEnd, value.length); - - const insertedText = text instanceof Function ? text(textBefore, textAfter) : text; - const newText = textBefore + insertedText + textAfter; - - target.value = newText; - target.selectionStart = target.selectionEnd = selectionStart + insertedText.length; - - // Trigger autosave - $(target).trigger('input'); - - // Trigger autosize - var event = document.createEvent('Event'); - event.initEvent('autosize:update', true, false); - target.dispatchEvent(event); - }; - - w.gl.utils.nodeMatchesSelector = (node, selector) => { - const matches = Element.prototype.matches || - Element.prototype.matchesSelector || - Element.prototype.mozMatchesSelector || - Element.prototype.msMatchesSelector || - Element.prototype.oMatchesSelector || - Element.prototype.webkitMatchesSelector; - - if (matches) { - return matches.call(node, selector); - } - - // IE11 doesn't support `node.matches(selector)` - - let parentNode = node.parentNode; - if (!parentNode) { - parentNode = document.createElement('div'); - node = node.cloneNode(true); - parentNode.appendChild(node); - } - - const matchingNodes = parentNode.querySelectorAll(selector); - return Array.prototype.indexOf.call(matchingNodes, node) !== -1; - }; - - /** - this will take in the headers from an API response and normalize them - this way we don't run into production issues when nginx gives us lowercased header keys - */ - w.gl.utils.normalizeHeaders = (headers) => { - const upperCaseHeaders = {}; - - Object.keys(headers).forEach((e) => { - upperCaseHeaders[e.toUpperCase()] = headers[e]; - }); - - return upperCaseHeaders; - }; - - /** - this will take in the getAllResponseHeaders result and normalize them - this way we don't run into production issues when nginx gives us lowercased header keys - */ - w.gl.utils.normalizeCRLFHeaders = (headers) => { - const headersObject = {}; - const headersArray = headers.split('\n'); - - headersArray.forEach((header) => { - const keyValue = header.split(': '); - headersObject[keyValue[0]] = keyValue[1]; - }); - - return w.gl.utils.normalizeHeaders(headersObject); - }; - - /** - * Parses pagination object string values into numbers. - * - * @param {Object} paginationInformation - * @returns {Object} - */ - w.gl.utils.parseIntPagination = paginationInformation => ({ - perPage: parseInt(paginationInformation['X-PER-PAGE'], 10), - page: parseInt(paginationInformation['X-PAGE'], 10), - total: parseInt(paginationInformation['X-TOTAL'], 10), - totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10), - nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10), - previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10), - }); - - /** - * Updates the search parameter of a URL given the parameter and value provided. - * - * If no search params are present we'll add it. - * If param for page is already present, we'll update it - * If there are params but not for the given one, we'll add it at the end. - * Returns the new search parameters. - * - * @param {String} param - * @param {Number|String|Undefined|Null} value - * @return {String} - */ - w.gl.utils.setParamInURL = (param, value) => { - let search; - const locationSearch = window.location.search; - - if (locationSearch.length) { - const parameters = locationSearch.substring(1, locationSearch.length) - .split('&') - .reduce((acc, element) => { - const val = element.split('='); - acc[val[0]] = decodeURIComponent(val[1]); - return acc; - }, {}); - - parameters[param] = value; - - const toString = Object.keys(parameters) - .map(val => `${val}=${encodeURIComponent(parameters[val])}`) - .join('&'); - - search = `?${toString}`; - } else { - search = `?${param}=${value}`; - } - - return search; - }; - - /** - * Converts permission provided as strings to booleans. - * - * @param {String} string - * @returns {Boolean} - */ - w.gl.utils.convertPermissionToBoolean = permission => permission === 'true'; - - /** - * Back Off exponential algorithm - * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error> - * - * @param {Function<next, stop>} fn function to be called - * @param {Number} timeout - * @return {Promise<Any, Error>} - * @example - * ``` - * backOff(function (next, stop) { - * // Let's perform this function repeatedly for 60s or for the timeout provided. - * - * ourFunction() - * .then(function (result) { - * // continue if result is not what we need - * next(); - * - * // when result is what we need let's stop with the repetions and jump out of the cycle - * stop(result); - * }) - * .catch(function (error) { - * // if there is an error, we need to stop this with an error. - * stop(error); - * }) - * }, 60000) - * .then(function (result) {}) - * .catch(function (error) { - * // deal with errors passed to stop() - * }) - * ``` - */ - w.gl.utils.backOff = (fn, timeout = 60000) => { - const maxInterval = 32000; - let nextInterval = 2000; - let timeElapsed = 0; - - return new Promise((resolve, reject) => { - const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg)); - - const next = () => { - if (timeElapsed < timeout) { - setTimeout(() => fn(next, stop), nextInterval); - timeElapsed += nextInterval; - nextInterval = Math.min(nextInterval + nextInterval, maxInterval); - } else { - reject(new Error('BACKOFF_TIMEOUT')); - } - }; - - fn(next, stop); - }); - }; - - w.gl.utils.setFavicon = (faviconPath) => { - if (faviconEl && faviconPath) { - faviconEl.setAttribute('href', faviconPath); - } - }; - - w.gl.utils.resetFavicon = () => { - if (faviconEl) { - faviconEl.setAttribute('href', originalFavicon); - } - }; - - w.gl.utils.setCiStatusFavicon = (pageUrl) => { - $.ajax({ - url: pageUrl, - dataType: 'json', - success: function(data) { - if (data && data.favicon) { - gl.utils.setFavicon(data.favicon); - } else { - gl.utils.resetFavicon(); - } - }, - error: function() { - gl.utils.resetFavicon(); - } - }); - }; - })(window); -}).call(window); + }, + error: () => { + resetFavicon(); + }, + }); +}; + +window.gl = window.gl || {}; +window.gl.utils = { + ...(window.gl.utils || {}), + getPagePath, + isInGroupsPage, + isInProjectPage, + getProjectSlug, + getGroupSlug, + isInIssuePage, + ajaxGet, + ajaxPost, + rstrip, + updateTooltipTitle, + disableButtonIfEmptyField, + handleLocationHash, + isInViewport, + parseUrl, + parseUrlPathname, + getUrlParamsArray, + isMetaKey, + isMetaClick, + scrollToElement, + getParameterByName, + getSelectedFragment, + insertText, + nodeMatchesSelector, +}; diff --git a/app/assets/javascripts/lib/utils/csrf.js b/app/assets/javascripts/lib/utils/csrf.js new file mode 100644 index 00000000000..ae41cc5e8a8 --- /dev/null +++ b/app/assets/javascripts/lib/utils/csrf.js @@ -0,0 +1,56 @@ +/* +This module provides easy access to the CSRF token and caches +it for re-use. It also exposes some values commonly used in relation +to the CSRF token (header key and headers object). + +If you need to refresh the csrfToken for some reason, just call `init` and +then use the accessors as you would normally. + +If you need to compose a headers object, use the spread operator: + +``` + headers: { + ...csrf.headers, + someOtherHeader: '12345', + } +``` + */ + +const csrf = { + init() { + const tokenEl = document.querySelector('meta[name=csrf-token]'); + + if (tokenEl !== null) { + this.csrfToken = tokenEl.getAttribute('content'); + } else { + this.csrfToken = null; + } + }, + + get token() { + return this.csrfToken; + }, + + get headerKey() { + return 'X-CSRF-Token'; + }, + + get headers() { + if (this.csrfToken !== null) { + return { + [this.headerKey]: this.token, + }; + } + return {}; + }, +}; + +csrf.init(); + +// use our cached token for any $.rails-generated AJAX requests +if ($.rails) { + $.rails.csrfToken = () => csrf.token; +} + +export default csrf; + diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index 97666e13ebe..1485e900945 100644 --- a/app/assets/javascripts/lib/utils/poll.js +++ b/app/assets/javascripts/lib/utils/poll.js @@ -1,4 +1,5 @@ import httpStatusCodes from './http_status'; +import { normalizeHeaders } from './common_utils'; /** * Polling utility for handling realtime updates. @@ -57,7 +58,7 @@ export default class Poll { } checkConditions(response) { - const headers = gl.utils.normalizeHeaders(response.headers); + const headers = normalizeHeaders(response.headers); const pollInterval = parseInt(headers[this.intervalHeader], 10); if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) { diff --git a/app/assets/javascripts/lib/utils/pretty_time.js b/app/assets/javascripts/lib/utils/pretty_time.js index 227bf65b560..b1ffd797f7e 100644 --- a/app/assets/javascripts/lib/utils/pretty_time.js +++ b/app/assets/javascripts/lib/utils/pretty_time.js @@ -1,68 +1,61 @@ import _ from 'underscore'; -(() => { - /* - * TODO: Make these methods more configurable (e.g. stringifyTime condensed or - * non-condensed, abbreviateTimelengths) - * */ - - const utils = window.gl.utils = gl.utils || {}; - const prettyTime = utils.prettyTime = { - /* - * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } - * Seconds can be negative or positive, zero or non-zero. Can be configured for any day - * or week length. - */ - parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) { - const DAYS_PER_WEEK = daysPerWeek; - const HOURS_PER_DAY = hoursPerDay; - const MINUTES_PER_HOUR = 60; - const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR; - const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR; - - const timePeriodConstraints = { - weeks: MINUTES_PER_WEEK, - days: MINUTES_PER_DAY, - hours: MINUTES_PER_HOUR, - minutes: 1, - }; +/* + * TODO: Make these methods more configurable (e.g. stringifyTime condensed or + * non-condensed, abbreviateTimelengths) + * */ + +/* + * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } + * Seconds can be negative or positive, zero or non-zero. Can be configured for any day + * or week length. +*/ + +export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) { + const DAYS_PER_WEEK = daysPerWeek; + const HOURS_PER_DAY = hoursPerDay; + const MINUTES_PER_HOUR = 60; + const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR; + const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR; + + const timePeriodConstraints = { + weeks: MINUTES_PER_WEEK, + days: MINUTES_PER_DAY, + hours: MINUTES_PER_HOUR, + minutes: 1, + }; - let unorderedMinutes = prettyTime.secondsToMinutes(seconds); + let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR); - return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => { - const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); + return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => { + const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); - unorderedMinutes -= (periodCount * minutesPerPeriod); + unorderedMinutes -= (periodCount * minutesPerPeriod); - return periodCount; - }); - }, + return periodCount; + }); +} - /* - * Accepts a timeObject and returns a condensed string representation of it - * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included. - */ +/* +* Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it +* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included. +*/ - stringifyTime(timeObject) { - const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => { - const isNonZero = !!unitValue; - return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; - }, '').trim(); - return reducedTime.length ? reducedTime : '0m'; - }, +export function stringifyTime(timeObject) { + const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => { + const isNonZero = !!unitValue; + return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; + }, '').trim(); + return reducedTime.length ? reducedTime : '0m'; +} - /* - * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns - * the first non-zero unit/value pair. - */ +/* +* Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns +* the first non-zero unit/value pair. +*/ - abbreviateTime(timeStr) { - return timeStr.split(' ') - .filter(unitStr => unitStr.charAt(0) !== '0')[0]; - }, +export function abbreviateTime(timeStr) { + return timeStr.split(' ') + .filter(unitStr => unitStr.charAt(0) !== '0')[0]; +} - secondsToMinutes(seconds) { - return Math.abs(seconds / 60); - }, - }; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 0f84470828a..58d877f73fe 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -40,7 +40,7 @@ import './commit/image_file'; // lib/utils import './lib/utils/bootstrap_linked_tabs'; -import './lib/utils/common_utils'; +import { handleLocationHash } from './lib/utils/common_utils'; import './lib/utils/datetime_utility'; import './lib/utils/pretty_time'; import './lib/utils/text_utility'; @@ -101,7 +101,6 @@ import './label_manager'; import './labels'; import './labels_select'; import './layout_nav'; -import './feature_highlight/feature_highlight_options'; import LazyLoader from './lazy_loader'; import './line_highlighter'; import './logo'; @@ -161,10 +160,10 @@ document.addEventListener('beforeunload', function () { $('[data-toggle="popover"]').popover('destroy'); }); -window.addEventListener('hashchange', gl.utils.handleLocationHash); +window.addEventListener('hashchange', handleLocationHash); window.addEventListener('load', function onLoad() { window.removeEventListener('load', onLoad, false); - gl.utils.handleLocationHash(); + handleLocationHash(); }, false); gl.lazyLoader = new LazyLoader({ @@ -190,7 +189,7 @@ $(function () { $body.on('click', 'a[href^="#"]', function() { var href = this.getAttribute('href'); if (href.substr(1) === gl.utils.getLocationHash()) { - setTimeout(gl.utils.handleLocationHash, 1); + setTimeout(handleLocationHash, 1); } }); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 0c3c877ff15..f71a59aaa84 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -7,6 +7,11 @@ import './flash'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; import initChangesDropdown from './init_changes_dropdown'; import bp from './breakpoints'; +import { + parseUrlPathname, + handleLocationHash, + isMetaClick, +} from './lib/utils/common_utils'; /* eslint-disable max-len */ // MergeRequestTabs @@ -114,7 +119,7 @@ import bp from './breakpoints'; } clickTab(e) { - if (e.currentTarget && gl.utils.isMetaClick(e)) { + if (e.currentTarget && isMetaClick(e)) { const targetLink = e.currentTarget.getAttribute('href'); e.stopImmediatePropagation(); e.preventDefault(); @@ -260,7 +265,7 @@ import bp from './breakpoints'; // We extract pathname for the current Changes tab anchor href // some pages like MergeRequestsController#new has query parameters on that anchor - const urlPathname = gl.utils.parseUrlPathname(source); + const urlPathname = parseUrlPathname(source); this.ajaxGet({ url: `${urlPathname}.json${location.search}`, @@ -309,7 +314,7 @@ import bp from './breakpoints'; forceShow: true, }); anchor[0].scrollIntoView(); - window.gl.utils.handleLocationHash(); + handleLocationHash(); // We have multiple elements on the page with `#note_xxx` // (discussion and diff tabs) and `:target` only applies to the first anchor.addClass('target'); diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 5d96b193fce..192473b7dd1 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -7,6 +7,7 @@ import EmptyState from './empty_state.vue'; import MonitoringStore from '../stores/monitoring_store'; import eventHub from '../event_hub'; + import { convertPermissionToBoolean } from '../../lib/utils/common_utils'; export default { @@ -17,7 +18,7 @@ return { store, state: 'gettingStarted', - hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics), + hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics), documentationPath: metricsData.documentationPath, settingsPath: metricsData.settingsPath, metricsEndpoint: metricsData.additionalMetrics, diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js index 4ed651d5740..fed884d5c94 100644 --- a/app/assets/javascripts/monitoring/services/monitoring_service.js +++ b/app/assets/javascripts/monitoring/services/monitoring_service.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import VueResource from 'vue-resource'; import statusCodes from '../../lib/utils/http_status'; +import { backOff } from '../../lib/utils/common_utils'; Vue.use(VueResource); @@ -8,7 +9,7 @@ const MAX_REQUESTS = 3; function backOffRequest(makeRequestCallback) { let requestCounter = 0; - return gl.utils.backOff((next, stop) => { + return backOff((next, stop) => { makeRequestCallback().then((resp) => { if (resp.status === statusCodes.NO_CONTENT) { requestCounter += 1; diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js index f2eb2338a1e..997550b37fb 100644 --- a/app/assets/javascripts/new_sidebar.js +++ b/app/assets/javascripts/new_sidebar.js @@ -11,6 +11,7 @@ export default class NewNavSidebar { initDomElements() { this.$page = $('.page-with-sidebar'); this.$sidebar = $('.nav-sidebar'); + this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar); this.$overlay = $('.mobile-overlay'); this.$openSidebar = $('.toggle-mobile-nav'); this.$closeSidebar = $('.close-nav-button'); @@ -55,6 +56,16 @@ export default class NewNavSidebar { this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); } NewNavSidebar.setCollapsedCookie(collapsed); + + this.toggleSidebarOverflow(); + } + + toggleSidebarOverflow() { + if (this.$innerScroll.prop('scrollHeight') > this.$innerScroll.prop('offsetHeight')) { + this.$innerScroll.css('overflow-y', 'scroll'); + } else { + this.$innerScroll.css('overflow-y', ''); + } } render() { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index f5f7bb4653d..93aa29454a0 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -23,6 +23,7 @@ import loadAwardsHandler from './awards_handler'; import './autosave'; import './dropzone_input'; import TaskList from './task_list'; +import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils'; window.autosize = autosize; window.Dropzone = Dropzone; @@ -81,7 +82,7 @@ export default class Notes { this.setViewType(view); // We are in the Merge Requests page so we need another edit form for Changes tab - if (gl.utils.getPagePath(1) === 'merge_requests') { + if (getPagePath(1) === 'merge_requests') { $('.note-edit-form').clone() .addClass('mr-note-edit-form').insertAfter('.note-edit-form'); } @@ -175,7 +176,7 @@ export default class Notes { keydownNoteText(e) { var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText; - if (gl.utils.isMetaKey(e)) { + if (isMetaKey(e)) { return; } @@ -644,10 +645,10 @@ export default class Notes { } else { var $buttons = $el.find('.note-form-actions'); - var isWidgetVisible = gl.utils.isInViewport($el.get(0)); + var isWidgetVisible = isInViewport($el.get(0)); if (!isWidgetVisible) { - gl.utils.scrollToElement($el); + scrollToElement($el); } $el.find('.js-finish-edit-warning').show(); @@ -1188,7 +1189,7 @@ export default class Notes { } static checkMergeRequestStatus() { - if (gl.utils.getPagePath(1) === 'merge_requests') { + if (getPagePath(1) === 'merge_requests') { gl.mrWidget.checkStatus(); } } @@ -1326,7 +1327,7 @@ export default class Notes { * 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve * 3) Build temporary placeholder element (using `createPlaceholderNote`) * 4) Show placeholder note on UI - * 5) Perform network request to submit the note using `gl.utils.ajaxPost` + * 5) Perform network request to submit the note using `ajaxPost` * a) If request is successfully completed * 1. Remove placeholder element * 2. Show submitted Note element @@ -1408,7 +1409,7 @@ export default class Notes { /* eslint-disable promise/catch-or-return */ // Make request to submit comment on server - gl.utils.ajaxPost(formAction, formData) + ajaxPost(formAction, formData) .then((note) => { // Submission successful! remove placeholder $notesContainer.find(`#${noteUniqueId}`).remove(); @@ -1481,7 +1482,7 @@ export default class Notes { * * 1) Get Form metadata * 2) Update note element with new content - * 3) Perform network request to submit the updated note using `gl.utils.ajaxPost` + * 3) Perform network request to submit the updated note using `ajaxPost` * a) If request is successfully completed * 1. Show submitted Note element * b) If request failed @@ -1510,7 +1511,7 @@ export default class Notes { /* eslint-disable promise/catch-or-return */ // Make request to update comment on server - gl.utils.ajaxPost(formAction, formData) + ajaxPost(formAction, formData) .then((note) => { // Submission successful! render final note element this.updateNote(note, $editingNote); diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 16f4e22aa9b..fa7ac994058 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -2,6 +2,7 @@ /* global Flash, Autosave */ import { mapActions, mapGetters } from 'vuex'; import _ from 'underscore'; + import autosize from 'vendor/autosize'; import '../../autosave'; import TaskList from '../../task_list'; import * as constants from '../constants'; @@ -96,6 +97,8 @@ methods: { ...mapActions([ 'saveNote', + 'stopPolling', + 'restartPolling', 'removePlaceholderNotes', ]), setIsSubmitButtonDisabled(note, isSubmitting) { @@ -124,10 +127,14 @@ } this.isSubmitting = true; this.note = ''; // Empty textarea while being requested. Repopulate in catch + this.resizeTextarea(); + this.stopPolling(); this.saveNote(noteData) .then((res) => { this.isSubmitting = false; + this.restartPolling(); + if (res.errors) { if (res.errors.commands_only) { this.discard(); @@ -174,6 +181,8 @@ if (shouldClear) { this.note = ''; + this.resizeTextarea(); + this.$refs.markdownField.previewMarkdown = false; } // reset autostave @@ -205,6 +214,11 @@ selector: '.notes', }); }, + resizeTextarea() { + this.$nextTick(() => { + autosize.update(this.$refs.textarea); + }); + }, }, mounted() { // jQuery is needed here because it is a custom event being dispatched with jQuery. @@ -247,7 +261,8 @@ :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" :add-spacing-classes="false" - :is-confidential-issue="isConfidentialIssue"> + :is-confidential-issue="isConfidentialIssue" + ref="markdownField"> <textarea id="note-body" name="note[note]" diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 13cd74bfa1c..1a791039909 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -7,6 +7,7 @@ import * as constants from '../constants'; import service from '../services/issue_notes_service'; import loadAwardsHandler from '../../awards_handler'; import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; +import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; let eTagPoll; @@ -186,6 +187,14 @@ export const poll = ({ commit, state, getters }) => { }); }; +export const stopPolling = () => { + eTagPoll.stop(); +}; + +export const restartPolling = () => { + eTagPoll.restart(); +}; + export const fetchData = ({ commit, state, getters }) => { const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt }; @@ -211,7 +220,7 @@ export const toggleAwardRequest = ({ commit, getters, dispatch }, data) => { }; export const scrollToNoteIfNeeded = (context, el) => { - if (!gl.utils.isInViewport(el[0])) { - gl.utils.scrollToElement(el); + if (!isInViewport(el[0])) { + scrollToElement(el); } }; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 3b2b2089d6e..c2a08f3d6fe 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -5,15 +5,19 @@ import * as constants from '../constants'; export default { [types.ADD_NEW_NOTE](state, note) { const { discussion_id, type } = note; - const noteData = { - expanded: true, - id: discussion_id, - individual_note: !(type === constants.DISCUSSION_NOTE), - notes: [note], - reply_id: discussion_id, - }; - - state.notes.push(noteData); + const [exists] = state.notes.filter(n => n.id === note.discussion_id); + + if (!exists) { + const noteData = { + expanded: true, + id: discussion_id, + individual_note: !(type === constants.DISCUSSION_NOTE), + notes: [note], + reply_id: discussion_id, + }; + + state.notes.push(noteData); + } }, [types.ADD_NEW_REPLY_TO_DISCUSSION](state, note) { diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index 01110420cca..e3fc1e2fc2f 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -1,4 +1,4 @@ -import '~/lib/utils/common_utils'; +import { getParameterByName } from '~/lib/utils/common_utils'; import '~/lib/utils/url_utility'; (() => { @@ -9,7 +9,7 @@ import '~/lib/utils/url_utility'; init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) { this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']); this.limit = limit; - this.offset = parseInt(gl.utils.getParameterByName('offset'), 10) || this.limit; + this.offset = parseInt(getParameterByName('offset'), 10) || this.limit; this.disable = disable; this.prepareData = prepareData; this.callback = callback; diff --git a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js index 644efd10509..9e0e5cacb11 100644 --- a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js +++ b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js @@ -1,3 +1,5 @@ +import { convertPermissionToBoolean } from '../lib/utils/common_utils'; + function insertRow($row) { const $rowClone = $row.clone(); $rowClone.removeAttr('data-is-persisted'); @@ -6,7 +8,7 @@ function insertRow($row) { } function removeRow($row) { - const isPersisted = gl.utils.convertPermissionToBoolean($row.attr('data-is-persisted')); + const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted')); if (isPersisted) { $row.hide(); diff --git a/app/assets/javascripts/pipelines.js b/app/assets/javascripts/pipelines.js index 26a36ad54d1..07abe714367 100644 --- a/app/assets/javascripts/pipelines.js +++ b/app/assets/javascripts/pipelines.js @@ -1,4 +1,5 @@ import LinkedTabs from './lib/utils/bootstrap_linked_tabs'; +import { setCiStatusFavicon } from './lib/utils/common_utils'; export default class Pipelines { constructor(options = {}) { @@ -8,7 +9,7 @@ export default class Pipelines { } if (options.pipelineStatusUrl) { - gl.utils.setCiStatusFavicon(options.pipelineStatusUrl); + setCiStatusFavicon(options.pipelineStatusUrl); } } } diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 5e6d6b2fbdc..4c53e2541fc 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -4,6 +4,7 @@ import tablePagination from '../../vue_shared/components/table_pagination.vue'; import navigationTabs from './navigation_tabs.vue'; import navigationControls from './nav_controls.vue'; + import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils'; export default { props: { @@ -44,10 +45,10 @@ }, computed: { canCreatePipelineParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreatePipeline); + return convertPermissionToBoolean(this.canCreatePipeline); }, scope() { - const scope = gl.utils.getParameterByName('scope'); + const scope = getParameterByName('scope'); return scope === null ? 'all' : scope; }, @@ -105,10 +106,10 @@ }; }, pageParameter() { - return gl.utils.getParameterByName('page') || this.pagenum; + return getParameterByName('page') || this.pagenum; }, scopeParameter() { - return gl.utils.getParameterByName('scope') || this.apiScope; + return getParameterByName('scope') || this.apiScope; }, }, created() { @@ -122,7 +123,7 @@ * @param {Number} pageNumber desired page to go to. */ change(pageNumber) { - const param = gl.utils.setParamInURL('page', pageNumber); + const param = setParamInURL('page', pageNumber); gl.utils.visitUrl(param); return param; diff --git a/app/assets/javascripts/pipelines/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js index ffefe0192f2..651251d2623 100644 --- a/app/assets/javascripts/pipelines/stores/pipelines_store.js +++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js @@ -1,3 +1,5 @@ +import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils'; + export default class PipelinesStore { constructor() { this.state = {}; @@ -19,8 +21,8 @@ export default class PipelinesStore { let paginationInfo; if (Object.keys(pagination).length) { - const normalizedHeaders = gl.utils.normalizeHeaders(pagination); - paginationInfo = gl.utils.parseIntPagination(normalizedHeaders); + const normalizedHeaders = normalizeHeaders(pagination); + paginationInfo = parseIntPagination(normalizedHeaders); } else { paginationInfo = pagination; } diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index 4ccea0624ee..3deb242bc1f 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -1,5 +1,6 @@ /* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */ /* global Flash */ +import { getPagePath } from '../lib/utils/common_utils'; ((global) => { class Profile { @@ -93,7 +94,7 @@ return $title.val(comment[1]).change(); } }); - if (global.utils.getPagePath() === 'profiles') { + if (getPagePath() === 'profiles') { return new Profile(); } }); diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js index ef4d6df5138..a4d50a52315 100644 --- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -1,4 +1,5 @@ import PANEL_STATE from './constants'; +import { backOff } from '../lib/utils/common_utils'; export default class PrometheusMetrics { constructor(wrapperSelector) { @@ -79,7 +80,7 @@ export default class PrometheusMetrics { loadActiveMetrics() { this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING); - gl.utils.backOff((next, stop) => { + backOff((next, stop) => { $.getJSON(this.activeMetricsEndpoint) .done((res) => { if (res && res.success) { diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 003a15592f3..38c9a71dd20 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -1,4 +1,5 @@ /* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */ +import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils'; ((global) => { const KEYCODE = { @@ -146,14 +147,14 @@ } getCategoryContents() { - var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils; + var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName; userId = gon.current_user_id; userName = gon.current_username; - utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; - if (utils.isInGroupsPage() && groupOptions) { - options = groupOptions[utils.getGroupSlug()]; - } else if (utils.isInProjectPage() && projectOptions) { - options = projectOptions[utils.getProjectSlug()]; + projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; + if (isInGroupsPage() && groupOptions) { + options = groupOptions[getGroupSlug()]; + } else if (isInProjectPage() && projectOptions) { + options = projectOptions[getProjectSlug()]; } else if (dashboardOptions) { options = dashboardOptions; } diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js index 0da265053bd..a9fbc7f1a2f 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js @@ -1,6 +1,5 @@ import stopwatchSvg from 'icons/_icon_stopwatch.svg'; - -import '../../../lib/utils/pretty_time'; +import { abbreviateTime } from '../../../lib/utils/pretty_time'; export default { name: 'time-tracking-collapsed-state', @@ -79,7 +78,7 @@ export default { }, methods: { abbreviateTime(timeStr) { - return gl.utils.prettyTime.abbreviateTime(timeStr); + return abbreviateTime(timeStr); }, }, template: ` diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js index 40f5c89c5bb..fd0d4570d68 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js @@ -1,6 +1,4 @@ -import '../../../lib/utils/pretty_time'; - -const prettyTime = gl.utils.prettyTime; +import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time'; export default { name: 'time-tracking-comparison-pane', @@ -23,12 +21,12 @@ export default { }, }, computed: { - parsedRemaining() { + parsedTimeRemaining() { const diffSeconds = this.timeEstimate - this.timeSpent; - return prettyTime.parseSeconds(diffSeconds); + return parseSeconds(diffSeconds); }, timeRemainingHumanReadable() { - return prettyTime.stringifyTime(this.parsedRemaining); + return stringifyTime(this.parsedTimeRemaining); }, timeRemainingTooltip() { const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:'; @@ -44,13 +42,6 @@ export default { timeRemainingStatusClass() { return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate'; }, - /* Parsed time values */ - parsedEstimate() { - return prettyTime.parseSeconds(this.timeEstimate); - }, - parsedSpent() { - return prettyTime.parseSeconds(this.timeSpent); - }, }, template: ` <div class="time-tracking-comparison-pane"> diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js index a606852c22c..2fffe09c74e 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js @@ -1,6 +1,7 @@ /* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */ import UsersSelect from './users_select'; +import { isMetaClick } from './lib/utils/common_utils'; export default class Todos { constructor() { @@ -137,22 +138,17 @@ export default class Todos { goToTodoUrl(e) { const todoLink = this.dataset.url; - if (!todoLink) { + if (!todoLink || e.target.tagName === 'A' || e.target.tagName === 'IMG') { return; } - if (gl.utils.isMetaClick(e)) { + e.stopPropagation(); + e.preventDefault(); + + if (isMetaClick(e)) { const windowTarget = '_blank'; - const selected = e.target; - e.stopPropagation(); - e.preventDefault(); - - if (selected.tagName === 'IMG') { - const avatarUrl = selected.parentElement.getAttribute('href'); - window.open(avatarUrl, windowTarget); - } else { - window.open(todoLink, windowTarget); - } + + window.open(todoLink, windowTarget); } else { gl.utils.visitUrl(todoLink); } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js index aaca42e3ebc..219ff94924e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js @@ -72,12 +72,12 @@ export default { <a href="#modal_merge_info" data-toggle="modal" - class="btn btn-small inline"> + class="btn btn-sm inline"> Check out branch </a> <span class="dropdown prepend-left-10"> <a - class="btn btn-small inline dropdown-toggle" + class="btn btn-sm inline dropdown-toggle" data-toggle="dropdown" aria-label="Download as" role="button"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js index a4e34116c33..a8c686e5065 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js @@ -1,6 +1,6 @@ import statusCodes from '../../lib/utils/http_status'; import { bytesToMiB } from '../../lib/utils/number_utils'; - +import { backOff } from '../../lib/utils/common_utils'; import MemoryGraph from '../../vue_shared/components/memory_graph'; import MRWidgetService from '../services/mr_widget_service'; @@ -84,7 +84,7 @@ export default { } }, loadMetrics() { - gl.utils.backOff((next, stop) => { + backOff((next, stop) => { MRWidgetService.fetchMetrics(this.metricsUrl) .then((res) => { if (res.status === statusCodes.NO_CONTENT) { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js index 6c2e9ba1d30..c79b5c720eb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js @@ -12,6 +12,9 @@ export default { ciIcon, }, computed: { + hasPipeline() { + return this.mr.pipeline && Object.keys(this.mr.pipeline).length > 0; + }, hasCIError() { const { hasCI, ciStatus } = this.mr; @@ -28,7 +31,9 @@ export default { }, }, template: ` - <div class="mr-widget-heading"> + <div + v-if="hasPipeline || hasCIError" + class="mr-widget-heading"> <div class="ci-widget media"> <template v-if="hasCIError"> <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10"> @@ -40,7 +45,7 @@ export default { Could not connect to the CI server. Please check your settings and try again </div> </template> - <template v-else> + <template v-else-if="hasPipeline"> <div class="ci-status-icon append-right-10"> <a class="icon-link" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js index b01c923311b..703f3a56a34 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js @@ -27,7 +27,7 @@ export default { <button v-if="showDisabledButton" type="button" - class="btn btn-success btn-small" + class="btn btn-success btn-sm" disabled="true"> Merge </button> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js index 2b16a2d6817..b4e4a6aa161 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js @@ -11,7 +11,7 @@ export default { <status-icon status="failed" /> <button type="button" - class="btn btn-success btn-small" + class="btn btn-success btn-sm" disabled="true"> Merge </button> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js index 65187754009..ad709da51ee 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js @@ -29,6 +29,9 @@ export default { statusIcon, }, computed: { + shouldShowMergeWhenPipelineSucceedsText() { + return this.mr.isPipelineActive; + }, commitMessageLinkTitle() { const withDesc = 'Include description in commit message'; const withoutDesc = "Don't include description in commit message"; @@ -36,7 +39,7 @@ export default { return this.useCommitMessageWithDescription ? withoutDesc : withDesc; }, mergeButtonClass() { - const defaultClass = 'btn btn-small btn-success accept-merge-request'; + const defaultClass = 'btn btn-sm btn-success accept-merge-request'; const failedClass = `${defaultClass} btn-danger`; const inActionClass = `${defaultClass} btn-info`; const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr; @@ -56,7 +59,7 @@ export default { mergeButtonText() { if (this.isMergingImmediately) { return 'Merge in progress'; - } else if (this.mr.isPipelineActive) { + } else if (this.shouldShowMergeWhenPipelineSucceedsText) { return 'Merge when pipeline succeeds'; } @@ -68,7 +71,7 @@ export default { isMergeButtonDisabled() { const { commitMessage } = this; return Boolean(!commitMessage.length - || !this.isMergeAllowed() + || !this.shouldShowMergeControls() || this.isMakingRequest || this.mr.preventMerge); }, @@ -82,7 +85,12 @@ export default { }, methods: { isMergeAllowed() { - return !(this.mr.onlyAllowMergeIfPipelineSucceeds && this.mr.isPipelineFailed); + return !this.mr.onlyAllowMergeIfPipelineSucceeds || + this.mr.isPipelinePassing || + this.mr.isPipelineSkipped; + }, + shouldShowMergeControls() { + return this.isMergeAllowed() || this.shouldShowMergeWhenPipelineSucceedsText; }, updateCommitMessage() { const cmwd = this.mr.commitMessageWithDescription; @@ -202,8 +210,8 @@ export default { <div class="mr-widget-body media"> <status-icon status="success" /> <div class="media-body"> - <div class="media space-children"> - <span class="btn-group"> + <div class="mr-widget-body-controls media space-children"> + <span class="btn-group append-bottom-5"> <button @click="handleMergeButtonClick()" :disabled="isMergeButtonDisabled" @@ -219,7 +227,7 @@ export default { v-if="shouldShowMergeOptionsDropdown" :disabled="isMergeButtonDisabled" type="button" - class="btn btn-small btn-info dropdown-toggle js-merge-moment" + class="btn btn-sm btn-info dropdown-toggle js-merge-moment" data-toggle="dropdown" aria-label="Select merge moment"> <i @@ -260,8 +268,8 @@ export default { </li> </ul> </span> - <div class="media-body space-children"> - <template v-if="isMergeAllowed()"> + <div class="media-body-wrap space-children"> + <template v-if="shouldShowMergeControls()"> <label> <input id="remove-source-branch-input" @@ -286,7 +294,7 @@ export default { </template> <template v-else> <span class="bold"> - The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure + The pipeline for this merge request has not succeeded yet </span> </template> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js index 0042c48816f..044b664484b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -31,6 +31,7 @@ import { SquashBeforeMerge, notify, } from './dependencies'; +import { setFavicon } from '../lib/utils/common_utils'; export default { el: '#js-vue-mr-widget', @@ -57,7 +58,7 @@ export default { return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1; }, shouldRenderPipelines() { - return Object.keys(this.mr.pipeline).length || this.mr.hasCI; + return this.mr.hasCI; }, shouldRenderRelatedLinks() { return this.mr.relatedLinks; @@ -86,7 +87,7 @@ export default { .then((res) => { this.handleNotification(res); this.mr.setData(res); - this.setFavicon(); + this.setFaviconHelper(); if (cb) { cb.call(null, res); @@ -115,9 +116,9 @@ export default { immediateExecution: true, }); }, - setFavicon() { + setFaviconHelper() { if (this.mr.ciStatusFaviconPath) { - gl.utils.setFavicon(this.mr.ciStatusFaviconPath); + setFavicon(this.mr.ciStatusFaviconPath); } }, fetchDeployments() { @@ -193,7 +194,7 @@ export default { }); }, handleMounted() { - this.setFavicon(); + this.setFaviconHelper(); this.initDeploymentsPolling(); }, }, diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index fbea764b739..29464662578 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -85,7 +85,9 @@ export default class MergeRequestStore { this.ciEnvironmentsStatusPath = data.ci_environments_status_path; this.hasCI = data.has_ci; this.ciStatus = data.ci_status; - this.isPipelineFailed = this.ciStatus ? (this.ciStatus === 'failed' || this.ciStatus === 'canceled') : false; + this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled'; + this.isPipelinePassing = this.ciStatus === 'success' || this.ciStatus === 'success_with_warnings'; + this.isPipelineSkipped = this.ciStatus === 'skipped'; this.pipelineDetailedStatus = pipelineStatus; this.isPipelineActive = data.pipeline ? data.pipeline.active : false; this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false; diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js index 7f8e514fda1..b9693892f45 100644 --- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import VueResource from 'vue-resource'; +import csrf from '../lib/utils/csrf'; Vue.use(VueResource); @@ -18,9 +19,7 @@ Vue.http.interceptors.push((request, next) => { // New Vue Resource version uses Headers, we are expecting a plain object to render pagination // and polling. Vue.http.interceptors.push((request, next) => { - if ($.rails) { - request.headers.set('X-CSRF-Token', $.rails.csrfToken()); - } + request.headers.set(csrf.headerKey, csrf.token); next((response) => { // Headers object has a `forEach` property that iterates through all values. diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 35e7a10379f..923d14f2c3d 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -52,4 +52,3 @@ @import "framework/snippets"; @import "framework/memory_graph"; @import "framework/responsive-tables"; -@import "framework/feature_highlight"; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 82350c36df0..d178bc17462 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -46,15 +46,6 @@ } } -@mixin btn-svg { - svg { - height: 15px; - width: 15px; - position: relative; - top: 2px; - } -} - @mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) { background-color: $light; border-color: $border-light; @@ -132,7 +123,6 @@ .btn { @include btn-default; @include btn-white; - @include btn-svg; color: $gl-text-color; @@ -140,7 +130,6 @@ outline: 0; } - &.btn-small, &.btn-sm { padding: 4px 10px; font-size: 13px; @@ -232,6 +221,13 @@ } } + svg { + height: 15px; + width: 15px; + position: relative; + top: 2px; + } + svg, .fa { &:not(:last-child) { diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss deleted file mode 100644 index ebae473df50..00000000000 --- a/app/assets/stylesheets/framework/feature_highlight.scss +++ /dev/null @@ -1,94 +0,0 @@ -.feature-highlight { - position: relative; - margin-left: $gl-padding; - width: 20px; - height: 20px; - cursor: pointer; - - &::before { - content: ''; - display: block; - position: absolute; - top: 6px; - left: 6px; - width: 8px; - height: 8px; - background-color: $blue-500; - border-radius: 50%; - box-shadow: 0 0 0 rgba($blue-500, 0.4); - animation: pulse-highlight 2s infinite; - } - - &:hover::before, - &.disable-animation::before { - animation: none; - } - - &[disabled]::before { - display: none; - } -} - -.is-showing-fly-out { - .feature-highlight { - display: none; - } -} - -.feature-highlight-popover-content { - display: none; - - hr { - margin: $gl-padding * 0.5 0; - } - - .btn-link { - @include btn-svg; - - svg path { - fill: currentColor; - } - } - - .dismiss-feature-highlight { - padding: 0; - } - - svg:first-child { - width: 100%; - background-color: $indigo-50; - border-top-left-radius: 2px; - border-top-right-radius: 2px; - border-bottom: 1px solid darken($gray-normal, 8%); - } -} - -.popover .feature-highlight-popover-content { - display: block; -} - -.feature-highlight-popover { - padding: 0; - - .popover-content { - padding: 0; - } -} - -.feature-highlight-popover-sub-content { - padding: 9px 14px; -} - -@include keyframes(pulse-highlight) { - 0% { - box-shadow: 0 0 0 0 rgba($blue-200, 0.4); - } - - 70% { - box-shadow: 0 0 0 10px transparent; - } - - 100% { - box-shadow: 0 0 0 0 transparent; - } -} diff --git a/app/assets/stylesheets/framework/media_object.scss b/app/assets/stylesheets/framework/media_object.scss index b573052c14a..89c561479cc 100644 --- a/app/assets/stylesheets/framework/media_object.scss +++ b/app/assets/stylesheets/framework/media_object.scss @@ -6,3 +6,7 @@ .media-body { flex: 1; } + +.media-body-wrap { + flex-grow: 1; +} diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index 58e205537ef..8c5bafac637 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -375,8 +375,6 @@ header.navbar-gitlab-new { display: flex; width: 100%; position: relative; - padding-top: $gl-padding; - padding-bottom: $gl-padding; align-items: center; border-bottom: 1px solid $border-color; } @@ -388,6 +386,11 @@ header.navbar-gitlab-new { align-self: center; color: $gl-text-color-secondary; + @media (max-width: $screen-xs-max) { + padding-left: 17px; + border-left: 1px solid $gl-text-color-quaternary; + } + .avatar-tile { margin-right: 4px; border: 1px solid $border-color; diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index 8030854e527..9c404b7e542 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -192,7 +192,11 @@ $new-sidebar-collapsed-width: 50px; .nav-sidebar-inner-scroll { height: 100%; width: 100%; - overflow: scroll; + overflow: auto; + + @media (min-width: $screen-sm-min) { + overflow: hidden; + } } .with-performance-bar .nav-sidebar { @@ -441,9 +445,8 @@ $new-sidebar-collapsed-width: 50px; background-color: transparent; border: 0; padding: 6px 16px; - margin: 0 16px 0 -15px; + margin: 0 0 0 -15px; height: 46px; - border-right: 1px solid $gl-text-color-quaternary; i { font-size: 20px; @@ -451,7 +454,12 @@ $new-sidebar-collapsed-width: 50px; } @media (max-width: $screen-xs-max) { - display: inline-block; + display: flex; + align-items: center; + + i { + font-size: 18px; + } } } diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss new file mode 100644 index 00000000000..6c555aee20a --- /dev/null +++ b/app/assets/stylesheets/pages/admin.scss @@ -0,0 +1,6 @@ +.info-well { + .admin-well-statistics, + .admin-well-features { + padding-bottom: 46px; + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 439636fe026..09a14578dd3 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -356,6 +356,10 @@ } } +.mr-widget-body-controls { + flex-wrap: wrap; +} + .mr_source_commit, .mr_target_commit { margin-bottom: 0; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index e437bad4912..46d31e41ada 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -727,6 +727,12 @@ ul.notes { border-bottom-left-radius: 0; } + .btn { + svg path { + fill: $gray-darkest; + } + } + .btn.discussion-create-issue-btn { margin-left: -4px; border-radius: 0; @@ -741,10 +747,6 @@ ul.notes { border: 0; } } - - .new-issue-for-discussion path { - fill: $gray-darkest; - } } } @@ -778,6 +780,7 @@ ul.notes { background-color: transparent; border: none; outline: 0; + color: $gray-darkest; transition: color $general-hover-transition-duration $general-hover-transition-curve; &.is-disabled { @@ -801,7 +804,7 @@ ul.notes { } svg { - fill: $gray-darkest; + fill: currentColor; height: 16px; width: 16px; } @@ -816,16 +819,6 @@ ul.notes { vertical-align: middle; } -.discussion-next-btn { - svg { - margin: 0; - - path { - fill: $gray-darkest; - } - } -} - // Merge request notes in diffs .diff-file { // Diff is inline diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 7dfcf7b7d9c..4d4d92f9494 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -56,7 +56,6 @@ .tree-content-holder { display: flex; - max-height: 100vh; min-height: 300px; } @@ -156,7 +155,7 @@ list-style-type: none; background: $gray-normal; display: inline-block; - padding: 10px 18px; + padding: #{$gl-padding / 2} $gl-padding; border-right: 1px solid $white-dark; border-bottom: 1px solid $white-dark; white-space: nowrap; @@ -180,10 +179,9 @@ a { @include str-truncated(100px); color: $black; - width: 100px; - text-align: center; vertical-align: middle; text-decoration: none; + margin-right: 12px; &.close { width: auto; @@ -193,6 +191,10 @@ } } + .close-icon:hover { + color: $hint-color; + } + .close-icon, .unsaved-icon { float: right; |