diff options
Diffstat (limited to 'app/assets')
431 files changed, 4676 insertions, 2986 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 1f34c6b50c2..464611f66f0 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -9,7 +9,7 @@ const Api = { projectsPath: '/api/:version/projects.json', projectPath: '/api/:version/projects/:id', projectLabelsPath: '/:namespace_path/:project_path/labels', - groupLabelsPath: '/groups/:namespace_path/labels', + groupLabelsPath: '/groups/:namespace_path/-/labels', licensePath: '/api/:version/templates/licenses/:key', gitignorePath: '/api/:version/templates/gitignores/:key', gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key', @@ -32,7 +32,7 @@ const Api = { }, // Return groups list. Filtered by query - groups(query, options, callback) { + groups(query, options, callback = $.noop) { const url = Api.buildUrl(Api.groupsPath); return axios.get(url, { params: Object.assign({ diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index 0f28bd233ac..0da872db7e5 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -3,10 +3,10 @@ import AccessorUtilities from './lib/utils/accessor'; export default class Autosave { - constructor(field, key, resource) { + constructor(field, key) { this.field = field; + this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); - this.resource = resource; if (key.join != null) { key = key.join('/'); } @@ -17,31 +17,27 @@ export default class Autosave { } restore() { - var text; - if (!this.isLocalStorageAvailable) return; + if (!this.field.length) return; - text = window.localStorage.getItem(this.key); + const text = window.localStorage.getItem(this.key); if ((text != null ? text.length : void 0) > 0) { this.field.val(text); } - if (!this.resource && this.resource !== 'issue') { - this.field.trigger('input'); - } else { - // v-model does not update with jQuery trigger - // https://github.com/vuejs/vue/issues/2804#issuecomment-216968137 - const event = new Event('change', { bubbles: true, cancelable: false }); - const field = this.field.get(0); - if (field) { - field.dispatchEvent(event); - } - } + + this.field.trigger('input'); + // v-model does not update with jQuery trigger + // https://github.com/vuejs/vue/issues/2804#issuecomment-216968137 + const event = new Event('change', { bubbles: true, cancelable: false }); + const field = this.field.get(0); + field.dispatchEvent(event); } save() { - var text; - text = this.field.val(); + if (!this.field.length) return; + + const text = this.field.val(); if (this.isLocalStorageAvailable && (text != null ? text.length : void 0) > 0) { return window.localStorage.setItem(this.key, text); diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 87109a802e5..26e62732b33 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -2,7 +2,7 @@ import _ from 'underscore'; import Cookies from 'js-cookie'; import { __ } from './locale'; -import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils'; +import { isInIssuePage, isInMRPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils'; import flash from './flash'; import axios from './lib/utils/axios_utils'; @@ -50,10 +50,8 @@ class AwardsHandler { this.registerEventListener('on', $('html'), 'click', (e) => { const $target = $(e.target); - if (!$target.closest('.emoji-menu-content').length) { - $('.js-awards-block.current').removeClass('current'); - } if (!$target.closest('.emoji-menu').length) { + $('.js-awards-block.current').removeClass('current'); if ($('.emoji-menu').is(':visible')) { $('.js-add-award.is-active').removeClass('is-active'); this.hideMenuElement($('.emoji-menu')); @@ -241,9 +239,9 @@ class AwardsHandler { } addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { - const isMainAwardsBlock = votesBlock.closest('.js-issue-note-awards').length; + const isMainAwardsBlock = votesBlock.closest('.js-noteable-awards').length; - if (isInIssuePage() && !isMainAwardsBlock) { + if (this.isInVueNoteablePage() && !isMainAwardsBlock) { const id = votesBlock.attr('id').replace('note_', ''); this.hideMenuElement($('.emoji-menu')); @@ -295,8 +293,16 @@ class AwardsHandler { } } + isVueMRDiscussions() { + return isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible'); + } + + isInVueNoteablePage() { + return isInIssuePage() || this.isVueMRDiscussions(); + } + getVotesBlock() { - if (isInIssuePage()) { + if (this.isInVueNoteablePage()) { const $el = $('.js-add-award.is-active').closest('.note.timeline-entry'); if ($el.length) { @@ -314,7 +320,7 @@ class AwardsHandler { } getAwardUrl() { - return this.getVotesBlock().data('award-url'); + return this.getVotesBlock().data('awardUrl'); } checkMutuality(votesBlock, emoji) { diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js index cdea625fc8c..b669b63d23c 100644 --- a/app/assets/javascripts/behaviors/copy_to_clipboard.js +++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js @@ -2,7 +2,7 @@ import Clipboard from 'clipboard'; function showTooltip(target, title) { const $target = $(target); - const originalTitle = $target.data('original-title'); + const originalTitle = $target.data('originalTitle'); if (!$target.data('hideTooltip')) { $target diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 2cf8f4fa935..312edc0cd69 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -43,7 +43,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => { const $form = $(e.target).closest('form'); const $submitButton = $form.find('input[type=submit], button[type=submit]').first(); - if (!$submitButton.attr('disabled')) { + if (!$submitButton.prop('disabled')) { $submitButton.trigger('click', [e]); if (!isInIssuePage()) { diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index 035a7e5c431..e10cb2e3dc4 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -40,7 +40,7 @@ $.fn.requiresInput = function requiresInput() { // based on the option selected function hideOrShowHelpBlock(form) { const selected = $('.js-select-namespace option:selected'); - if (selected.length && selected.data('options-parent') === 'groups') { + if (selected.length && selected.data('optionsParent') === 'groups') { form.find('.help-block').hide(); } else if (selected.length) { form.find('.help-block').show(); diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js index 062577af385..06ef86ecb77 100644 --- a/app/assets/javascripts/blob/balsamiq_viewer.js +++ b/app/assets/javascripts/blob/balsamiq_viewer.js @@ -7,7 +7,7 @@ function onError() { return flash; } -function loadBalsamiqFile() { +export default function loadBalsamiqFile() { const viewer = document.getElementById('js-balsamiq-viewer'); if (!(viewer instanceof Element)) return; @@ -17,5 +17,3 @@ function loadBalsamiqFile() { const balsamiqViewer = new BalsamiqViewer(viewer); balsamiqViewer.loadFile(endpoint).catch(onError); } - -$(loadBalsamiqFile); diff --git a/app/assets/javascripts/blob/notebook_viewer.js b/app/assets/javascripts/blob/notebook_viewer.js index b7a0a195a92..226ae69893e 100644 --- a/app/assets/javascripts/blob/notebook_viewer.js +++ b/app/assets/javascripts/blob/notebook_viewer.js @@ -1,3 +1,3 @@ import renderNotebook from './notebook'; -document.addEventListener('DOMContentLoaded', renderNotebook); +export default renderNotebook; diff --git a/app/assets/javascripts/blob/pdf_viewer.js b/app/assets/javascripts/blob/pdf_viewer.js index 91abe9dd699..cabbb396ea7 100644 --- a/app/assets/javascripts/blob/pdf_viewer.js +++ b/app/assets/javascripts/blob/pdf_viewer.js @@ -1,3 +1,3 @@ import renderPDF from './pdf'; -document.addEventListener('DOMContentLoaded', renderPDF); +export default renderPDF; diff --git a/app/assets/javascripts/blob/sketch_viewer.js b/app/assets/javascripts/blob/sketch_viewer.js index 0640dd26855..2c1c6339fdb 100644 --- a/app/assets/javascripts/blob/sketch_viewer.js +++ b/app/assets/javascripts/blob/sketch_viewer.js @@ -1,8 +1,8 @@ /* eslint-disable no-new */ import SketchLoader from './sketch'; -document.addEventListener('DOMContentLoaded', () => { +export default () => { const el = document.getElementById('js-sketch-viewer'); new SketchLoader(el); -}); +}; diff --git a/app/assets/javascripts/blob/stl_viewer.js b/app/assets/javascripts/blob/stl_viewer.js index f611c4fe640..63236b6477f 100644 --- a/app/assets/javascripts/blob/stl_viewer.js +++ b/app/assets/javascripts/blob/stl_viewer.js @@ -1,6 +1,6 @@ import Renderer from './3d_viewer'; -document.addEventListener('DOMContentLoaded', () => { +export default () => { const viewer = new Renderer(document.getElementById('js-stl-viewer')); [].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => { @@ -16,4 +16,4 @@ document.addEventListener('DOMContentLoaded', () => { viewer.changeObjectMaterials(target.dataset.type); }); }); -}); +}; diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 612f604e725..92ea91c45a8 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -5,6 +5,7 @@ import axios from '../../lib/utils/axios_utils'; export default class BlobViewer { constructor() { BlobViewer.initAuxiliaryViewer(); + BlobViewer.initRichViewer(); this.initMainViewers(); } @@ -16,6 +17,38 @@ export default class BlobViewer { BlobViewer.loadViewer(auxiliaryViewer); } + static initRichViewer() { + const viewer = document.querySelector('.blob-viewer[data-type="rich"]'); + if (!viewer || !viewer.dataset.richType) return; + + const initViewer = promise => promise + .then(module => module.default(viewer)) + .catch((error) => { + Flash('Error loading file viewer.'); + throw error; + }); + + switch (viewer.dataset.richType) { + case 'balsamiq': + initViewer(import(/* webpackChunkName: 'balsamiq_viewer' */ '../balsamiq_viewer')); + break; + case 'notebook': + initViewer(import(/* webpackChunkName: 'notebook_viewer' */ '../notebook_viewer')); + break; + case 'pdf': + initViewer(import(/* webpackChunkName: 'pdf_viewer' */ '../pdf_viewer')); + break; + case 'sketch': + initViewer(import(/* webpackChunkName: 'sketch_viewer' */ '../sketch_viewer')); + break; + case 'stl': + initViewer(import(/* webpackChunkName: 'stl_viewer' */ '../stl_viewer')); + break; + default: + break; + } + } + initMainViewers() { this.$fileHolder = $('.file-holder'); if (!this.$fileHolder.length) return; diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index 6b06344f5ba..931ed042dfd 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -4,16 +4,16 @@ import NewCommitForm from '../new_commit_form'; import EditBlob from './edit_blob'; import BlobFileDropzone from '../blob/blob_file_dropzone'; -$(() => { +export default () => { const editBlobForm = $('.js-edit-blob-form'); const uploadBlobForm = $('.js-upload-blob-form'); const deleteBlobForm = $('.js-delete-blob-form'); if (editBlobForm.length) { - const urlRoot = editBlobForm.data('relative-url-root'); - const assetsPath = editBlobForm.data('assets-prefix'); - const blobLanguage = editBlobForm.data('blob-language'); - const currentAction = $('.js-file-title').data('current-action'); + const urlRoot = editBlobForm.data('relativeUrlRoot'); + const assetsPath = editBlobForm.data('assetsPrefix'); + const blobLanguage = editBlobForm.data('blobLanguage'); + const currentAction = $('.js-file-title').data('currentAction'); new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction); new NewCommitForm(editBlobForm); @@ -34,4 +34,4 @@ $(() => { if (deleteBlobForm.length) { new NewCommitForm(deleteBlobForm); } -}); +}; diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index a25f7fb3dcd..d4f6adaccbc 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -59,7 +59,7 @@ export default class EditBlob { if (paneId === '#preview') { this.$toggleButton.hide(); - axios.post(currentLink.data('preview-url'), { + axios.post(currentLink.data('previewUrl'), { content: this.editor.getValue(), }) .then(({ data }) => { diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 9a0442e2afe..6637904d87d 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -1,6 +1,6 @@ <script> import Sortable from 'vendor/Sortable'; -import boardNewIssue from './board_new_issue'; +import boardNewIssue from './board_new_issue.vue'; import boardCard from './board_card.vue'; import eventHub from '../eventhub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.vue index bc28f7f45f4..efface7143d 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -1,5 +1,6 @@ -/* global ListIssue */ +<script> import eventHub from '../eventhub'; +import ListIssue from '../models/issue'; const Store = gl.issueBoards.BoardsStore; @@ -17,6 +18,9 @@ export default { error: false, }; }, + mounted() { + this.$refs.input.focus(); + }, methods: { submit(e) { e.preventDefault(); @@ -59,42 +63,51 @@ export default { eventHub.$emit(`hide-issue-form-${this.list.id}`); }, }, - mounted() { - this.$refs.input.focus(); - }, - template: ` - <div class="card board-new-issue-form"> - <form @submit="submit($event)"> - <div class="flash-container" - v-if="error"> - <div class="flash-alert"> - An error occurred. Please try again. - </div> - </div> - <label class="label-light" - :for="list.id + '-title'"> - Title - </label> - <input class="form-control" - type="text" - v-model="title" - ref="input" - autocomplete="off" - :id="list.id + '-title'" /> - <div class="clearfix prepend-top-10"> - <button class="btn btn-success pull-left" - type="submit" - :disabled="title === ''" - ref="submit-button"> - Submit issue - </button> - <button class="btn btn-default pull-right" - type="button" - @click="cancel"> - Cancel - </button> - </div> - </form> - </div> - `, }; +</script> + +<template> + <div class="card board-new-issue-form"> + <form @submit="submit($event)"> + <div + class="flash-container" + v-if="error" + > + <div class="flash-alert"> + An error occurred. Please try again. + </div> + </div> + <label + class="label-light" + :for="list.id + '-title'" + > + Title + </label> + <input + class="form-control" + type="text" + v-model="title" + ref="input" + autocomplete="off" + :id="list.id + '-title'" + /> + <div class="clearfix prepend-top-10"> + <button + class="btn btn-success pull-left" + type="submit" + :disabled="title === ''" + ref="submit-button" + > + Submit issue + </button> + <button + class="btn btn-default pull-right" + type="button" + @click="cancel" + > + Cancel + </button> + </div> + </form> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 983429550f0..9501e35b178 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -2,10 +2,11 @@ import Vue from 'vue'; import Flash from '../../flash'; +import { __ } from '../../locale'; import Sidebar from '../../right_sidebar'; import eventHub from '../../sidebar/event_hub'; import assigneeTitle from '../../sidebar/components/assignees/assignee_title'; -import assignees from '../../sidebar/components/assignees/assignees'; +import assignees from '../../sidebar/components/assignees/assignees.vue'; import DueDateSelectors from '../../due_date_select'; import './sidebar/remove_issue'; import IssuableContext from '../../issuable_context'; @@ -95,7 +96,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({ }) .catch(() => { this.loadingAssignees = false; - return new Flash('An error occurred while saving assignees'); + Flash(__('An error occurred while saving assignees')); }); }, }, diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index 182957113a2..03cd7ef65cb 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -1,7 +1,6 @@ -/* eslint-disable no-new */ - import Vue from 'vue'; import Flash from '../../../flash'; +import { __ } from '../../../locale'; import './lists_dropdown'; import { pluralize } from '../../../lib/utils/text_utility'; @@ -36,7 +35,7 @@ gl.issueBoards.ModalFooter = Vue.extend({ gl.boardService.bulkUpdate(issueIds, { add_label_ids: [list.label.id], }).catch(() => { - new Flash('Failed to update issues, please try again.', 'alert'); + Flash(__('Failed to update issues, please try again.')); selectedIssues.forEach((issue) => { list.removeIssue(issue); diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index c19c989680d..362ef43e6f7 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, no-new, space-before-function-paren, one-var, promise/catch-or-return */ +import axios from '~/lib/utils/axios_utils'; import _ from 'underscore'; import CreateLabelDropdown from '../../create_label'; @@ -24,13 +25,13 @@ $(document).off('created.label').on('created.label', (e, label) => { gl.issueBoards.newListDropdownInit = () => { $('.js-new-board-list').each(function () { const $this = $(this); - new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path')); + new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespacePath'), $this.data('projectPath')); $this.glDropdown({ data(term, callback) { - $.get($this.attr('data-list-labels-path')) - .then((resp) => { - callback(resp); + axios.get($this.attr('data-list-labels-path')) + .then(({ data }) => { + callback(data); }); }, renderRow (label) { diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js index 1ad97211934..0ae32bb4d0a 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js @@ -1,7 +1,6 @@ -/* eslint-disable no-new */ - import Vue from 'vue'; import Flash from '../../../flash'; +import { __ } from '../../../locale'; const Store = gl.issueBoards.BoardsStore; @@ -45,7 +44,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ }, }; Vue.http.patch(this.updateUrl, data).catch(() => { - new Flash('Failed to remove issue from board, please try again.', 'alert'); + Flash(__('Failed to remove issue from board, please try again.')); lists.forEach((list) => { list.addIssue(issue); diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 184665f395c..57a7cc4ca30 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -1,9 +1,12 @@ /* eslint-disable class-methods-use-this */ import FilteredSearchContainer from '../filtered_search/container'; +import FilteredSearchManager from '../filtered_search/filtered_search_manager'; -export default class FilteredSearchBoards extends gl.FilteredSearchManager { +export default class FilteredSearchBoards extends FilteredSearchManager { constructor(store, updateUrl = false, cantEdit = []) { - super('boards'); + super({ + page: 'boards', + }); this.store = store; this.updateUrl = updateUrl; diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/index.js index 90166b3d3d1..8e31f1865f0 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/index.js @@ -2,11 +2,13 @@ import _ from 'underscore'; import Vue from 'vue'; -import Flash from '../flash'; -import { __ } from '../locale'; + +import Flash from '~/flash'; +import { __ } from '~/locale'; + import FilteredSearchBoards from './filtered_search_boards'; import eventHub from './eventhub'; -import sidebarEventHub from '../sidebar/event_hub'; +import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import/first import './models/issue'; import './models/label'; import './models/list'; @@ -22,9 +24,9 @@ import './components/board'; import './components/board_sidebar'; import './components/new_list_dropdown'; import './components/modal/index'; -import '../vue_shared/vue_resource_interceptor'; +import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/first -$(() => { +export default () => { const $boardApp = document.getElementById('board-app'); const Store = gl.issueBoards.BoardsStore; const ModalStore = gl.issueBoards.ModalStore; @@ -236,4 +238,4 @@ $(() => { </div> `, }); -}); +}; diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index 81edd95bf2b..3bfb6d39ad5 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -110,3 +110,5 @@ class ListIssue { } window.ListIssue = ListIssue; + +export default ListIssue; diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 798d7e0d147..348cdeec737 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -2,7 +2,7 @@ /* global List */ import _ from 'underscore'; import Cookies from 'js-cookie'; -import { getUrlParamsArray } from '../../lib/utils/common_utils'; +import { getUrlParamsArray } from '~/lib/utils/common_utils'; window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js index 76f93e5c6bd..b33adff609f 100644 --- a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js @@ -75,6 +75,7 @@ export default class AjaxVariableList { if (res.status === statusCodes.OK && res.data) { this.updateRowsWithPersistedVariables(res.data.variables); + this.variableList.hideValues(); } else if (res.status === statusCodes.BAD_REQUEST) { // Validation failed this.errorBox.innerHTML = generateErrorBoxContent(res.data); diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js index d91789c2192..745f3404295 100644 --- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js @@ -39,7 +39,7 @@ export default class VariableList { }, protected: { selector: '.js-ci-variable-input-protected', - default: 'true', + default: 'false', }, environment_scope: { // We can't use a `.js-` class here because @@ -178,6 +178,10 @@ export default class VariableList { this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled); } + hideValues() { + this.secretValues.updateDom(false); + } + getAllData() { // Ignore the last empty row because we don't want to try persist // a blank variable and run into validation problems. diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 3d6ec37e6dd..01aec4f36af 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -32,14 +32,17 @@ export default class Clusters { installIngressPath, installRunnerPath, installPrometheusPath, + managePrometheusPath, clusterStatus, clusterStatusReason, helpPath, ingressHelpPath, + ingressDnsHelpPath, } = document.querySelector('.js-edit-cluster-form').dataset; this.store = new ClustersStore(); - this.store.setHelpPaths(helpPath, ingressHelpPath); + this.store.setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath); + this.store.setManagePrometheusPath(managePrometheusPath); this.store.updateStatus(clusterStatus); this.store.updateStatusReason(clusterStatusReason); this.service = new ClustersService({ @@ -95,6 +98,8 @@ export default class Clusters { applications: this.state.applications, helpPath: this.state.helpPath, ingressHelpPath: this.state.ingressHelpPath, + managePrometheusPath: this.state.managePrometheusPath, + ingressDnsHelpPath: this.state.ingressDnsHelpPath, }, }); }, diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index c13bbcee863..c2a35341eb2 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -32,9 +32,9 @@ type: String, required: false, }, - description: { + manageLink: { type: String, - required: true, + required: false, }, status: { type: String, @@ -89,6 +89,12 @@ return label; }, + showManageButton() { + return this.manageLink && this.status === APPLICATION_INSTALLED; + }, + manageButtonLabel() { + return s__('ClusterIntegration|Manage'); + }, hasError() { return this.status === APPLICATION_ERROR || this.requestStatus === REQUEST_FAILURE; @@ -138,12 +144,24 @@ class="table-section section-wrap" role="gridcell" > - <div v-html="description"></div> + <slot name="description"></slot> </div> <div - class="table-section table-button-footer section-15 section-align-top" + class="table-section table-button-footer section-align-top" + :class="{ 'section-20': showManageButton, 'section-15': !showManageButton }" role="gridcell" > + <div + v-if="showManageButton" + class="btn-group table-action-buttons" + > + <a + class="btn" + :href="manageLink" + > + {{ manageButtonLabel }} + </a> + </div> <div class="btn-group table-action-buttons"> <loading-button class="js-cluster-application-install-button" diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index f4259700370..1325a268214 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -2,10 +2,16 @@ import _ from 'underscore'; import { s__, sprintf } from '../../locale'; import applicationRow from './application_row.vue'; + import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; + import { + APPLICATION_INSTALLED, + INGRESS, + } from '../constants'; export default { components: { applicationRow, + clipboardButton, }, props: { applications: { @@ -23,13 +29,24 @@ required: false, default: '', }, + ingressDnsHelpPath: { + type: String, + required: false, + default: '', + }, + managePrometheusPath: { + type: String, + required: false, + default: '', + }, }, computed: { generalApplicationDescription() { return sprintf( - _.escape(s__(`ClusterIntegration|Install applications on your Kubernetes cluster. - Read more about %{helpLink}`)), - { + _.escape(s__( + `ClusterIntegration|Install applications on your Kubernetes cluster. + Read more about %{helpLink}`, + )), { helpLink: `<a href="${this.helpPath}"> ${_.escape(s__('ClusterIntegration|installing applications'))} </a>`, @@ -37,19 +54,16 @@ false, ); }, - helmTillerDescription() { - return _.escape(s__( - `ClusterIntegration|Helm streamlines installing and managing Kubernetes applications. - Tiller runs inside of your Kubernetes Cluster, and manages - releases of your charts.`, - )); + ingressId() { + return INGRESS; + }, + ingressInstalled() { + return this.applications.ingress.status === APPLICATION_INSTALLED; + }, + ingressExternalIp() { + return this.applications.ingress.externalIp; }, ingressDescription() { - const descriptionParagraph = _.escape(s__( - `ClusterIntegration|Ingress gives you a way to route requests to services based on the - request host or path, centralizing a number of services into a single entrypoint.`, - )); - const extraCostParagraph = sprintf( _.escape(s__( `ClusterIntegration|%{boldNotice} This will add some extra resources @@ -78,9 +92,6 @@ return ` <p> - ${descriptionParagraph} - </p> - <p> ${extraCostParagraph} </p> <p class="settings-message append-bottom-0"> @@ -88,19 +99,14 @@ </p> `; }, - gitlabRunnerDescription() { - return _.escape(s__( - `ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs - and send the results back to GitLab.`, - )); - }, prometheusDescription() { return sprintf( - _.escape(s__(`ClusterIntegration|Prometheus is an open-source monitoring system - with %{gitlabIntegrationLink} to monitor deployed applications.`)), - { + _.escape(s__( + `ClusterIntegration|Prometheus is an open-source monitoring system + with %{gitlabIntegrationLink} to monitor deployed applications.`, + )), { gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html" -target="_blank" rel="noopener noreferrer"> + target="_blank" rel="noopener noreferrer"> ${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`, }, false, @@ -129,32 +135,137 @@ target="_blank" rel="noopener noreferrer"> id="helm" :title="applications.helm.title" title-link="https://docs.helm.sh/" - :description="helmTillerDescription" :status="applications.helm.status" :status-reason="applications.helm.statusReason" :request-status="applications.helm.requestStatus" :request-reason="applications.helm.requestReason" - /> + > + <div slot="description"> + {{ s__(`ClusterIntegration|Helm streamlines installing + and managing Kubernetes applications. + Tiller runs inside of your Kubernetes Cluster, + and manages releases of your charts.`) }} + </div> + </application-row> <application-row - id="ingress" + :id="ingressId" :title="applications.ingress.title" title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" - :description="ingressDescription" :status="applications.ingress.status" :status-reason="applications.ingress.statusReason" :request-status="applications.ingress.requestStatus" :request-reason="applications.ingress.requestReason" - /> + > + <div slot="description"> + <p> + {{ s__(`ClusterIntegration|Ingress gives you a way to route + requests to services based on the request host or path, + centralizing a number of services into a single entrypoint.`) }} + </p> + + <template v-if="ingressInstalled"> + <div class="form-group"> + <label for="ingress-ip-address"> + {{ s__('ClusterIntegration|Ingress IP Address') }} + </label> + <div + v-if="ingressExternalIp" + class="input-group" + > + <input + type="text" + id="ingress-ip-address" + class="form-control js-ip-address" + :value="ingressExternalIp" + readonly + /> + <span class="input-group-btn"> + <clipboard-button + :text="ingressExternalIp" + :title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')" + css-class="btn btn-default js-clipboard-btn" + /> + </span> + </div> + <input + v-else + type="text" + class="form-control js-ip-address" + readonly + value="?" + /> + </div> + + <p + v-if="!ingressExternalIp" + class="settings-message js-no-ip-message" + > + {{ s__(`ClusterIntegration|The IP address is in + the process of being assigned. Please check your Kubernetes + cluster or Quotas on GKE if it takes a long time.`) }} + + <a + :href="ingressHelpPath" + target="_blank" + rel="noopener noreferrer" + > + {{ __('More information') }} + </a> + </p> + + <p> + {{ s__(`ClusterIntegration|Point a wildcard DNS to this + generated IP address in order to access + your application after it has been deployed.`) }} + <a + :href="ingressDnsHelpPath" + target="_blank" + rel="noopener noreferrer" + > + {{ __('More information') }} + </a> + </p> + + </template> + <div + v-else + v-html="ingressDescription" + > + </div> + </div> + </application-row> <application-row id="prometheus" :title="applications.prometheus.title" title-link="https://prometheus.io/docs/introduction/overview/" - :description="prometheusDescription" + :manage-link="managePrometheusPath" :status="applications.prometheus.status" :status-reason="applications.prometheus.statusReason" :request-status="applications.prometheus.requestStatus" :request-reason="applications.prometheus.requestReason" - /> + > + <div + slot="description" + v-html="prometheusDescription" + > + </div> + </application-row> + <application-row + id="runner" + :title="applications.runner.title" + title-link="https://docs.gitlab.com/runner/" + :status="applications.runner.status" + :status-reason="applications.runner.statusReason" + :request-status="applications.runner.requestStatus" + :request-reason="applications.runner.requestReason" + > + <div slot="description"> + {{ s__(`ClusterIntegration|GitLab Runner connects to this + project's repository and executes CI/CD jobs, + pushing results back and deploying, + applications to production.`) }} + </div> + </application-row> <!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js index 93223aefff8..b7179f52bb3 100644 --- a/app/assets/javascripts/clusters/constants.js +++ b/app/assets/javascripts/clusters/constants.js @@ -10,3 +10,4 @@ export const APPLICATION_ERROR = 'errored'; export const REQUEST_LOADING = 'request-loading'; export const REQUEST_SUCCESS = 'request-success'; export const REQUEST_FAILURE = 'request-failure'; +export const INGRESS = 'ingress'; diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 49c3d184ef9..348bbec3b25 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -1,4 +1,5 @@ import { s__ } from '../../locale'; +import { INGRESS } from '../constants'; export default class ClusterStore { constructor() { @@ -21,6 +22,7 @@ export default class ClusterStore { statusReason: null, requestStatus: null, requestReason: null, + externalIp: null, }, runner: { title: s__('ClusterIntegration|GitLab Runner'), @@ -40,9 +42,14 @@ export default class ClusterStore { }; } - setHelpPaths(helpPath, ingressHelpPath) { + setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath) { this.state.helpPath = helpPath; this.state.ingressHelpPath = ingressHelpPath; + this.state.ingressDnsHelpPath = ingressDnsHelpPath; + } + + setManagePrometheusPath(managePrometheusPath) { + this.state.managePrometheusPath = managePrometheusPath; } updateStatus(status) { @@ -60,6 +67,7 @@ export default class ClusterStore { updateStateFromServer(serverState = {}) { this.state.status = serverState.status; this.state.statusReason = serverState.status_reason; + serverState.applications.forEach((serverAppEntry) => { const { name: appId, @@ -72,6 +80,10 @@ export default class ClusterStore { status, statusReason, }; + + if (appId === INGRESS) { + this.state.applications.ingress.externalIp = serverAppEntry.external_ip; + } }); } } diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 525fbf9dac9..6504a0bbbfc 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -1,5 +1,4 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */ -import 'vendor/jquery.waitforimages'; // Width where images must fits in, for 2-up this gets divided by 2 const availWidth = 900; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 1f9153d95bd..3d89bf1316e 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -15,7 +15,7 @@ const CommitPipelinesTable = Vue.extend(commitPipelinesTable); window.gl = window.gl || {}; window.gl.CommitPipelinesTable = CommitPipelinesTable; -document.addEventListener('DOMContentLoaded', () => { +export default () => { const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); if (pipelineTableViewEl) { @@ -43,4 +43,4 @@ document.addEventListener('DOMContentLoaded', () => { pipelineTableViewEl.appendChild(table.$el); } } -}); +}; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index da0e8063ccb..ce19069f103 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -7,7 +7,6 @@ mixins: [ pipelinesMixin, ], - props: { endpoint: { type: String, diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 4b2f75fffde..2be63bd8c76 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -1,52 +1,36 @@ -/* eslint-disable func-names, wrap-iife, consistent-return, - no-return-assign, no-param-reassign, one-var-declaration-per-line, no-unused-vars, - prefer-template, object-shorthand, prefer-arrow-callback */ - import { pluralize } from './lib/utils/text_utility'; import { localTimeAgo } from './lib/utils/datetime_utility'; import Pager from './pager'; import axios from './lib/utils/axios_utils'; -export default (function () { - const CommitsList = {}; - - CommitsList.timer = null; +export default class CommitsList { + constructor(limit = 0) { + this.timer = null; - CommitsList.init = function (limit) { this.$contentList = $('.content_list'); - $('body').on('click', '.day-commits-table li.commit', function (e) { - if (e.target.nodeName !== 'A') { - location.href = $(this).attr('url'); - e.stopPropagation(); - return false; - } - }); - - Pager.init(parseInt(limit, 10), false, false, this.processCommits); + Pager.init(parseInt(limit, 10), false, false, this.processCommits.bind(this)); this.content = $('#commits-list'); this.searchField = $('#commits-search'); this.lastSearch = this.searchField.val(); - return this.initSearch(); - }; + this.initSearch(); + } - CommitsList.initSearch = function () { + initSearch() { this.timer = null; - return this.searchField.keyup((function (_this) { - return function () { - clearTimeout(_this.timer); - return _this.timer = setTimeout(_this.filterResults, 500); - }; - })(this)); - }; + this.searchField.on('keyup', () => { + clearTimeout(this.timer); + this.timer = setTimeout(this.filterResults.bind(this), 500); + }); + } - CommitsList.filterResults = function () { + filterResults() { const form = $('.commits-search-form'); - const search = CommitsList.searchField.val(); - if (search === CommitsList.lastSearch) return Promise.resolve(); - const commitsUrl = form.attr('action') + '?' + form.serialize(); - CommitsList.content.fadeTo('fast', 0.5); + const search = this.searchField.val(); + if (search === this.lastSearch) return Promise.resolve(); + const commitsUrl = `${form.attr('action')}?${form.serialize()}`; + this.content.fadeTo('fast', 0.5); const params = form.serializeArray().reduce((acc, obj) => Object.assign(acc, { [obj.name]: obj.value, }), {}); @@ -55,9 +39,9 @@ export default (function () { params, }) .then(({ data }) => { - CommitsList.lastSearch = search; - CommitsList.content.html(data.html); - CommitsList.content.fadeTo('fast', 1.0); + this.lastSearch = search; + this.content.html(data.html); + this.content.fadeTo('fast', 1.0); // Change url so if user reload a page - search results are saved history.replaceState({ @@ -65,16 +49,16 @@ export default (function () { }, document.title, commitsUrl); }) .catch(() => { - CommitsList.content.fadeTo('fast', 1.0); - CommitsList.lastSearch = null; + this.content.fadeTo('fast', 1.0); + this.lastSearch = null; }); - }; + } // Prepare loaded data. - CommitsList.processCommits = (data) => { + processCommits(data) { let processedData = data; const $processedData = $(processedData); - const $commitsHeadersLast = CommitsList.$contentList.find('li.js-commit-header').last(); + const $commitsHeadersLast = this.$contentList.find('li.js-commit-header').last(); const lastShownDay = $commitsHeadersLast.data('day'); const $loadedCommitsHeadersFirst = $processedData.filter('li.js-commit-header').first(); const loadedShownDayFirst = $loadedCommitsHeadersFirst.data('day'); @@ -97,7 +81,5 @@ export default (function () { localTimeAgo($processedData.find('.js-timeago')); return processedData; - }; - - return CommitsList; -})(); + } +} diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js index c11b7d5f340..db96da4ccba 100644 --- a/app/assets/javascripts/commons/bootstrap.js +++ b/app/assets/javascripts/commons/bootstrap.js @@ -13,6 +13,6 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/popover'; // custom jQuery functions $.fn.extend({ - disable() { return $(this).attr('disabled', 'disabled').addClass('disabled'); }, - enable() { return $(this).removeAttr('disabled').removeClass('disabled'); }, + disable() { return $(this).prop('disabled', true).addClass('disabled'); }, + enable() { return $(this).prop('disabled', false).removeClass('disabled'); }, }); diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js index b93e94a3c97..a7ed175f7a4 100644 --- a/app/assets/javascripts/commons/jquery.js +++ b/app/assets/javascripts/commons/jquery.js @@ -6,5 +6,5 @@ import 'vendor/jquery.endless-scroll'; import 'vendor/jquery.caret'; import 'vendor/jquery.atwho'; import 'vendor/jquery.scrollTo'; -import 'vendor/jquery.waitforimages'; +import 'jquery.waitforimages'; import 'select2/select2'; diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js index e2a008e8904..d5a35ed81a6 100644 --- a/app/assets/javascripts/compare.js +++ b/app/assets/javascripts/compare.js @@ -13,7 +13,7 @@ export default class Compare { $dropdown = $(dropdown); return $dropdown.glDropdown({ selectable: true, - fieldName: $dropdown.data('field-name'), + fieldName: $dropdown.data('fieldName'), filterable: true, id: function(obj, $el) { return $el.data('id'); diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js index 59899e97be1..fa341918fc1 100644 --- a/app/assets/javascripts/compare_autocomplete.js +++ b/app/assets/javascripts/compare_autocomplete.js @@ -9,7 +9,7 @@ export default function initCompareAutocomplete() { $dropdown = $(this); selected = $dropdown.data('selected'); const $dropdownContainer = $dropdown.closest('.dropdown'); - const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer); + const $fieldInput = $(`input[name="${$dropdown.data('fieldName')}"]`, $dropdownContainer); const $filterInput = $('input[type="search"]', $dropdownContainer); $dropdown.glDropdown({ data: function(term, callback) { @@ -25,7 +25,7 @@ export default function initCompareAutocomplete() { selectable: true, filterable: true, filterRemote: true, - fieldName: $dropdown.data('field-name'), + fieldName: $dropdown.data('fieldName'), filterInput: 'input[type="search"]', renderRow: function(ref) { var link; diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index 482d83621e2..fb1fc9cd32e 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -180,6 +180,7 @@ export default class CreateMergeRequestDropdown { valueAttribute: 'data-text', }, ], + hideOnClick: false, }; } diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue index 39b699a6395..34aa04083e6 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue @@ -37,7 +37,7 @@ > <div class="item-details"> <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> + <user-avatar-image :img-src="mergeRequest.author.avatarUrl" /> <h5 class="item-title merge-merquest-title"> <a :href="mergeRequest.url"> {{ mergeRequest.title }} diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 034f2923b3b..46d89c825f9 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -14,10 +14,10 @@ import CycleAnalyticsStore from './cycle_analytics_store'; Vue.use(Translate); -$(() => { +export default () => { const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; - gl.cycleAnalyticsApp = new Vue({ + new Vue({ // eslint-disable-line no-new el: '#cycle-analytics', name: 'CycleAnalytics', components: { @@ -132,4 +132,4 @@ $(() => { }, }, }); -}); +}; diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js index ca8798facc9..b727261648c 100644 --- a/app/assets/javascripts/deploy_keys/index.js +++ b/app/assets/javascripts/deploy_keys/index.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import deployKeysApp from './components/app.vue'; -document.addEventListener('DOMContentLoaded', () => new Vue({ +export default () => new Vue({ el: document.getElementById('js-deploy-keys'), components: { deployKeysApp, @@ -18,4 +18,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ }, }); }, -})); +}); diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index a162424b3cf..3df082e8c0c 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -1,3 +1,6 @@ +import axios from '~/lib/utils/axios_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; import { getLocationHash } from './lib/utils/url_utility'; import FilesCommentButton from './files_comment_button'; import SingleFileDiff from './single_file_diff'; @@ -65,11 +68,13 @@ export default class Diff { } const file = $target.parents('.diff-file'); - const link = file.data('blob-diff-path'); + const link = file.data('blobDiffPath'); const view = file.data('view'); const params = { since, to, bottom, offset, unfold, view }; - $.get(link, params, response => $target.parent().replaceWith(response)); + axios.get(link, { params }) + .then(({ data }) => $target.parent().replaceWith(data)) + .catch(() => flash(__('An error occurred while loading diff'))); } openAnchoredDiff(cb) { @@ -116,7 +121,7 @@ export default class Diff { } // eslint-disable-next-line class-methods-use-this diffViewType() { - return $('.inline-parallel-buttons a.active').data('view-type'); + return $('.inline-parallel-buttons a.active').data('viewType'); } // eslint-disable-next-line class-methods-use-this lineNumbers(line) { diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js index e77910a83d4..fadc34959e1 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js @@ -197,7 +197,7 @@ const JumpToDiscussion = Vue.extend({ } $.scrollTo($target, { - offset: 0 + offset: -150 }); } }, diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js index 20ddcbfb8bd..cc9192deae3 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js @@ -87,6 +87,7 @@ const ResolveBtn = Vue.extend({ CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by); this.discussion.updateHeadline(data); gl.mrWidget.checkStatus(); + document.dispatchEvent(new CustomEvent('refreshVueNotes')); this.updateTooltip(); }) diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index e0422057090..5f49609fe88 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -14,8 +14,9 @@ import './components/resolve_count'; import './components/resolve_discussion_btn'; import './components/diff_note_avatars'; import './components/new_issue_for_discussion'; +import { hasVueMRDiscussionsCookie } from '../lib/utils/common_utils'; -$(() => { +export default () => { const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box'); const projectPath = projectPathHolder.dataset.projectPath; const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn'; @@ -67,12 +68,14 @@ $(() => { gl.diffNotesCompileComponents(); - new Vue({ - el: '#resolve-count-app', - components: { - 'resolve-count': ResolveCount - } - }); + if (!hasVueMRDiscussionsCookie()) { + new Vue({ + el: '#resolve-count-app', + components: { + 'resolve-count': ResolveCount + }, + }); + } $(window).trigger('resize.nav'); -}); +}; diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js index 96fe23640af..d16f9297de1 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js +++ b/app/assets/javascripts/diff_notes/services/resolve.js @@ -8,8 +8,8 @@ window.gl = window.gl || {}; class ResolveServiceClass { constructor(root) { - this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`); - this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`); + this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`); + this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`); } resolve(noteId) { @@ -45,6 +45,7 @@ class ResolveServiceClass { if (gl.mrWidget) gl.mrWidget.checkStatus(); discussion.updateHeadline(data); + document.dispatchEvent(new CustomEvent('refreshVueNotes')); }) .catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.')); } diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index ab28b7d8d44..1ccf96a75dc 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,633 +1,85 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ -import MergeRequest from './merge_request'; import Flash from './flash'; import GfmAutoComplete from './gfm_auto_complete'; -import ZenMode from './zen_mode'; -import initNotes from './init_notes'; -import initIssuableSidebar from './init_issuable_sidebar'; import { convertPermissionToBoolean } from './lib/utils/common_utils'; import GlFieldErrors from './gl_field_errors'; import Shortcuts from './shortcuts'; -import ShortcutsIssuable from './shortcuts_issuable'; -import Diff from './diff'; import SearchAutocomplete from './search_autocomplete'; -var Dispatcher; - -(function() { - Dispatcher = (function() { - function Dispatcher() { - this.initSearch(); - this.initFieldErrors(); - this.initPageScripts(); - } - - Dispatcher.prototype.initPageScripts = function() { - var path, shortcut_handler; - const page = $('body').attr('data-page'); - if (!page) { - return false; - } - - const fail = () => Flash('Error loading dynamic module'); - const callDefault = m => m.default(); - - path = page.split(':'); - shortcut_handler = null; - - $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { - const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); - const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete); - gfm.setup($(el), { - emojis: true, - members: enableGFM, - issues: enableGFM, - milestones: enableGFM, - mergeRequests: enableGFM, - labels: enableGFM, - }); - }); - - switch (page) { - case 'projects:environments:metrics': - import('./pages/projects/environments/metrics') - .then(callDefault) - .catch(fail); - break; - case 'projects:merge_requests:index': - case 'projects:issues:index': - case 'projects:issues:show': - shortcut_handler = true; - break; - case 'projects:milestones:index': - import('./pages/projects/milestones/index') - .then(callDefault) - .catch(fail); - break; - case 'projects:milestones:show': - import('./pages/projects/milestones/show') - .then(callDefault) - .catch(fail); - break; - case 'groups:milestones:show': - import('./pages/groups/milestones/show') - .then(callDefault) - .catch(fail); - break; - case 'dashboard:milestones:show': - import('./pages/dashboard/milestones/show') - .then(callDefault) - .catch(fail); - break; - case 'dashboard:issues': - import('./pages/dashboard/issues') - .then(callDefault) - .catch(fail); - break; - case 'dashboard:merge_requests': - import('./pages/dashboard/merge_requests') - .then(callDefault) - .catch(fail); - break; - case 'groups:issues': - import('./pages/groups/issues') - .then(callDefault) - .catch(fail); - break; - case 'groups:merge_requests': - import('./pages/groups/merge_requests') - .then(callDefault) - .catch(fail); - break; - case 'dashboard:todos:index': - import('./pages/dashboard/todos/index') - .then(callDefault) - .catch(fail); - break; - case 'admin:jobs:index': - import('./pages/admin/jobs/index') - .then(callDefault) - .catch(fail); - break; - case 'dashboard:projects:index': - case 'dashboard:projects:starred': - import('./pages/dashboard/projects') - .then(callDefault) - .catch(fail); - break; - case 'explore:projects:index': - case 'explore:projects:trending': - case 'explore:projects:starred': - import('./pages/explore/projects') - .then(callDefault) - .catch(fail); - break; - case 'explore:groups:index': - import('./pages/explore/groups') - .then(callDefault) - .catch(fail); - break; - case 'projects:milestones:new': - case 'projects:milestones:create': - import('./pages/projects/milestones/new') - .then(callDefault) - .catch(fail); - break; - case 'projects:milestones:edit': - case 'projects:milestones:update': - import('./pages/projects/milestones/edit') - .then(callDefault) - .catch(fail); - break; - case 'groups:milestones:new': - case 'groups:milestones:create': - import('./pages/groups/milestones/new') - .then(callDefault) - .catch(fail); - break; - case 'groups:milestones:edit': - case 'groups:milestones:update': - import('./pages/groups/milestones/edit') - .then(callDefault) - .catch(fail); - break; - case 'projects:compare:show': - import('./pages/projects/compare/show') - .then(callDefault) - .catch(fail); - break; - case 'projects:branches:new': - import('./pages/projects/branches/new') - .then(callDefault) - .catch(fail); - break; - case 'projects:branches:create': - import('./pages/projects/branches/new') - .then(callDefault) - .catch(fail); - break; - case 'projects:branches:index': - import('./pages/projects/branches/index') - .then(callDefault) - .catch(fail); - break; - case 'projects:issues:new': - import('./pages/projects/issues/new') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - case 'projects:issues:edit': - import('./pages/projects/issues/edit') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - case 'projects:merge_requests:creations:new': - import('./pages/projects/merge_requests/creations/new') - .then(callDefault) - .catch(fail); - case 'projects:merge_requests:creations:diffs': - import('./pages/projects/merge_requests/creations/diffs') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - case 'projects:merge_requests:edit': - import('./pages/projects/merge_requests/edit') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - case 'projects:tags:new': - import('./pages/projects/tags/new') - .then(callDefault) - .catch(fail); - break; - case 'projects:snippets:show': - import('./pages/projects/snippets/show') - .then(callDefault) - .catch(fail); - break; - case 'projects:snippets:new': - case 'projects:snippets:create': - import('./pages/projects/snippets/new') - .then(callDefault) - .catch(fail); - break; - case 'projects:snippets:edit': - case 'projects:snippets:update': - import('./pages/projects/snippets/edit') - .then(callDefault) - .catch(fail); - break; - case 'snippets:new': - import('./pages/snippets/new') - .then(callDefault) - .catch(fail); - break; - case 'snippets:edit': - import('./pages/snippets/edit') - .then(callDefault) - .catch(fail); - break; - case 'snippets:create': - import('./pages/snippets/new') - .then(callDefault) - .catch(fail); - break; - case 'snippets:update': - import('./pages/snippets/edit') - .then(callDefault) - .catch(fail); - break; - case 'projects:releases:edit': - import('./pages/projects/releases/edit') - .then(callDefault) - .catch(fail); - break; - case 'projects:merge_requests:show': - new Diff(); - new ZenMode(); +function initSearch() { + // Only when search form is present + if ($('.search').length) { + return new SearchAutocomplete(); + } +} - initIssuableSidebar(); - initNotes(); +function initFieldErrors() { + $('.gl-show-field-errors').each((i, form) => { + new GlFieldErrors(form); + }); +} - const mrShowNode = document.querySelector('.merge-request'); - window.mergeRequest = new MergeRequest({ - action: mrShowNode.dataset.mrAction, - }); - shortcut_handler = new ShortcutsIssuable(true); - break; - case 'dashboard:activity': - import('./pages/dashboard/activity') - .then(callDefault) - .catch(fail); - break; - case 'projects:commit:show': - import('./pages/projects/commit/show') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - case 'projects:commit:pipelines': - import('./pages/projects/commit/pipelines') - .then(callDefault) - .catch(fail); - break; - case 'projects:activity': - import('./pages/projects/activity') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - case 'projects:commits:show': - import('./pages/projects/commits/show') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - case 'projects:show': - shortcut_handler = true; - break; - case 'projects:edit': - import('./pages/projects/edit') - .then(callDefault) - .catch(fail); - break; - case 'projects:imports:show': - import('./pages/projects/imports/show') - .then(callDefault) - .catch(fail); - break; - case 'projects:pipelines:new': - case 'projects:pipelines:create': - import('./pages/projects/pipelines/new') - .then(callDefault) - .catch(fail); - break; - case 'projects:pipelines:builds': - case 'projects:pipelines:failures': - case 'projects:pipelines:show': - import('./pages/projects/pipelines/builds') - .then(callDefault) - .catch(fail); - break; - case 'groups:activity': - import('./pages/groups/activity') - .then(callDefault) - .catch(fail); - break; - case 'groups:show': - shortcut_handler = true; - break; - case 'groups:group_members:index': - import('./pages/groups/group_members/index') - .then(callDefault) - .catch(fail); - break; - case 'projects:project_members:index': - import('./pages/projects/project_members') - .then(callDefault) - .catch(fail); - break; - case 'groups:create': - case 'groups:new': - import('./pages/groups/new') - .then(callDefault) - .catch(fail); - break; - case 'groups:edit': - import('./pages/groups/edit') - .then(callDefault) - .catch(fail); - break; - case 'admin:groups:create': - case 'admin:groups:new': - import('./pages/admin/groups/new') - .then(callDefault) - .catch(fail); - break; - case 'admin:groups:edit': - import('./pages/admin/groups/edit') - .then(callDefault) - .catch(fail); - break; - case 'projects:tree:show': - import('./pages/projects/tree/show') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - case 'projects:find_file:show': - import('./pages/projects/find_file/show') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - case 'projects:blob:show': - import('./pages/projects/blob/show') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - case 'projects:blame:show': - import('./pages/projects/blame/show') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - case 'groups:labels:new': - import('./pages/groups/labels/new') - .then(callDefault) - .catch(fail); - break; - case 'groups:labels:edit': - import('./pages/groups/labels/edit') - .then(callDefault) - .catch(fail); - break; - case 'projects:labels:new': - import('./pages/projects/labels/new') - .then(callDefault) - .catch(fail); - break; - case 'projects:labels:edit': - import('./pages/projects/labels/edit') - .then(callDefault) - .catch(fail); - break; - case 'groups:labels:index': - import('./pages/groups/labels/index') - .then(callDefault) - .catch(fail); - break; - case 'projects:labels:index': - import('./pages/projects/labels/index') - .then(callDefault) - .catch(fail); - break; - case 'projects:network:show': - // Ensure we don't create a particular shortcut handler here. This is - // already created, where the network graph is created. - shortcut_handler = true; - break; - case 'projects:forks:new': - import('./pages/projects/forks/new') - .then(callDefault) - .catch(fail); - break; - case 'projects:artifacts:browse': - import('./pages/projects/artifacts/browse') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - case 'projects:artifacts:file': - import('./pages/projects/artifacts/file') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - case 'help:index': - import('./pages/help') - .then(callDefault) - .catch(fail); - break; - case 'search:show': - import('./pages/search/show') - .then(callDefault) - .catch(fail); - break; - case 'projects:settings:repository:show': - import('./pages/projects/settings/repository/show') - .then(callDefault) - .catch(fail); - break; - case 'projects:settings:ci_cd:show': - import('./pages/projects/settings/ci_cd/show') - .then(callDefault) - .catch(fail); - break; - case 'groups:settings:ci_cd:show': - import('./pages/groups/settings/ci_cd/show') - .then(callDefault) - .catch(fail); - break; - case 'ci:lints:create': - case 'ci:lints:show': - import('./pages/ci/lints') - .then(callDefault) - .catch(fail); - break; - case 'users:show': - import('./pages/users/show') - .then(callDefault) - .catch(fail); - break; - case 'admin:conversational_development_index:show': - import('./pages/admin/conversational_development_index/show') - .then(callDefault) - .catch(fail); - break; - case 'snippets:show': - import('./pages/snippets/show') - .then(callDefault) - .catch(fail); - break; - case 'import:fogbugz:new_user_map': - import('./pages/import/fogbugz/new_user_map') - .then(callDefault) - .catch(fail); - break; - case 'profiles:personal_access_tokens:index': - import('./pages/profiles/personal_access_tokens') - .then(callDefault) - .catch(fail); - break; - case 'admin:impersonation_tokens:index': - import('./pages/admin/impersonation_tokens') - .then(callDefault) - .catch(fail); - break; - case 'projects:clusters:show': - case 'projects:clusters:update': - case 'projects:clusters:destroy': - import('./pages/projects/clusters/show') - .then(callDefault) - .catch(fail); - break; - case 'projects:clusters:index': - import('./pages/projects/clusters/index') - .then(callDefault) - .catch(fail); - break; - case 'dashboard:groups:index': - import('./pages/dashboard/groups/index') - .then(callDefault) - .catch(fail); - break; - } - switch (path[0]) { - case 'sessions': - import('./pages/sessions') - .then(callDefault) - .catch(fail); - break; - case 'omniauth_callbacks': - import('./pages/omniauth_callbacks') - .then(callDefault) - .catch(fail); - break; - case 'admin': - import('./pages/admin') - .then(callDefault) - .catch(fail); - switch (path[1]) { - case 'broadcast_messages': - import('./pages/admin/broadcast_messages') - .then(callDefault) - .catch(fail); - break; - case 'cohorts': - import('./pages/admin/cohorts') - .then(callDefault) - .catch(fail); - break; - case 'groups': - switch (path[2]) { - case 'show': - import('./pages/admin/groups/show') - .then(callDefault) - .catch(fail); - break; - } - break; - case 'projects': - import('./pages/admin/projects') - .then(callDefault) - .catch(fail); - break; - case 'labels': - switch (path[2]) { - case 'new': - import('./pages/admin/labels/new') - .then(callDefault) - .catch(fail); - break; - case 'edit': - import('./pages/admin/labels/edit') - .then(callDefault) - .catch(fail); - break; - } - case 'abuse_reports': - import('./pages/admin/abuse_reports') - .then(callDefault) - .catch(fail); - break; - } - break; - case 'profiles': - import('./pages/profiles/index') - .then(callDefault) - .catch(fail); - break; - case 'projects': - import('./pages/projects') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - switch (path[1]) { - case 'compare': - import('./pages/projects/compare') - .then(callDefault) - .catch(fail); - break; - case 'create': - case 'new': - import('./pages/projects/new') - .then(callDefault) - .catch(fail); - break; - case 'wikis': - import('./pages/projects/wikis') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - } - break; - } - // If we haven't installed a custom shortcut handler, install the default one - if (!shortcut_handler) { - new Shortcuts(); - } +function initPageShortcuts(page) { + const pagesWithCustomShortcuts = [ + 'projects:activity', + 'projects:artifacts:browse', + 'projects:artifacts:file', + 'projects:blame:show', + 'projects:blob:show', + 'projects:commit:show', + 'projects:commits:show', + 'projects:find_file:show', + 'projects:issues:edit', + 'projects:issues:index', + 'projects:issues:new', + 'projects:issues:show', + 'projects:merge_requests:creations:diffs', + 'projects:merge_requests:creations:new', + 'projects:merge_requests:edit', + 'projects:merge_requests:index', + 'projects:merge_requests:show', + 'projects:network:show', + 'projects:show', + 'projects:tree:show', + 'groups:show', + ]; - if (document.querySelector('#peek')) { - import('./performance_bar') - .then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap - .catch(fail); - } - }; + if (pagesWithCustomShortcuts.indexOf(page) === -1) { + new Shortcuts(); + } +} - Dispatcher.prototype.initSearch = function() { - // Only when search form is present - if ($('.search').length) { - return new SearchAutocomplete(); - } - }; +function initGFMInput() { + $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { + const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); + const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete); + gfm.setup($(el), { + emojis: true, + members: enableGFM, + issues: enableGFM, + milestones: enableGFM, + mergeRequests: enableGFM, + labels: enableGFM, + }); + }); +} - Dispatcher.prototype.initFieldErrors = function() { - $('.gl-show-field-errors').each((i, form) => { - new GlFieldErrors(form); - }); - }; +function initPerformanceBar() { + if (document.querySelector('#peek')) { + import('./performance_bar') + .then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap + .catch(() => Flash('Error loading performance bar module')); + } +} - return Dispatcher; - })(); -})(); +export default () => { + initSearch(); + initFieldErrors(); -export default function initDispatcher() { - return new Dispatcher(); -} + const page = $('body').attr('data-page'); + if (page) { + initPageShortcuts(page); + initGFMInput(); + initPerformanceBar(); + } +}; diff --git a/app/assets/javascripts/docs/docs_bundle.js b/app/assets/javascripts/docs/docs_bundle.js index a32bd6d0fc7..897439f56b0 100644 --- a/app/assets/javascripts/docs/docs_bundle.js +++ b/app/assets/javascripts/docs/docs_bundle.js @@ -4,10 +4,7 @@ function addMousetrapClick(el, key) { el.addEventListener('click', () => Mousetrap.trigger(key)); } -function domContentLoaded() { +export default () => { addMousetrapClick(document.querySelector('.js-trigger-shortcut'), '?'); addMousetrapClick(document.querySelector('.js-trigger-search-bar'), 's'); -} - -document.addEventListener('DOMContentLoaded', domContentLoaded); - +}; diff --git a/app/assets/javascripts/droplab/constants.js b/app/assets/javascripts/droplab/constants.js index 673e9bb4c0f..868d47e91b3 100644 --- a/app/assets/javascripts/droplab/constants.js +++ b/app/assets/javascripts/droplab/constants.js @@ -3,7 +3,6 @@ const DATA_DROPDOWN = 'data-dropdown'; const SELECTED_CLASS = 'droplab-item-selected'; const ACTIVE_CLASS = 'droplab-item-active'; const IGNORE_CLASS = 'droplab-item-ignore'; -const IGNORE_HIDING_CLASS = 'droplab-item-ignore-hiding'; // Matches `{{anything}}` and `{{ everything }}`. const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g; @@ -14,5 +13,4 @@ export { ACTIVE_CLASS, TEMPLATE_REGEX, IGNORE_CLASS, - IGNORE_HIDING_CLASS, }; diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js index 5eb0a339a1c..3cc316c3f3e 100644 --- a/app/assets/javascripts/droplab/drop_down.js +++ b/app/assets/javascripts/droplab/drop_down.js @@ -1,13 +1,14 @@ import utils from './utils'; -import { SELECTED_CLASS, IGNORE_CLASS, IGNORE_HIDING_CLASS } from './constants'; +import { SELECTED_CLASS, IGNORE_CLASS } from './constants'; class DropDown { - constructor(list, config = {}) { + constructor(list, config = { }) { this.currentIndex = 0; this.hidden = true; this.list = typeof list === 'string' ? document.querySelector(list) : list; this.items = []; this.eventWrapper = {}; + this.hideOnClick = config.hideOnClick !== false; if (config.addActiveClassToDropdownButton) { this.dropdownToggle = this.list.parentNode.querySelector('.js-dropdown-toggle'); @@ -37,15 +38,17 @@ class DropDown { clickEvent(e) { if (e.target.tagName === 'UL') return; - if (e.target.classList.contains(IGNORE_CLASS)) return; + if (e.target.closest(`.${IGNORE_CLASS}`)) return; - const selected = utils.closest(e.target, 'LI'); + const selected = e.target.closest('li'); if (!selected) return; this.addSelectedClass(selected); e.preventDefault(); - if (!e.target.classList.contains(IGNORE_HIDING_CLASS)) this.hide(); + if (this.hideOnClick) { + this.hide(); + } const listEvent = new CustomEvent('click.dl', { detail: { diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index bd4c58b7cb1..417258e0092 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -17,9 +17,9 @@ class DueDateSelect { this.$value = $block.find('.value'); this.$valueContent = $block.find('.value-content'); this.$sidebarValue = $('.js-due-date-sidebar-value', $block); - this.fieldName = $dropdown.data('field-name'); - this.abilityName = $dropdown.data('ability-name'); - this.issueUpdateURL = $dropdown.data('issue-update'); + this.fieldName = $dropdown.data('fieldName'); + this.abilityName = $dropdown.data('abilityName'); + this.issueUpdateURL = $dropdown.data('issueUpdate'); this.rawSelectedDate = null; this.displayedDate = null; diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index a9d554e549e..79326ca3487 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -1,8 +1,9 @@ <script> import Timeago from 'timeago.js'; import _ from 'underscore'; - import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; - import { humanize } from '../../lib/utils/text_utility'; + import tooltip from '~/vue_shared/directives/tooltip'; + import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; + import { humanize } from '~/lib/utils/text_utility'; import ActionsComponent from './environment_actions.vue'; import ExternalUrlComponent from './environment_external_url.vue'; import StopComponent from './environment_stop.vue'; @@ -21,14 +22,18 @@ export default { components: { - userAvatarLink, - 'commit-component': CommitComponent, - 'actions-component': ActionsComponent, - 'external-url-component': ExternalUrlComponent, - 'stop-component': StopComponent, - 'rollback-component': RollbackComponent, - 'terminal-button-component': TerminalButtonComponent, - 'monitoring-button-component': MonitoringButtonComponent, + UserAvatarLink, + CommitComponent, + ActionsComponent, + ExternalUrlComponent, + StopComponent, + RollbackComponent, + TerminalButtonComponent, + MonitoringButtonComponent, + }, + + directives: { + tooltip, }, props: { @@ -443,7 +448,11 @@ v-if="!model.isFolder" class="environment-name flex-truncate-parent table-mobile-content" :href="environmentPath"> - <span class="flex-truncate-child">{{ model.name }}</span> + <span + class="flex-truncate-child" + v-tooltip + :title="model.name" + >{{ model.name }}</span> </a> <span v-else diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index b4eca47957e..22863e926d4 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -2,8 +2,8 @@ /** * Render environments table. */ +import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import environmentItem from './environment_item.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { components: { diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index 5d2d14c7682..de0fbdb2e91 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -5,7 +5,7 @@ import Translate from '../../vue_shared/translate'; Vue.use(Translate); -document.addEventListener('DOMContentLoaded', () => new Vue({ +export default () => new Vue({ el: '#environments-folder-list-view', components: { environmentsFolderApp, @@ -32,4 +32,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ }, }); }, -})); +}); diff --git a/app/assets/javascripts/environments/environments_bundle.js b/app/assets/javascripts/environments/index.js index 2e0a4001b7c..afc4aba6554 100644 --- a/app/assets/javascripts/environments/environments_bundle.js +++ b/app/assets/javascripts/environments/index.js @@ -5,7 +5,7 @@ import Translate from '../vue_shared/translate'; Vue.use(Translate); -document.addEventListener('DOMContentLoaded', () => new Vue({ +export default () => new Vue({ el: '#environments-list-view', components: { environmentsComponent, @@ -36,4 +36,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ }, }); }, -})); +}); diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 90020344748..6a4874e1ab8 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -25,7 +25,7 @@ export default { if (!this.userCanCreateNote) { // data-can-create-note is an empty string when true, otherwise undefined - this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === ''; + this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('canCreateNote') === ''; } this.isParallelView = Cookies.get('diff_view') === 'parallel'; diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js deleted file mode 100644 index c51d4b056af..00000000000 --- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js +++ /dev/null @@ -1,101 +0,0 @@ -import eventHub from '../event_hub'; - -export default { - name: 'RecentSearchesDropdownContent', - - props: { - items: { - type: Array, - required: true, - }, - isLocalStorageAvailable: { - type: Boolean, - required: false, - default: true, - }, - allowedKeys: { - type: Array, - required: true, - }, - }, - - computed: { - processedItems() { - return this.items.map((item) => { - const { tokens, searchToken } - = gl.FilteredSearchTokenizer.processTokens(item, this.allowedKeys); - - const resultantTokens = tokens.map(token => ({ - prefix: `${token.key}:`, - suffix: `${token.symbol}${token.value}`, - })); - - return { - text: item, - tokens: resultantTokens, - searchToken, - }; - }); - }, - hasItems() { - return this.items.length > 0; - }, - }, - - methods: { - onItemActivated(text) { - eventHub.$emit('recentSearchesItemSelected', text); - }, - onRequestClearRecentSearches(e) { - // Stop the dropdown from closing - e.stopPropagation(); - - eventHub.$emit('requestClearRecentSearches'); - }, - }, - - template: ` - <div> - <div - v-if="!isLocalStorageAvailable" - class="dropdown-info-note"> - This feature requires local storage to be enabled - </div> - <ul v-else-if="hasItems"> - <li - v-for="(item, index) in processedItems" - :key="index"> - <button - type="button" - class="filtered-search-history-dropdown-item" - @click="onItemActivated(item.text)"> - <span> - <span - v-for="(token, tokenIndex) in item.tokens" - class="filtered-search-history-dropdown-token"> - <span class="name">{{ token.prefix }}</span><span class="value">{{ token.suffix }}</span> - </span> - </span> - <span class="filtered-search-history-dropdown-search-token"> - {{ item.searchToken }} - </span> - </button> - </li> - <li class="divider"></li> - <li> - <button - type="button" - class="filtered-search-history-clear-button" - @click="onRequestClearRecentSearches($event)"> - Clear recent searches - </button> - </li> - </ul> - <div - v-else - class="dropdown-info-note"> - You don't have any recent searches - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue new file mode 100644 index 00000000000..26618af9515 --- /dev/null +++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue @@ -0,0 +1,104 @@ +<script> +import eventHub from '../event_hub'; +import FilteredSearchTokenizer from '../filtered_search_tokenizer'; + +export default { + name: 'RecentSearchesDropdownContent', + props: { + items: { + type: Array, + required: true, + }, + isLocalStorageAvailable: { + type: Boolean, + required: false, + default: true, + }, + allowedKeys: { + type: Array, + required: true, + }, + }, + computed: { + processedItems() { + return this.items.map((item) => { + const { tokens, searchToken } + = FilteredSearchTokenizer.processTokens(item, this.allowedKeys); + + const resultantTokens = tokens.map(token => ({ + prefix: `${token.key}:`, + suffix: `${token.symbol}${token.value}`, + })); + + return { + text: item, + tokens: resultantTokens, + searchToken, + }; + }); + }, + hasItems() { + return this.items.length > 0; + }, + }, + methods: { + onItemActivated(text) { + eventHub.$emit('recentSearchesItemSelected', text); + }, + onRequestClearRecentSearches(e) { + // Stop the dropdown from closing + e.stopPropagation(); + + eventHub.$emit('requestClearRecentSearches'); + }, + }, +}; +</script> +<template> + <div> + <div + v-if="!isLocalStorageAvailable" + class="dropdown-info-note"> + This feature requires local storage to be enabled + </div> + <ul v-else-if="hasItems"> + <li + v-for="(item, index) in processedItems" + :key="`processed-items-${index}`" + > + <button + type="button" + class="filtered-search-history-dropdown-item" + @click="onItemActivated(item.text)"> + <span> + <span + class="filtered-search-history-dropdown-token" + v-for="(token, index) in item.tokens" + :key="`dropdown-token-${index}`" + > + <span class="name">{{ token.prefix }}</span> + <span class="value">{{ token.suffix }}</span> + </span> + </span> + <span class="filtered-search-history-dropdown-search-token"> + {{ item.searchToken }} + </span> + </button> + </li> + <li class="divider"></li> + <li> + <button + type="button" + class="filtered-search-history-clear-button" + @click="onRequestClearRecentSearches($event)"> + Clear recent searches + </button> + </li> + </ul> + <div + v-else + class="dropdown-info-note"> + You don't have any recent searches + </div> + </div> +</template> diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js index a6cc079d720..5ddd0e5e690 100644 --- a/app/assets/javascripts/filtered_search/dropdown_emoji.js +++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js @@ -1,9 +1,10 @@ import Flash from '../flash'; import Ajax from '../droplab/plugins/ajax'; import Filter from '../droplab/plugins/filter'; -import './filtered_search_dropdown'; +import FilteredSearchDropdown from './filtered_search_dropdown'; +import DropdownUtils from './dropdown_utils'; -class DropdownEmoji extends gl.FilteredSearchDropdown { +export default class DropdownEmoji extends FilteredSearchDropdown { constructor(options = {}) { super(options); this.config = { @@ -49,7 +50,7 @@ class DropdownEmoji extends gl.FilteredSearchDropdown { itemClicked(e) { super.itemClicked(e, (selected) => { const name = selected.querySelector('.js-data-value').innerText.trim(); - return gl.DropdownUtils.getEscapedText(name); + return DropdownUtils.getEscapedText(name); }); } @@ -76,6 +77,3 @@ class DropdownEmoji extends gl.FilteredSearchDropdown { .addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init(); } } - -window.gl = window.gl || {}; -gl.DropdownEmoji = DropdownEmoji; diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 23040cd9eb8..184b34b7b5e 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -1,14 +1,17 @@ import Filter from '~/droplab/plugins/filter'; -import './filtered_search_dropdown'; +import FilteredSearchDropdown from './filtered_search_dropdown'; +import DropdownUtils from './dropdown_utils'; +import FilteredSearchDropdownManager from './filtered_search_dropdown_manager'; +import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; -class DropdownHint extends gl.FilteredSearchDropdown { +export default class DropdownHint extends FilteredSearchDropdown { constructor(options = {}) { const { input, tokenKeys } = options; super(options); this.config = { Filter: { template: 'hint', - filterFunction: gl.DropdownUtils.filterHint.bind(null, { + filterFunction: DropdownUtils.filterHint.bind(null, { input, allowedKeys: tokenKeys.getKeys(), }), @@ -45,10 +48,10 @@ class DropdownHint extends gl.FilteredSearchDropdown { }); if (searchTerms.length > 0) { - gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' ')); + FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' ')); } - gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container); + FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container); } this.dismissDropdown(); this.dispatchInputEvent(); @@ -73,6 +76,3 @@ class DropdownHint extends gl.FilteredSearchDropdown { this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init(); } } - -window.gl = window.gl || {}; -gl.DropdownHint = DropdownHint; diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js index 788fb1dc614..2ffda7e2037 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js @@ -1,9 +1,10 @@ import Flash from '../flash'; import Ajax from '../droplab/plugins/ajax'; import Filter from '../droplab/plugins/filter'; -import './filtered_search_dropdown'; +import FilteredSearchDropdown from './filtered_search_dropdown'; +import DropdownUtils from './dropdown_utils'; -class DropdownNonUser extends gl.FilteredSearchDropdown { +export default class DropdownNonUser extends FilteredSearchDropdown { constructor(options = {}) { const { input, endpoint, symbol, preprocessing } = options; super(options); @@ -21,7 +22,7 @@ class DropdownNonUser extends gl.FilteredSearchDropdown { }, }, Filter: { - filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input), + filterFunction: DropdownUtils.filterWithSymbol.bind(null, this.symbol, input), template: 'title', }, }; @@ -30,7 +31,7 @@ class DropdownNonUser extends gl.FilteredSearchDropdown { itemClicked(e) { super.itemClicked(e, (selected) => { const title = selected.querySelector('.js-data-value').innerText.trim(); - return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`; + return `${this.symbol}${DropdownUtils.getEscapedText(title)}`; }); } @@ -45,6 +46,3 @@ class DropdownNonUser extends gl.FilteredSearchDropdown { .addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init(); } } - -window.gl = window.gl || {}; -gl.DropdownNonUser = DropdownNonUser; diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index a9e2b65def0..d36f38a70b5 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -1,9 +1,11 @@ import Flash from '../flash'; import AjaxFilter from '../droplab/plugins/ajax_filter'; -import './filtered_search_dropdown'; +import FilteredSearchDropdown from './filtered_search_dropdown'; import { addClassIfElementExists } from '../lib/utils/dom_utils'; +import DropdownUtils from './dropdown_utils'; +import FilteredSearchTokenizer from './filtered_search_tokenizer'; -class DropdownUser extends gl.FilteredSearchDropdown { +export default class DropdownUser extends FilteredSearchDropdown { constructor(options = {}) { const { tokenKeys } = options; super(options); @@ -12,7 +14,6 @@ class DropdownUser extends gl.FilteredSearchDropdown { endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, searchKey: 'search', params: { - per_page: 20, active: true, group_id: this.getGroupId(), project_id: this.getProjectId(), @@ -56,8 +57,8 @@ class DropdownUser extends gl.FilteredSearchDropdown { } getSearchInput() { - const query = gl.DropdownUtils.getSearchInput(this.input); - const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query, this.tokenKeys.get()); + const query = DropdownUtils.getSearchInput(this.input); + const { lastToken } = FilteredSearchTokenizer.processTokens(query, this.tokenKeys.get()); let value = lastToken || ''; @@ -78,6 +79,3 @@ class DropdownUser extends gl.FilteredSearchDropdown { this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init(); } } - -window.gl = window.gl || {}; -gl.DropdownUser = DropdownUser; diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index cf8a9b0402b..9bc36c1f9b6 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -1,7 +1,10 @@ import _ from 'underscore'; import FilteredSearchContainer from './container'; +import FilteredSearchTokenizer from './filtered_search_tokenizer'; +import FilteredSearchDropdownManager from './filtered_search_dropdown_manager'; +import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; -class DropdownUtils { +export default class DropdownUtils { static getEscapedText(text) { let escapedText = text; const hasSpace = text.indexOf(' ') !== -1; @@ -24,7 +27,7 @@ class DropdownUtils { static filterWithSymbol(filterSymbol, input, item) { const updatedItem = item; - const searchInput = gl.DropdownUtils.getSearchInput(input); + const searchInput = DropdownUtils.getSearchInput(input); const title = updatedItem.title.toLowerCase(); let value = searchInput.toLowerCase(); @@ -114,9 +117,9 @@ class DropdownUtils { static filterHint(config, item) { const { input, allowedKeys } = config; const updatedItem = item; - const searchInput = gl.DropdownUtils.getSearchQuery(input); + const searchInput = DropdownUtils.getSearchQuery(input); const { lastToken, tokens } = - gl.FilteredSearchTokenizer.processTokens(searchInput, allowedKeys); + FilteredSearchTokenizer.processTokens(searchInput, allowedKeys); const lastKey = lastToken.key || lastToken || ''; const allowMultiple = item.type === 'array'; const itemInExistingTokens = tokens.some(t => t.key === item.hint); @@ -140,7 +143,7 @@ class DropdownUtils { const dataValue = selected.getAttribute('data-value'); if (dataValue) { - gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true); + FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true); } // Return boolean based on whether it was set @@ -190,7 +193,7 @@ class DropdownUtils { } } else if (token.classList.contains('input-token')) { const { isLastVisualTokenValid } = - gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); const input = FilteredSearchContainer.container.querySelector('.filtered-search'); const inputValue = input && input.value; @@ -211,7 +214,7 @@ class DropdownUtils { static getSearchInput(filteredSearchInput) { const inputValue = filteredSearchInput.value; - const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput); + const { right } = DropdownUtils.getInputSelectionPosition(filteredSearchInput); return inputValue.slice(0, right); } @@ -252,6 +255,3 @@ class DropdownUtils { }; } } - -window.gl = window.gl || {}; -gl.DropdownUtils = DropdownUtils; diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js deleted file mode 100644 index 6d5dd747224..00000000000 --- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js +++ /dev/null @@ -1,11 +0,0 @@ -import './dropdown_emoji'; -import './dropdown_hint'; -import './dropdown_non_user'; -import './dropdown_user'; -import './dropdown_utils'; -import './filtered_search_token_keys'; -import './filtered_search_dropdown_manager'; -import './filtered_search_dropdown'; -import './filtered_search_manager'; -import './filtered_search_tokenizer'; -import './filtered_search_visual_tokens'; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js index 9e9a9ef74be..fb4ae1d17dd 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js @@ -1,6 +1,9 @@ +import DropdownUtils from './dropdown_utils'; +import FilteredSearchDropdownManager from './filtered_search_dropdown_manager'; + const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; -class FilteredSearchDropdown { +export default class FilteredSearchDropdown { constructor({ droplab, dropdown, input, filter }) { this.droplab = droplab; this.hookId = input && input.id; @@ -30,11 +33,11 @@ class FilteredSearchDropdown { const { selected } = e.detail; if (selected.tagName === 'LI' && selected.innerHTML) { - const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected); + const dataValueSet = DropdownUtils.setDataValueIfSelected(this.filter, selected); if (!dataValueSet) { const value = getValueFunction(selected); - gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true); + FilteredSearchDropdownManager.addWordToInput(this.filter, value, true); } this.resetFilters(); @@ -108,6 +111,9 @@ class FilteredSearchDropdown { if (hook) { const data = hook.list.data || []; + + if (!data) return; + const results = data.map((o) => { const updated = o; updated.droplab_hidden = false; @@ -117,6 +123,3 @@ class FilteredSearchDropdown { } } } - -window.gl = window.gl || {}; -gl.FilteredSearchDropdown = FilteredSearchDropdown; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index ff046aa286a..ee49a7be0b2 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -1,15 +1,31 @@ import _ from 'underscore'; import DropLab from '~/droplab/drop_lab'; import FilteredSearchContainer from './container'; - -class FilteredSearchDropdownManager { - constructor(baseEndpoint = '', tokenizer, page) { +import FilteredSearchTokenKeys from './filtered_search_token_keys'; +import DropdownUtils from './dropdown_utils'; +import DropdownHint from './dropdown_hint'; +import DropdownEmoji from './dropdown_emoji'; +import DropdownNonUser from './dropdown_non_user'; +import DropdownUser from './dropdown_user'; +import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; + +export default class FilteredSearchDropdownManager { + constructor({ + baseEndpoint = '', + tokenizer, + page, + isGroup, + isGroupAncestor, + filteredSearchTokenKeys, + }) { this.container = FilteredSearchContainer.container; this.baseEndpoint = baseEndpoint.replace(/\/$/, ''); this.tokenizer = tokenizer; - this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; + this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys; this.filteredSearchInput = this.container.querySelector('.filtered-search'); this.page = page; + this.groupsOnly = isGroup; + this.groupAncestor = isGroupAncestor; this.setupMapping(); @@ -29,57 +45,80 @@ class FilteredSearchDropdownManager { } setupMapping() { - this.mapping = { + const supportedTokens = this.filteredSearchTokenKeys.getKeys(); + const allowedMappings = { + hint: { + reference: null, + gl: DropdownHint, + element: this.container.querySelector('#js-dropdown-hint'), + }, + }; + const availableMappings = { author: { reference: null, - gl: 'DropdownUser', + gl: DropdownUser, element: this.container.querySelector('#js-dropdown-author'), }, assignee: { reference: null, - gl: 'DropdownUser', + gl: DropdownUser, element: this.container.querySelector('#js-dropdown-assignee'), }, milestone: { reference: null, - gl: 'DropdownNonUser', + gl: DropdownNonUser, extraArguments: { - endpoint: `${this.baseEndpoint}/milestones.json`, + endpoint: this.getMilestoneEndpoint(), symbol: '%', }, element: this.container.querySelector('#js-dropdown-milestone'), }, label: { reference: null, - gl: 'DropdownNonUser', + gl: DropdownNonUser, extraArguments: { - endpoint: `${this.baseEndpoint}/labels.json`, + endpoint: this.getLabelsEndpoint(), symbol: '~', - preprocessing: gl.DropdownUtils.duplicateLabelPreprocessing, + preprocessing: DropdownUtils.duplicateLabelPreprocessing, }, element: this.container.querySelector('#js-dropdown-label'), }, 'my-reaction': { reference: null, - gl: 'DropdownEmoji', + gl: DropdownEmoji, element: this.container.querySelector('#js-dropdown-my-reaction'), }, - hint: { - reference: null, - gl: 'DropdownHint', - element: this.container.querySelector('#js-dropdown-hint'), - }, }; + + supportedTokens.forEach((type) => { + if (availableMappings[type]) { + allowedMappings[type] = availableMappings[type]; + } + }); + + this.mapping = allowedMappings; + } + + getMilestoneEndpoint() { + const endpoint = `${this.baseEndpoint}/milestones.json`; + + return endpoint; + } + + getLabelsEndpoint() { + const endpoint = `${this.baseEndpoint}/labels.json`; + + return endpoint; } static addWordToInput(tokenName, tokenValue = '', clicked = false) { const input = FilteredSearchContainer.container.querySelector('.filtered-search'); - gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue); + FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue); input.value = ''; if (clicked) { - gl.FilteredSearchVisualTokens.moveInputToTheRight(); + FilteredSearchVisualTokens.moveInputToTheRight(); } } @@ -120,9 +159,9 @@ class FilteredSearchDropdownManager { const extraArguments = mappingKey.extraArguments || {}; const glArguments = Object.assign({}, defaultArguments, extraArguments); - // Passing glArguments to `new gl[glClass](<arguments>)` + // Passing glArguments to `new glClass(<arguments>)` mappingKey.reference = - new (Function.prototype.bind.apply(gl[glClass], [null, glArguments]))(); + new (Function.prototype.bind.apply(glClass, [null, glArguments]))(); } if (firstLoad) { @@ -160,7 +199,7 @@ class FilteredSearchDropdownManager { } setDropdown() { - const query = gl.DropdownUtils.getSearchQuery(true); + const query = DropdownUtils.getSearchQuery(true); const { lastToken, searchToken } = this.tokenizer.processTokens(query, this.filteredSearchTokenKeys.getKeys()); @@ -205,6 +244,3 @@ class FilteredSearchDropdownManager { this.droplab.destroy(); } } - -window.gl = window.gl || {}; -gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 58ed0012f01..c6970d7837f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -1,22 +1,46 @@ import _ from 'underscore'; +import { + getParameterByName, + getUrlParamsArray, +} from '~/lib/utils/common_utils'; import { visitUrl } from '../lib/utils/url_utility'; import Flash from '../flash'; import FilteredSearchContainer from './container'; +import FilteredSearchTokenKeys from './filtered_search_token_keys'; import RecentSearchesRoot from './recent_searches_root'; import RecentSearchesStore from './stores/recent_searches_store'; import RecentSearchesService from './services/recent_searches_service'; import eventHub from './event_hub'; import { addClassIfElementExists } from '../lib/utils/dom_utils'; +import FilteredSearchTokenizer from './filtered_search_tokenizer'; +import FilteredSearchDropdownManager from './filtered_search_dropdown_manager'; +import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; +import DropdownUtils from './dropdown_utils'; + +export default class FilteredSearchManager { + constructor({ + page, + isGroup = false, + isGroupAncestor = false, + filteredSearchTokenKeys = FilteredSearchTokenKeys, + stateFiltersSelector = '.issues-state-filters', + }) { + this.isGroup = isGroup; + this.isGroupAncestor = isGroupAncestor; + this.states = ['opened', 'closed', 'merged', 'all']; -class FilteredSearchManager { - constructor(page) { this.page = page; this.container = FilteredSearchContainer.container; this.filteredSearchInput = this.container.querySelector('.filtered-search'); this.filteredSearchInputForm = this.filteredSearchInput.form; this.clearSearchButton = this.container.querySelector('.clear-search'); this.tokensContainer = this.container.querySelector('.tokens-container'); - this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; + this.filteredSearchTokenKeys = filteredSearchTokenKeys; + this.stateFiltersSelector = stateFiltersSelector; + this.recentsStorageKeyNames = { + issues: 'issue-recent-searches', + merge_requests: 'merge-request-recent-searches', + }; this.recentSearchesStore = new RecentSearchesStore({ isLocalStorageAvailable: RecentSearchesService.isAvailable(), @@ -25,11 +49,7 @@ class FilteredSearchManager { this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown'); const fullPath = this.searchHistoryDropdownElement ? this.searchHistoryDropdownElement.dataset.fullPath : 'project'; - let recentSearchesPagePrefix = 'issue-recent-searches'; - if (this.page === 'merge_requests') { - recentSearchesPagePrefix = 'merge-request-recent-searches'; - } - const recentSearchesKey = `${fullPath}-${recentSearchesPagePrefix}`; + const recentSearchesKey = `${fullPath}-${this.recentsStorageKeyNames[this.page]}`; this.recentSearchesService = new RecentSearchesService(recentSearchesKey); } @@ -57,8 +77,15 @@ class FilteredSearchManager { }); if (this.filteredSearchInput) { - this.tokenizer = gl.FilteredSearchTokenizer; - this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', this.tokenizer, this.page); + this.tokenizer = FilteredSearchTokenizer; + this.dropdownManager = new FilteredSearchDropdownManager({ + baseEndpoint: this.filteredSearchInput.getAttribute('data-base-endpoint') || '', + tokenizer: this.tokenizer, + page: this.page, + isGroup: this.isGroup, + isGroupAncestor: this.isGroupAncestor, + filteredSearchTokenKeys: this.filteredSearchTokenKeys, + }); this.recentSearchesRoot = new RecentSearchesRoot( this.recentSearchesStore, @@ -70,7 +97,6 @@ class FilteredSearchManager { this.bindEvents(); this.loadSearchParamsFromURL(); this.dropdownManager.setDropdown(); - this.cleanupWrapper = this.cleanup.bind(this); document.addEventListener('beforeunload', this.cleanupWrapper); } @@ -86,40 +112,33 @@ class FilteredSearchManager { } bindStateEvents() { - this.stateFilters = document.querySelector('.container-fluid .issues-state-filters'); + this.stateFilters = document.querySelector(`.container-fluid ${this.stateFiltersSelector}`); if (this.stateFilters) { this.searchStateWrapper = this.searchState.bind(this); - this.stateFilters.querySelector('[data-state="opened"]') - .addEventListener('click', this.searchStateWrapper); - this.stateFilters.querySelector('[data-state="closed"]') - .addEventListener('click', this.searchStateWrapper); - this.stateFilters.querySelector('[data-state="all"]') - .addEventListener('click', this.searchStateWrapper); - - this.mergedState = this.stateFilters.querySelector('[data-state="merged"]'); - if (this.mergedState) { - this.mergedState.addEventListener('click', this.searchStateWrapper); - } + this.applyToStateFilters((filterEl) => { + filterEl.addEventListener('click', this.searchStateWrapper); + }); } } unbindStateEvents() { if (this.stateFilters) { - this.stateFilters.querySelector('[data-state="opened"]') - .removeEventListener('click', this.searchStateWrapper); - this.stateFilters.querySelector('[data-state="closed"]') - .removeEventListener('click', this.searchStateWrapper); - this.stateFilters.querySelector('[data-state="all"]') - .removeEventListener('click', this.searchStateWrapper); - - if (this.mergedState) { - this.mergedState.removeEventListener('click', this.searchStateWrapper); - } + this.applyToStateFilters((filterEl) => { + filterEl.removeEventListener('click', this.searchStateWrapper); + }); } } + applyToStateFilters(callback) { + this.stateFilters.querySelectorAll('a[data-state]').forEach((filterEl) => { + if (this.states.indexOf(filterEl.dataset.state) > -1) { + callback(filterEl); + } + }); + } + bindEvents() { this.handleFormSubmit = this.handleFormSubmit.bind(this); this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); @@ -189,8 +208,8 @@ class FilteredSearchManager { // 8 = Backspace Key // 46 = Delete Key if (e.keyCode === 8 || e.keyCode === 46) { - const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); - const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken); + const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + const { tokenName, tokenValue } = DropdownUtils.getVisualTokenValues(lastVisualToken); const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue); if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) { @@ -198,8 +217,8 @@ class FilteredSearchManager { if (backspaceCount === 2) { backspaceCount = 0; - this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); - gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + this.filteredSearchInput.value = FilteredSearchVisualTokens.getLastTokenPartial(); + FilteredSearchVisualTokens.removeLastTokenPartial(); } } @@ -267,7 +286,7 @@ class FilteredSearchManager { e.stopImmediatePropagation(); const button = e.target.closest('.selectable'); - gl.FilteredSearchVisualTokens.selectToken(button, true); + FilteredSearchVisualTokens.selectToken(button, true); this.removeSelectedToken(); } } @@ -279,7 +298,7 @@ class FilteredSearchManager { const isElementTokensContainer = e.target.classList.contains('tokens-container'); if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) { - gl.FilteredSearchVisualTokens.moveInputToTheRight(); + FilteredSearchVisualTokens.moveInputToTheRight(); this.dropdownManager.resetDropdowns(); } } @@ -292,13 +311,13 @@ class FilteredSearchManager { if (token && canEdit) { e.preventDefault(); e.stopPropagation(); - gl.FilteredSearchVisualTokens.editToken(token); + FilteredSearchVisualTokens.editToken(token); this.tokenChange(); } } toggleClearSearchButton() { - const query = gl.DropdownUtils.getSearchQuery(); + const query = DropdownUtils.getSearchQuery(); const hidden = 'hidden'; const hasHidden = this.clearSearchButton.classList.contains(hidden); @@ -310,7 +329,7 @@ class FilteredSearchManager { } handleInputPlaceholder() { - const query = gl.DropdownUtils.getSearchQuery(); + const query = DropdownUtils.getSearchQuery(); const placeholder = 'Search or filter results...'; const currentPlaceholder = this.filteredSearchInput.placeholder; @@ -330,7 +349,7 @@ class FilteredSearchManager { } removeSelectedToken() { - gl.FilteredSearchVisualTokens.removeSelectedToken(); + FilteredSearchVisualTokens.removeSelectedToken(); this.handleInputPlaceholder(); this.toggleClearSearchButton(); this.dropdownManager.updateCurrentDropdownOffset(); @@ -350,7 +369,7 @@ class FilteredSearchManager { let canClearToken = t.classList.contains('js-visual-token'); if (canClearToken) { - const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(t); + const { tokenName, tokenValue } = DropdownUtils.getVisualTokenValues(t); canClearToken = this.canEdit && this.canEdit(tokenName, tokenValue); } @@ -378,12 +397,12 @@ class FilteredSearchManager { const { tokens, searchToken } = this.tokenizer.processTokens(input.value, this.filteredSearchTokenKeys.getKeys()); const { isLastVisualTokenValid } - = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); if (isLastVisualTokenValid) { tokens.forEach((t) => { input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, ''); - gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`); + FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`); }); const fragments = searchToken.split(':'); @@ -396,10 +415,10 @@ class FilteredSearchManager { const searchTerms = inputValues.join(' '); input.value = input.value.replace(searchTerms, ''); - gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms); + FilteredSearchVisualTokens.addSearchVisualToken(searchTerms); } - gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey); + FilteredSearchVisualTokens.addFilterVisualToken(tokenKey); input.value = input.value.replace(`${tokenKey}:`, ''); } } else { @@ -407,7 +426,7 @@ class FilteredSearchManager { const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g; if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') { - gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken); + FilteredSearchVisualTokens.addFilterVisualToken(searchToken); // Trim the last space as seen in the if statement above input.value = input.value.replace(searchToken, '').trim(); @@ -423,7 +442,7 @@ class FilteredSearchManager { saveCurrentSearchQuery() { // Don't save before we have fetched the already saved searches this.fetchingRecentSearchesPromise.then(() => { - const searchQuery = gl.DropdownUtils.getSearchQuery(); + const searchQuery = DropdownUtils.getSearchQuery(); if (searchQuery.length > 0) { const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery); this.recentSearchesService.save(resultantSearches); @@ -439,7 +458,7 @@ class FilteredSearchManager { } loadSearchParamsFromURL() { - const urlParams = gl.utils.getUrlParamsArray(); + const urlParams = getUrlParamsArray(); const params = this.getAllParams(urlParams); const usernameParams = this.getUsernameParams(); let hasFilteredSearch = false; @@ -455,7 +474,7 @@ class FilteredSearchManager { if (condition) { hasFilteredSearch = true; const canEdit = this.canEdit && this.canEdit(condition.tokenKey); - gl.FilteredSearchVisualTokens.addFilterVisualToken( + FilteredSearchVisualTokens.addFilterVisualToken( condition.tokenKey, condition.value, canEdit, @@ -484,7 +503,7 @@ class FilteredSearchManager { hasFilteredSearch = true; const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue); - gl.FilteredSearchVisualTokens.addFilterVisualToken( + FilteredSearchVisualTokens.addFilterVisualToken( sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`, canEdit, @@ -495,7 +514,7 @@ class FilteredSearchManager { hasFilteredSearch = true; const tokenName = 'assignee'; const canEdit = this.canEdit && this.canEdit(tokenName); - gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit); + FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit); } } else if (!match && keyParam === 'author_id') { const id = parseInt(value, 10); @@ -503,7 +522,7 @@ class FilteredSearchManager { hasFilteredSearch = true; const tokenName = 'author'; const canEdit = this.canEdit && this.canEdit(tokenName); - gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit); + FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit); } } else if (!match && keyParam === 'search') { hasFilteredSearch = true; @@ -535,13 +554,13 @@ class FilteredSearchManager { search(state = null) { const paths = []; - const searchQuery = gl.DropdownUtils.getSearchQuery(); + const searchQuery = DropdownUtils.getSearchQuery(); this.saveCurrentSearchQuery(); const { tokens, searchToken } = this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys()); - const currentState = state || gl.utils.getParameterByName('state') || 'opened'; + const currentState = state || getParameterByName('state') || 'opened'; paths.push(`state=${currentState}`); tokens.forEach((token) => { @@ -620,6 +639,3 @@ class FilteredSearchManager { return true; } } - -window.gl = window.gl || {}; -gl.FilteredSearchManager = FilteredSearchManager; diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js index be595d7df1a..087ef5cd6f2 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -71,7 +71,7 @@ const conditions = [{ value: 'none', }]; -class FilteredSearchTokenKeys { +export default class FilteredSearchTokenKeys { static get() { return tokenKeys; } @@ -121,6 +121,3 @@ class FilteredSearchTokenKeys { .find(condition => condition.tokenKey === key && condition.value === value) || null; } } - -window.gl = window.gl || {}; -gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys; diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js index f2e66503e5e..d75610f6d68 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js @@ -1,6 +1,6 @@ import './filtered_search_token_keys'; -class FilteredSearchTokenizer { +export default class FilteredSearchTokenizer { static processTokens(input, allowedKeys) { // Regex extracts `(token):(symbol)(value)` // Values that start with a double quote must end in a double quote (same for single) @@ -50,6 +50,3 @@ class FilteredSearchTokenizer { }; } } - -window.gl = window.gl || {}; -gl.FilteredSearchTokenizer = FilteredSearchTokenizer; diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index 2e859d2de3a..a19bb882410 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -3,8 +3,9 @@ import AjaxCache from '../lib/utils/ajax_cache'; import Flash from '../flash'; import FilteredSearchContainer from './container'; import UsersCache from '../lib/utils/users_cache'; +import DropdownUtils from './dropdown_utils'; -class FilteredSearchVisualTokens { +export default class FilteredSearchVisualTokens { static getLastVisualTokenBeforeInput() { const inputLi = FilteredSearchContainer.container.querySelector('.input-token'); const lastVisualToken = inputLi && inputLi.previousElementSibling; @@ -74,7 +75,7 @@ class FilteredSearchVisualTokens { let processed = labels; if (!labels.preprocessed) { - processed = gl.DropdownUtils.duplicateLabelPreprocessing(labels); + processed = DropdownUtils.duplicateLabelPreprocessing(labels); AjaxCache.override(labelsEndpoint, processed); processed.preprocessed = true; } @@ -90,7 +91,7 @@ class FilteredSearchVisualTokens { return AjaxCache.retrieve(labelsEndpoint) .then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint)) .then((labels) => { - const matchingLabel = (labels || []).find(label => `~${gl.DropdownUtils.getEscapedText(label.title)}` === tokenValue); + const matchingLabel = (labels || []).find(label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue); if (!matchingLabel) { return; @@ -259,11 +260,11 @@ class FilteredSearchVisualTokens { static tokenizeInput() { const input = FilteredSearchContainer.container.querySelector('.filtered-search'); const { isLastVisualTokenValid } = - gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); if (input.value) { if (isLastVisualTokenValid) { - gl.FilteredSearchVisualTokens.addSearchVisualToken(input.value); + FilteredSearchVisualTokens.addSearchVisualToken(input.value); } else { FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement(input.value); } @@ -324,12 +325,12 @@ class FilteredSearchVisualTokens { if (!tokenContainer.lastElementChild.isEqualNode(inputLi)) { const { isLastVisualTokenValid } = - gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); if (!isLastVisualTokenValid) { - const lastPartial = gl.FilteredSearchVisualTokens.getLastTokenPartial(); - gl.FilteredSearchVisualTokens.removeLastTokenPartial(); - gl.FilteredSearchVisualTokens.addSearchVisualToken(lastPartial); + const lastPartial = FilteredSearchVisualTokens.getLastTokenPartial(); + FilteredSearchVisualTokens.removeLastTokenPartial(); + FilteredSearchVisualTokens.addSearchVisualToken(lastPartial); } tokenContainer.removeChild(inputLi); @@ -337,6 +338,3 @@ class FilteredSearchVisualTokens { } } } - -window.gl = window.gl || {}; -gl.FilteredSearchVisualTokens = FilteredSearchVisualTokens; diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js index c99ed63c4af..f9338b82acf 100644 --- a/app/assets/javascripts/filtered_search/recent_searches_root.js +++ b/app/assets/javascripts/filtered_search/recent_searches_root.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content'; +import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content.vue'; import eventHub from './event_hub'; class RecentSearchesRoot { @@ -33,7 +33,7 @@ class RecentSearchesRoot { this.vm = new Vue({ el: this.wrapperElement, components: { - 'recent-searches-dropdown-content': RecentSearchesDropdownContent, + RecentSearchesDropdownContent, }, data() { return state; }, template: ` diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 15df7a7f989..6cf78bab6ad 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -485,7 +485,7 @@ GitLabDropdown = (function() { $target = $(e.target); if ($target && !$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && - !$target.data('is-link')) { + !$target.data('isLink')) { e.stopPropagation(); return false; } else { @@ -607,7 +607,20 @@ GitLabDropdown = (function() { }; GitLabDropdown.prototype.renderItem = function(data, group, index) { - var field, fieldName, html, selected, text, url, value; + var field, fieldName, html, selected, text, url, value, rowHidden; + + if (!this.options.renderRow) { + value = this.options.id ? this.options.id(data) : data.id; + + if (value) { + value = value.toString().replace(/'/g, '\\\''); + } + } + + // Hide element + if (this.options.hideRow && this.options.hideRow(value)) { + rowHidden = true; + } if (group == null) { group = false; } @@ -616,6 +629,7 @@ GitLabDropdown = (function() { index = false; } html = document.createElement('li'); + if (data === 'divider' || data === 'separator') { html.className = data; return html; @@ -631,11 +645,9 @@ GitLabDropdown = (function() { html = this.options.renderRow.call(this.options, data, this); } else { if (!selected) { - value = this.options.id ? this.options.id(data) : data.id; fieldName = this.options.fieldName; if (value) { - value = value.toString().replace(/'/g, '\\\''); field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`); if (field.length) { selected = true; diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index d0f9e6af0f8..2d40856e038 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -1,5 +1,4 @@ -/* global autosize */ - +import autosize from 'autosize'; import GfmAutoComplete from './gfm_auto_complete'; import dropzoneInput from './dropzone_input'; import textUtils from './lib/utils/text_markdown'; @@ -13,7 +12,7 @@ export default class GLForm { this.destroy(); // Setup the form this.setupForm(); - this.form.data('gl-form', this); + this.form.data('glForm', this); } destroy() { @@ -22,7 +21,7 @@ export default class GLForm { if (this.autoComplete) { this.autoComplete.destroy(); } - this.form.data('gl-form', null); + this.form.data('glForm', null); } setupForm() { diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js index 7ac9dcd1112..6bf21f4f27d 100644 --- a/app/assets/javascripts/gpg_badges.js +++ b/app/assets/javascripts/gpg_badges.js @@ -1,3 +1,8 @@ +import { parseQueryStringIntoObject } from '~/lib/utils/common_utils'; +import axios from '~/lib/utils/axios_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; + export default class GpgBadges { static fetch() { const badges = $('.js-loading-gpg-badge'); @@ -5,13 +10,13 @@ export default class GpgBadges { badges.html('<i class="fa fa-spinner fa-spin"></i>'); - $.get({ - url: form.data('signatures-path'), - data: form.serialize(), - }).done((response) => { - response.signatures.forEach((signature) => { + const params = parseQueryStringIntoObject(form.serialize()); + return axios.get(form.data('signaturesPath'), { params }) + .then(({ data }) => { + data.signatures.forEach((signature) => { badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); }); - }); + }) + .catch(() => flash(__('An error occurred while loading commits'))); } } diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js deleted file mode 100644 index 534bc535bb6..00000000000 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ /dev/null @@ -1,4 +0,0 @@ -import Chart from 'vendor/Chart'; - -// export to global scope -window.Chart = Chart; diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index e035ba462db..b8f0566f48c 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -152,14 +152,14 @@ export default { showLeaveGroupModal(group, parentGroup) { this.targetGroup = group; this.targetParentGroup = parentGroup; - this.showModal = true; + this.updateModal = true; this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`); }, hideLeaveGroupModal() { - this.showModal = false; + this.updateModal = false; }, leaveGroup() { - this.showModal = false; + this.updateModal = false; this.targetGroup.isBeingRemoved = true; this.service.leaveGroup(this.targetGroup.leavePath) .then(res => res.json()) diff --git a/app/assets/javascripts/groups/components/item_stats_value.vue b/app/assets/javascripts/groups/components/item_stats_value.vue index 08d0bf6e344..4d86ac8023c 100644 --- a/app/assets/javascripts/groups/components/item_stats_value.vue +++ b/app/assets/javascripts/groups/components/item_stats_value.vue @@ -30,11 +30,11 @@ default: 'bottom', }, /** - * value could either be number or string - * as `memberCount` is always passed as string - * while `subgroupCount` & `projectCount` - * are always number - */ + * value could either be number or string + * as `memberCount` is always passed as string + * while `subgroupCount` & `projectCount` + * are always number + */ value: { type: [Number, String], required: false, diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index a69a0bde17b..12fc5f9b5c9 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,13 +1,14 @@ +import axios from './lib/utils/axios_utils'; import Api from './api'; -import { normalizeCRLFHeaders } from './lib/utils/common_utils'; +import { normalizeHeaders } from './lib/utils/common_utils'; export default function groupsSelect() { // Needs to be accessible in rspec window.GROUP_SELECT_PER_PAGE = 20; $('.ajax-groups-select').each(function setAjaxGroupsSelect2() { const $select = $(this); - const allAvailable = $select.data('all-available'); - const skipGroups = $select.data('skip-groups') || []; + const allAvailable = $select.data('allAvailable'); + const skipGroups = $select.data('skipGroups') || []; $select.select2({ placeholder: 'Search for a group', multiple: $select.hasClass('multiselect'), @@ -17,24 +18,23 @@ export default function groupsSelect() { dataType: 'json', quietMillis: 250, transport(params) { - return $.ajax(params) - .then((data, status, xhr) => { - const results = data || []; - - const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders()); + axios[params.type.toLowerCase()](params.url, { + params: params.data, + }) + .then((res) => { + const results = res.data || []; + const headers = normalizeHeaders(res.headers); const currentPage = parseInt(headers['X-PAGE'], 10) || 0; const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; const more = currentPage < totalPages; - return { + params.success({ results, pagination: { more, }, - }; - }) - .then(params.success) - .fail(params.error); + }); + }).catch(params.error); }, data(search, page) { return { diff --git a/app/assets/javascripts/help/help.js b/app/assets/javascripts/help/help.js index 4a22ebf187d..d02477b19a2 100644 --- a/app/assets/javascripts/help/help.js +++ b/app/assets/javascripts/help/help.js @@ -1,6 +1,8 @@ // We will render the icons list here -if ($('#user-content-gitlab-icons').length > 0) { - const $iconsHeader = $('#user-content-gitlab-icons'); - const $iconsList = $('<div id="iconsList">ICONS</div>'); - $($iconsList).insertAfter($iconsHeader.parent()); -} +export default () => { + if ($('#user-content-gitlab-icons').length > 0) { + const $iconsHeader = $('#user-content-gitlab-icons'); + const $iconsList = $('<div id="iconsList">ICONS</div>'); + $($iconsList).insertAfter($iconsHeader.parent()); + } +}; diff --git a/app/assets/javascripts/how_to_merge.js b/app/assets/javascripts/how_to_merge.js index 19f4a946f73..12e6f24595a 100644 --- a/app/assets/javascripts/how_to_merge.js +++ b/app/assets/javascripts/how_to_merge.js @@ -1,12 +1,13 @@ -document.addEventListener('DOMContentLoaded', () => { - const modal = $('#modal_merge_info').modal({ - modal: true, - show: false, - }); - $('.how_to_merge_link').on('click', () => { - modal.show(); - }); - $('.modal-header .close').on('click', () => { - modal.hide(); - }); -}); +export default () => { + const modal = $('#modal_merge_info'); + + if (modal) { + modal.modal({ + modal: true, + show: false, + }); + + $('.how_to_merge_link').on('click', modal.show); + $('.modal-header .close').on('click', modal.hide); + } +}; diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue index dd947f66969..9d933b8891d 100644 --- a/app/assets/javascripts/ide/components/ide_context_bar.vue +++ b/app/assets/javascripts/ide/components/ide_context_bar.vue @@ -1,8 +1,8 @@ <script> import { mapGetters, mapState, mapActions } from 'vuex'; + import icon from '~/vue_shared/components/icon.vue'; + import panelResizer from '~/vue_shared/components/panel_resizer.vue'; import repoCommitSection from './repo_commit_section.vue'; - import icon from '../../vue_shared/components/icon.vue'; - import panelResizer from '../../vue_shared/components/panel_resizer.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue index af2f7341a91..2fbff2bd789 100644 --- a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue +++ b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue @@ -1,6 +1,6 @@ <script> +import icon from '~/vue_shared/components/icon.vue'; import repoTree from './ide_repo_tree.vue'; -import icon from '../../vue_shared/components/icon.vue'; import newDropdown from './new_dropdown/index.vue'; export default { diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue index ed49a0e72a2..32bf7175c88 100644 --- a/app/assets/javascripts/ide/components/ide_project_tree.vue +++ b/app/assets/javascripts/ide/components/ide_project_tree.vue @@ -1,6 +1,6 @@ <script> +import projectAvatarImage from '~/vue_shared/components/project_avatar/image.vue'; import branchesTree from './ide_project_branches_tree.vue'; -import projectAvatarImage from '../../vue_shared/components/project_avatar/image.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue index 4651e345d75..4a324264992 100644 --- a/app/assets/javascripts/ide/components/ide_repo_tree.vue +++ b/app/assets/javascripts/ide/components/ide_repo_tree.vue @@ -1,8 +1,8 @@ <script> import { mapState } from 'vuex'; +import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import repoPreviousDirectory from './repo_prev_directory.vue'; import repoFile from './repo_file.vue'; -import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; import { treeList } from '../stores/utils'; export default { diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index a68f8ce0169..18b5059a17f 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,9 +1,9 @@ <script> import { mapState, mapActions } from 'vuex'; + import icon from '~/vue_shared/components/icon.vue'; + import panelResizer from '~/vue_shared/components/panel_resizer.vue'; + import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import projectTree from './ide_project_tree.vue'; - import icon from '../../vue_shared/components/icon.vue'; - import panelResizer from '../../vue_shared/components/panel_resizer.vue'; - import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index e48c446c4a4..97ae64b206d 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -1,8 +1,8 @@ <script> import { mapState } from 'vuex'; - import icon from '../../vue_shared/components/icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; - import timeAgoMixin from '../../vue_shared/mixins/timeago'; + import icon from '~/vue_shared/components/icon.vue'; + import tooltip from '~/vue_shared/directives/tooltip'; + import timeAgoMixin from '~/vue_shared/mixins/timeago'; export default { components: { diff --git a/app/assets/javascripts/ide/components/new_branch_form.vue b/app/assets/javascripts/ide/components/new_branch_form.vue index 56e31256132..1e8d5bb6453 100644 --- a/app/assets/javascripts/ide/components/new_branch_form.vue +++ b/app/assets/javascripts/ide/components/new_branch_form.vue @@ -1,7 +1,7 @@ <script> import { mapState, mapActions } from 'vuex'; - import flash, { hideFlash } from '../../flash'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import flash, { hideFlash } from '~/flash'; + import loadingIcon from '~/vue_shared/components/loading_icon.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 96b1bb78c1d..37f2cf30a29 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -1,8 +1,8 @@ <script> import { mapGetters, mapState, mapActions } from 'vuex'; -import tooltip from '../../vue_shared/directives/tooltip'; -import icon from '../../vue_shared/components/icon.vue'; -import modal from '../../vue_shared/components/modal.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import icon from '~/vue_shared/components/icon.vue'; +import modal from '~/vue_shared/components/modal.vue'; import commitFilesList from './commit_sidebar/list.vue'; export default { diff --git a/app/assets/javascripts/ide/components/repo_edit_button.vue b/app/assets/javascripts/ide/components/repo_edit_button.vue index c43e9163340..fe4320731d9 100644 --- a/app/assets/javascripts/ide/components/repo_edit_button.vue +++ b/app/assets/javascripts/ide/components/repo_edit_button.vue @@ -1,6 +1,6 @@ <script> import { mapGetters, mapActions, mapState } from 'vuex'; -import modal from '../../vue_shared/components/modal.vue'; +import modal from '~/vue_shared/components/modal.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index f99228012f4..f31cc12339b 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -1,7 +1,7 @@ <script> /* global monaco */ import { mapState, mapGetters, mapActions } from 'vuex'; -import flash from '../../flash'; +import flash from '~/flash'; import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index 110918872fb..cbbab765e1c 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -1,9 +1,9 @@ <script> import { mapState } from 'vuex'; - import timeAgoMixin from '../../vue_shared/mixins/timeago'; - import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; + import timeAgoMixin from '~/vue_shared/mixins/timeago'; + import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; + import fileIcon from '~/vue_shared/components/file_icon.vue'; import newDropdown from './new_dropdown/index.vue'; - import fileIcon from '../../vue_shared/components/file_icon.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue index 3aeb6f0b28f..79af8c0b0c7 100644 --- a/app/assets/javascripts/ide/components/repo_loading_file.vue +++ b/app/assets/javascripts/ide/components/repo_loading_file.vue @@ -1,6 +1,6 @@ <script> import { mapState } from 'vuex'; - import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; + import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/components/repo_preview.vue b/app/assets/javascripts/ide/components/repo_preview.vue index e47270a9855..a216269e292 100644 --- a/app/assets/javascripts/ide/components/repo_preview.vue +++ b/app/assets/javascripts/ide/components/repo_preview.vue @@ -1,7 +1,7 @@ <script> import { mapGetters } from 'vuex'; - import LineHighlighter from '../../line_highlighter'; - import syntaxHighlight from '../../syntax_highlight'; + import LineHighlighter from '~/line_highlighter'; + import syntaxHighlight from '~/syntax_highlight'; export default { computed: { diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index 5ed7bddf6ae..5656081c598 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -1,6 +1,6 @@ <script> import { mapActions } from 'vuex'; - import fileIcon from '../../vue_shared/components/file_icon.vue'; + import fileIcon from '~/vue_shared/components/file_icon.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/monaco_loader.js b/app/assets/javascripts/ide/monaco_loader.js index af83a1ec0b4..142a220097b 100644 --- a/app/assets/javascripts/ide/monaco_loader.js +++ b/app/assets/javascripts/ide/monaco_loader.js @@ -6,6 +6,11 @@ monacoContext.require.config({ }, }); +// ignore CDN config and use local assets path for service worker which cannot be cross-domain +const relativeRootPath = (gon && gon.relative_url_root) || ''; +const monacoPath = `${relativeRootPath}/assets/webpack/monaco-editor/vs`; +window.MonacoEnvironment = { getWorkerUrl: () => `${monacoPath}/base/worker/workerMain.js` }; + // eslint-disable-next-line no-underscore-dangle window.__monaco_context__ = monacoContext; export default monacoContext.require; diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index d007d0ae78f..2c690b1f635 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import { visitUrl } from '../../lib/utils/url_utility'; -import flash from '../../flash'; +import { visitUrl } from '~/lib/utils/url_utility'; +import flash from '~/flash'; import service from '../services'; import * as types from './mutation_types'; import { stripHtml } from '../../lib/utils/text_utility'; diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index 1dc70872d92..35094f8e73b 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -1,3 +1,7 @@ +import { __ } from './locale'; +import axios from './lib/utils/axios_utils'; +import flash from './flash'; + class ImporterStatus { constructor(jobsUrl, importUrl) { this.jobsUrl = jobsUrl; @@ -9,29 +13,7 @@ class ImporterStatus { initStatusPage() { $('.js-add-to-import') .off('click') - .on('click', (event) => { - const $btn = $(event.currentTarget); - const $tr = $btn.closest('tr'); - const $targetField = $tr.find('.import-target'); - const $namespaceInput = $targetField.find('.js-select-namespace option:selected'); - const id = $tr.attr('id').replace('repo_', ''); - let targetNamespace; - let newName; - if ($namespaceInput.length > 0) { - targetNamespace = $namespaceInput[0].innerHTML; - newName = $targetField.find('#path').prop('value'); - $targetField.empty().append(`${targetNamespace}/${newName}`); - } - $btn.disable().addClass('is-loading'); - - return $.post(this.importUrl, { - repo_id: id, - target_namespace: targetNamespace, - new_name: newName, - }, { - dataType: 'script', - }); - }); + .on('click', this.addToImport.bind(this)); $('.js-import-all') .off('click') @@ -44,34 +26,74 @@ class ImporterStatus { }); } - setAutoUpdate() { - return setInterval(() => $.get(this.jobsUrl, data => $.each(data, (i, job) => { - const jobItem = $(`#project_${job.id}`); - const statusField = jobItem.find('.job-status'); + addToImport(event) { + const $btn = $(event.currentTarget); + const $tr = $btn.closest('tr'); + const $targetField = $tr.find('.import-target'); + const $namespaceInput = $targetField.find('.js-select-namespace option:selected'); + const id = $tr.attr('id').replace('repo_', ''); + let targetNamespace; + let newName; + if ($namespaceInput.length > 0) { + targetNamespace = $namespaceInput[0].innerHTML; + newName = $targetField.find('#path').prop('value'); + $targetField.empty().append(`${targetNamespace}/${newName}`); + } + $btn.disable().addClass('is-loading'); + + return axios.post(this.importUrl, { + repo_id: id, + target_namespace: targetNamespace, + new_name: newName, + }) + .then(({ data }) => { + const job = $(`tr#repo_${id}`); + job.attr('id', `project_${data.id}`); - const spinner = '<i class="fa fa-spinner fa-spin"></i>'; + job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`); + $('table.import-jobs tbody').prepend(job); - switch (job.import_status) { - case 'finished': - jobItem.removeClass('active').addClass('success'); - statusField.html('<span><i class="fa fa-check"></i> done</span>'); - break; - case 'scheduled': - statusField.html(`${spinner} scheduled`); - break; - case 'started': - statusField.html(`${spinner} started`); - break; - default: - statusField.html(job.import_status); - break; - } - })), 4000); + job.addClass('active'); + job.find('.import-actions').html('<i class="fa fa-spinner fa-spin" aria-label="importing"></i> started'); + }) + .catch(() => flash(__('An error occurred while importing project'))); + } + + autoUpdate() { + return axios.get(this.jobsUrl) + .then(({ data = [] }) => { + data.forEach((job) => { + const jobItem = $(`#project_${job.id}`); + const statusField = jobItem.find('.job-status'); + + const spinner = '<i class="fa fa-spinner fa-spin"></i>'; + + switch (job.import_status) { + case 'finished': + jobItem.removeClass('active').addClass('success'); + statusField.html('<span><i class="fa fa-check"></i> done</span>'); + break; + case 'scheduled': + statusField.html(`${spinner} scheduled`); + break; + case 'started': + statusField.html(`${spinner} started`); + break; + default: + statusField.html(job.import_status); + break; + } + }); + }); + } + + setAutoUpdate() { + setInterval(this.autoUpdate.bind(this), 4000); } } // eslint-disable-next-line consistent-return -export default function initImporterStatus() { +function initImporterStatus() { const importerStatus = document.querySelector('.js-importer-status'); if (importerStatus) { @@ -79,3 +101,8 @@ export default function initImporterStatus() { return new ImporterStatus(data.jobsImportPath, data.importPath); } } + +export { + initImporterStatus as default, + ImporterStatus, +}; diff --git a/app/assets/javascripts/integrations/index.js b/app/assets/javascripts/integrations/index.js deleted file mode 100644 index 10fe6bac0e8..00000000000 --- a/app/assets/javascripts/integrations/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable no-new */ -import IntegrationSettingsForm from './integration_settings_form'; - -$(() => { - const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); - integrationSettingsForm.init(); -}); diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js index 3f27cfc2f6d..2848fe003cb 100644 --- a/app/assets/javascripts/integrations/integration_settings_form.js +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -6,8 +6,8 @@ export default class IntegrationSettingsForm { this.$form = $(formSelector); // Form Metadata - this.canTestService = this.$form.data('can-test'); - this.testEndPoint = this.$form.data('test-url'); + this.canTestService = this.$form.data('canTest'); + this.testEndPoint = this.$form.data('testUrl'); // Form Child Elements this.$serviceToggle = this.$form.find('#service_active'); diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index ff65ea99e9a..333bbd9e0ba 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,5 +1,4 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */ -import 'vendor/jquery.waitforimages'; import axios from './lib/utils/axios_utils'; import { addDelimiter } from './lib/utils/text_utility'; import flash from './flash'; @@ -25,6 +24,51 @@ export default class Issue { if (Issue.createMrDropdownWrap) { this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap); } + + // Listen to state changes in the Vue app + document.addEventListener('issuable_vue_app:change', (event) => { + this.updateTopState(event.detail.isClosed, event.detail.data); + }); + } + + /** + * This method updates the top area of the issue. + * + * Once the issue state changes, either through a click on the top area (jquery) + * or a click on the bottom area (Vue) we need to update the top area. + * + * @param {Boolean} isClosed + * @param {Array} data + * @param {String} issueFailMessage + */ + updateTopState(isClosed, data, issueFailMessage = 'Unable to update this issue at this time.') { + if ('id' in data) { + const isClosedBadge = $('div.status-box-issue-closed'); + const isOpenBadge = $('div.status-box-open'); + const projectIssuesCounter = $('.issue_counter'); + + isClosedBadge.toggleClass('hidden', !isClosed); + isOpenBadge.toggleClass('hidden', isClosed); + + $(document).trigger('issuable:change', isClosed); + this.toggleCloseReopenButton(isClosed); + + let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, '')); + numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1; + projectIssuesCounter.text(addDelimiter(numProjectIssues)); + + if (this.createMergeRequestDropdown) { + if (isClosed) { + this.createMergeRequestDropdown.unavailable(); + this.createMergeRequestDropdown.disable(); + } else { + // We should check in case a branch was created in another tab + this.createMergeRequestDropdown.checkAbilityToCreateBranch(); + } + } + } else { + flash(issueFailMessage); + } } initIssueBtnEventListeners() { @@ -45,34 +89,8 @@ export default class Issue { url = $button.attr('href'); return axios.put(url) .then(({ data }) => { - const isClosedBadge = $('div.status-box-issue-closed'); - const isOpenBadge = $('div.status-box-open'); - const projectIssuesCounter = $('.issue_counter'); - - if ('id' in data) { - const isClosed = $button.hasClass('btn-close'); - isClosedBadge.toggleClass('hidden', !isClosed); - isOpenBadge.toggleClass('hidden', isClosed); - - $(document).trigger('issuable:change', isClosed); - this.toggleCloseReopenButton(isClosed); - - let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, '')); - numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1; - projectIssuesCounter.text(addDelimiter(numProjectIssues)); - - if (this.createMergeRequestDropdown) { - if (isClosed) { - this.createMergeRequestDropdown.unavailable(); - this.createMergeRequestDropdown.disable(); - } else { - // We should check in case a branch was created in another tab - this.createMergeRequestDropdown.checkAbilityToCreateBranch(); - } - } - } else { - flash(issueFailMessage); - } + const isClosed = $button.hasClass('btn-close'); + this.updateTopState(isClosed, data); }) .catch(() => flash(issueFailMessage)) .then(() => { diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index 9afa9dea126..1338be0ec4b 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -78,6 +78,7 @@ taskListUpdateSuccess(data) { try { this.checkForSpam(data); + this.closeRecaptcha(); } catch (error) { if (error && error.name === 'SpamError') this.openRecaptcha(); } diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js index 03546f61d1f..71c0f894389 100644 --- a/app/assets/javascripts/issue_status_select.js +++ b/app/assets/javascripts/issue_status_select.js @@ -1,6 +1,6 @@ export default function issueStatusSelect() { $('.js-issue-status').each((i, el) => { - const fieldName = $(el).data('field-name'); + const fieldName = $(el).data('fieldName'); return $(el).glDropdown({ selectable: true, fieldName, diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index d0b7ea75082..f39ae764d3c 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -217,7 +217,7 @@ export default class Job { } this.isLogComplete = log.complete; - if (!log.complete) { + if (log.complete === false) { this.timeout = setTimeout(() => { this.getBuildTrace(); }, 4000); diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index db53b04de0e..85a88ae409b 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -3,7 +3,7 @@ import JobMediator from './job_details_mediator'; import jobHeader from './components/header.vue'; import detailsBlock from './components/sidebar_details_block.vue'; -document.addEventListener('DOMContentLoaded', () => { +export default () => { const dataset = document.getElementById('js-job-details-vue').dataset; const mediator = new JobMediator({ endpoint: dataset.endpoint }); @@ -55,4 +55,4 @@ document.addEventListener('DOMContentLoaded', () => { }); }, }); -}); +}; diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 5ecf81ad11d..9b46bbf83da 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -21,23 +21,23 @@ export default class LabelsSelect { } $els.each(function(i, dropdown) { - var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; + var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; $dropdown = $(dropdown); $dropdownContainer = $dropdown.closest('.labels-filter'); $toggleText = $dropdown.find('.dropdown-toggle-text'); - namespacePath = $dropdown.data('namespace-path'); - projectPath = $dropdown.data('project-path'); + namespacePath = $dropdown.data('namespacePath'); + projectPath = $dropdown.data('projectPath'); labelUrl = $dropdown.data('labels'); issueUpdateURL = $dropdown.data('issueUpdate'); selectedLabel = $dropdown.data('selected'); if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) { selectedLabel = selectedLabel.split(','); } - showNo = $dropdown.data('show-no'); - showAny = $dropdown.data('show-any'); + showNo = $dropdown.data('showNo'); + showAny = $dropdown.data('showAny'); showMenuAbove = $dropdown.data('showMenuAbove'); - defaultLabel = $dropdown.data('default-label'); - abilityName = $dropdown.data('ability-name'); + defaultLabel = $dropdown.data('defaultLabel'); + abilityName = $dropdown.data('abilityName'); $selectbox = $dropdown.closest('.selectbox'); $block = $selectbox.closest('.block'); $form = $dropdown.closest('form, .js-issuable-update'); @@ -45,21 +45,14 @@ export default class LabelsSelect { $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip'); $value = $block.find('.value'); $loading = $block.find('.block-loading').fadeOut(); - fieldName = $dropdown.data('field-name'); + fieldName = $dropdown.data('fieldName'); useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown'); propertyName = useId ? 'id' : 'title'; initialSelected = $selectbox - .find('input[name="' + $dropdown.data('field-name') + '"]') + .find('input[name="' + $dropdown.data('fieldName') + '"]') .map(function () { return this.value; }).get(); - if (issueUpdateURL != null) { - issueURLSplit = issueUpdateURL.split('/'); - } - if (issueUpdateURL) { - labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>'); - labelNoneHTMLTemplate = '<span class="no-value">None</span>'; - } const handleClick = options.handleClick; $sidebarLabelTooltip.tooltip(); @@ -91,14 +84,17 @@ export default class LabelsSelect { $loading.fadeOut(); $dropdown.trigger('loaded.gl.dropdown'); $selectbox.hide(); - data.issueURLSplit = issueURLSplit; + data.issueUpdateURL = issueUpdateURL; labelCount = 0; - if (data.labels.length) { - template = labelHTMLTemplate(data); + if (data.labels.length && issueUpdateURL) { + template = LabelsSelect.getLabelTemplate({ + labels: data.labels, + issueUpdateURL, + }); labelCount = data.labels.length; } else { - template = labelNoneHTMLTemplate; + template = '<span class="no-value">None</span>'; } $value.removeAttr('style').html(template); $sidebarCollapsedValue.text(labelCount); @@ -213,7 +209,7 @@ export default class LabelsSelect { } } if (label.duplicate) { - color = gl.DropdownUtils.duplicateLabelColor(label.color); + color = DropdownUtils.duplicateLabelColor(label.color); } else { if (label.color != null) { @@ -242,10 +238,16 @@ export default class LabelsSelect { filterable: true, selected: $dropdown.data('selected') || [], toggleLabel: function(selected, el) { + var $dropdownParent = $dropdown.parent(); + var $dropdownInputField = $dropdownParent.find('.dropdown-input-field'); var isSelected = el !== null ? el.hasClass('is-active') : false; var title = selected.title; var selectedLabels = this.selected; + if ($dropdownInputField.length && $dropdownInputField.val().length) { + $dropdownParent.find('.dropdown-input-clear').trigger('click'); + } + if (selected.id === 0) { this.selected = []; return 'No Label'; @@ -268,7 +270,7 @@ export default class LabelsSelect { return defaultLabel; } }, - fieldName: $dropdown.data('field-name'), + fieldName: $dropdown.data('fieldName'), id: function(label) { if (label.id <= 0) return label.title; @@ -316,9 +318,9 @@ export default class LabelsSelect { }, multiSelect: $dropdown.hasClass('js-multiselect'), vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function(options) { - const { $el, e, isMarking } = options; - const label = options.selectedObj; + clicked: function (clickEvent) { + const { $el, e, isMarking } = clickEvent; + const label = clickEvent.selectedObj; var isIssueIndex, isMRIndex, page, boardsModel; var fadeOutLoader = () => { @@ -412,6 +414,26 @@ export default class LabelsSelect { this.bindEvents(); } + static getLabelTemplate(tplData) { + // We could use ES6 template string here + // and properly indent markup for readability + // but that also introduces unintended white-space + // so best approach is to use traditional way of + // concatenation + // see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays + const tpl = _.template([ + '<% _.each(labels, function(label){ %>', + '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">', + '<span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">', + '<%- label.title %>', + '</span>', + '</a>', + '<% }); %>', + ].join('')); + + return tpl(tplData); + } + bindEvents() { return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue); } diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index ab3cc29146a..1b4900827b8 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -4,7 +4,7 @@ import initFlyOutNav from './fly_out_nav'; function hideEndFade($scrollingTabs) { $scrollingTabs.each(function scrollTabsLoop() { const $this = $(this); - $this.siblings('.fade-right').toggleClass('scrolling', $this.width() < $this.prop('scrollWidth')); + $this.siblings('.fade-right').toggleClass('scrolling', Math.round($this.width()) < $this.prop('scrollWidth')); }); } diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 5811d059e0b..e741789fbb6 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,5 +1,8 @@ +import jQuery from 'jquery'; +import Cookies from 'js-cookie'; import axios from './axios_utils'; import { getLocationHash } from './url_utility'; +import { convertToCamelCase } from './text_utility'; export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index]; @@ -21,13 +24,18 @@ export const getGroupSlug = () => { return null; }; -export const isInIssuePage = () => { - const page = getPagePath(1); - const action = getPagePath(2); +export const checkPageAndAction = (page, action) => { + const pagePath = getPagePath(1); + const actionPath = getPagePath(2); - return page === 'issues' && action === 'show'; + return pagePath === page && actionPath === action; }; +export const isInIssuePage = () => checkPageAndAction('issues', 'show'); +export const isInMRPage = () => checkPageAndAction('merge_requests', 'show'); +export const isInNoteablePage = () => isInIssuePage() || isInMRPage(); +export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions'); + export const ajaxGet = url => axios.get(url, { params: { format: 'js' }, responseType: 'text', @@ -132,7 +140,11 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; // 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) => { +export const scrollToElement = (element) => { + let $el = element; + if (!(element instanceof jQuery)) { + $el = $(element); + } const top = $el.offset().top; const mrTabsHeight = $('.merge-request-tabs').height() || 0; const headerHeight = $('.navbar-gitlab').height() || 0; @@ -395,8 +407,38 @@ export const spriteIcon = (icon, className = '') => { return `<svg ${classAttribute}><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`; }; +/** + * This method takes in object with snake_case property names + * and returns new object with camelCase property names + * + * Reasoning for this method is to ensure consistent property + * naming conventions across JS code. + */ +export const convertObjectPropsToCamelCase = (obj = {}) => { + if (obj === null) { + return {}; + } + + return Object.keys(obj).reduce((acc, prop) => { + const result = acc; + + result[convertToCamelCase(prop)] = obj[prop]; + return acc; + }, {}); +}; + export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`; +export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => { + // Click a .js-select-on-focus field, select the contents + // Prevent a mouseup event from deselecting the input + $(selector).on('focusin', function selectOnFocusCallback() { + $(this).select().one('mouseup', (e) => { + e.preventDefault(); + }); + }); +}; + window.gl = window.gl || {}; window.gl.utils = { ...(window.gl.utils || {}), diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 1fa6715180e..d6cccbef42b 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -10,6 +10,20 @@ window.timeago = timeago; window.dateFormat = dateFormat; /** + * Returns i18n month names array. + * If `abbreviated` is provided, returns abbreviated + * name. + * + * @param {Boolean} abbreviated + */ +const getMonthNames = (abbreviated) => { + if (abbreviated) { + return [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')]; + } + return [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')]; +}; + +/** * Given a date object returns the day of the week in English * @param {date} date * @returns {String} @@ -143,7 +157,6 @@ export const getDayDifference = (a, b) => { * @param {Number} seconds * @return {String} */ -// eslint-disable-next-line import/prefer-default-export export function timeIntervalInWords(intervalInSeconds) { const secondsInteger = parseInt(intervalInSeconds, 10); const minutes = Math.floor(secondsInteger / 60); @@ -158,7 +171,7 @@ export function timeIntervalInWords(intervalInSeconds) { return text; } -export function dateInWords(date, abbreviated = false) { +export function dateInWords(date, abbreviated = false, hideYear = false) { if (!date) return date; const month = date.getMonth(); @@ -169,9 +182,115 @@ export function dateInWords(date, abbreviated = false) { const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month]; + if (hideYear) { + return `${monthName} ${date.getDate()}`; + } + return `${monthName} ${date.getDate()}, ${year}`; } +/** + * Returns month name based on provided date. + * + * @param {Date} date + * @param {Boolean} abbreviated + */ +export const monthInWords = (date, abbreviated = false) => { + if (!date) { + return ''; + } + + return getMonthNames(abbreviated)[date.getMonth()]; +}; + +/** + * Returns number of days in a month for provided date. + * courtesy: https://stacko(verflow.com/a/1185804/414749 + * + * @param {Date} date + */ +export const totalDaysInMonth = (date) => { + if (!date) { + return 0; + } + return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(); +}; + +/** + * Returns list of Dates referring to Sundays of the month + * based on provided date + * + * @param {Date} date + */ +export const getSundays = (date) => { + if (!date) { + return []; + } + + const daysToSunday = ['Saturday', 'Friday', 'Thursday', 'Wednesday', 'Tuesday', 'Monday', 'Sunday']; + + const month = date.getMonth(); + const year = date.getFullYear(); + const sundays = []; + const dateOfMonth = new Date(year, month, 1); + + while (dateOfMonth.getMonth() === month) { + const dayName = getDayName(dateOfMonth); + if (dayName === 'Sunday') { + sundays.push(new Date(dateOfMonth.getTime())); + } + + const daysUntilNextSunday = daysToSunday.indexOf(dayName) + 1; + dateOfMonth.setDate(dateOfMonth.getDate() + daysUntilNextSunday); + } + + return sundays; +}; + +/** + * Returns list of Dates representing a timeframe of Months from month of provided date (inclusive) + * up to provided length + * + * For eg; + * If current month is January 2018 and `length` provided is `6` + * Then this method will return list of Date objects as follows; + * + * [ October 2017, November 2017, December 2017, January 2018, February 2018, March 2018 ] + * + * If current month is March 2018 and `length` provided is `3` + * Then this method will return list of Date objects as follows; + * + * [ February 2018, March 2018, April 2018 ] + * + * @param {Number} length + * @param {Date} date + */ +export const getTimeframeWindow = (length, date) => { + if (!length) { + return []; + } + + const currentDate = date instanceof Date ? date : new Date(); + const currentMonthIndex = Math.floor(length / 2); + const timeframe = []; + + // Move date object backward to the first month of timeframe + currentDate.setDate(1); + currentDate.setMonth(currentDate.getMonth() - currentMonthIndex); + + // Iterate and update date for the size of length + // and push date reference to timeframe list + for (let i = 0; i < length; i += 1) { + timeframe.push(new Date(currentDate.getTime())); + currentDate.setMonth(currentDate.getMonth() + 1); + } + + // Change date of last timeframe item to last date of the month + timeframe[length - 1].setDate(totalDaysInMonth(timeframe[length - 1])); + + return timeframe; +}; + window.gl = window.gl || {}; window.gl.utils = { ...(window.gl.utils || {}), diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 2dc9cf0cc29..5dc98b4a920 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -138,7 +138,7 @@ textUtils.init = function(form) { return $('.js-md', form).off('click').on('click', function() { var $this; $this = $(this); - return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend')); + return self.updateText($this.closest('.md-area').find('textarea'), $this.data('mdTag'), $this.data('mdBlock'), !$this.data('mdPrepend')); }); }; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 62d80c4a649..c0ce0786518 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -65,6 +65,20 @@ export function capitalizeFirstCharacter(text) { return `${text[0].toUpperCase()}${text.slice(1)}`; } +export function camelCase(str) { + return str.replace(/_+([a-z])/gi, ($1, $2) => $2.toUpperCase()); +} + +export function camelCaseKeys(obj = {}) { + return Object.keys(obj).reduce((acc, key) => { + const camelKey = camelCase(key); + return { + ...acc, + [camelKey]: obj[key], + }; + }, {}); +} + /** * Replaces all html tags from a string with the given replacement. * @@ -73,3 +87,10 @@ export function capitalizeFirstCharacter(text) { * @returns {String} */ export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace); + +/** + * Converts snake_case string to camelCase + * + * @param {*} string + */ +export const convertToCamelCase = string => string.replace(/(_\w)/g, s => s[1].toUpperCase()); diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index fbd381d8ff7..e5c1fce3db9 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -83,7 +83,7 @@ LineHighlighter.prototype.clickHandler = function(event) { var current, lineNumber, range; event.preventDefault(); this.clearHighlight(); - lineNumber = $(event.target).closest('a').data('line-number'); + lineNumber = $(event.target).closest('a').data('lineNumber'); current = this.hashToRange(this._hash); if (!(current[0] && event.shiftKey)) { // If there's no current selection, or there is but Shift wasn't held, diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index b99cb257ce3..659dc9eaa1f 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -10,7 +10,7 @@ window.jQuery = jQuery; window.$ = jQuery; // lib/utils -import { handleLocationHash } from './lib/utils/common_utils'; +import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils'; import { localTimeAgo } from './lib/utils/datetime_utility'; import { getLocationHash, visitUrl } from './lib/utils/url_utility'; @@ -61,7 +61,7 @@ gl.lazyLoader = new LazyLoader({ observerNode: '#content-body', }); -$(() => { +document.addEventListener('DOMContentLoaded', () => { const $body = $('body'); const $document = $(document); const $window = $(window); @@ -104,13 +104,7 @@ $(() => { return true; }); - // Click a .js-select-on-focus field, select the contents - // Prevent a mouseup event from deselecting the input - $('.js-select-on-focus').on('focusin', function selectOnFocusCallback() { - $(this).select().one('mouseup', (e) => { - e.preventDefault(); - }); - }); + addSelectOnFocusBehaviour('.js-select-on-focus'); $('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() { $(this).tooltip('destroy') @@ -220,7 +214,7 @@ $(() => { $document.on('click', '.js-confirm-danger', (e) => { const btn = $(e.target); const form = btn.closest('form'); - const text = btn.data('confirm-danger-message'); + const text = btn.data('confirmDangerMessage'); e.preventDefault(); // eslint-disable-next-line no-new diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js index 52315e969d1..330ebed5f73 100644 --- a/app/assets/javascripts/members.js +++ b/app/assets/javascripts/members.js @@ -19,7 +19,7 @@ export default class Members { isSelectable(selected, $el) { return !$el.hasClass('is-active'); }, - fieldName: $btn.data('field-name'), + fieldName: $btn.data('fieldName'), id(selected, $el) { return $el.data('id'); }, @@ -51,7 +51,7 @@ export default class Members { } // eslint-disable-next-line class-methods-use-this getMemberListItems($el) { - const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`); + const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('elId')}`); return { $memberListItem, diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js index 93f8f6ee926..2cb238529aa 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js @@ -2,7 +2,9 @@ /* global ace */ import Vue from 'vue'; -import Flash from '../../flash'; +import axios from '~/lib/utils/axios_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; ((global) => { global.mergeConflicts = global.mergeConflicts || {}; @@ -49,27 +51,26 @@ import Flash from '../../flash'; loadEditor() { this.loading = true; - $.get(this.file.content_path) - .done((file) => { + axios.get(this.file.content_path) + .then(({ data }) => { const content = this.$el.querySelector('pre'); - const fileContent = document.createTextNode(file.content); + const fileContent = document.createTextNode(data.content); content.textContent = fileContent.textContent; - this.originalContent = file.content; + this.originalContent = data.content; this.fileLoaded = true; this.editor = ace.edit(content); this.editor.$blockScrolling = Infinity; // Turn off annoying warning - this.editor.getSession().setMode(`ace/mode/${file.blob_ace_mode}`); + this.editor.getSession().setMode(`ace/mode/${data.blob_ace_mode}`); this.editor.on('change', () => { this.saveDiffResolution(); }); this.saveDiffResolution(); + this.loading = false; }) - .fail(() => { - new Flash('Failed to load the file, please try again.'); - }) - .always(() => { + .catch(() => { + flash(__('An error occurred while loading the file')); this.loading = false; }); }, diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index b4b3c15108d..66b258839ae 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -12,7 +12,7 @@ import './components/inline_conflict_lines'; import './components/parallel_conflict_lines'; import syntaxHighlight from '../syntax_highlight'; -$(() => { +export default function initMergeConflicts() { const INTERACTIVE_RESOLVE_MODE = 'interactive'; const conflictsEl = document.querySelector('#conflicts'); const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore; @@ -91,4 +91,4 @@ $(() => { } } }); -}); +} diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index bedd50de1bb..a64093afcf4 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,6 +1,4 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */ - -import 'vendor/jquery.waitforimages'; import { __ } from '~/locale'; import TaskList from './task_list'; import MergeRequestTabs from './merge_request_tabs'; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 3e97a8c758d..46789e324c2 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -241,6 +241,10 @@ export default class MergeRequestTabs { return newState; } + getCurrentAction() { + return this.currentAction; + } + loadCommits(source) { if (this.commitsLoaded) { return; @@ -361,7 +365,7 @@ export default class MergeRequestTabs { } diffViewType() { - return $('.inline-parallel-buttons a.active').data('view-type'); + return $('.inline-parallel-buttons a.active').data('viewType'); } isDiffAction(action) { diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 6581be606eb..2841ecb558b 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -24,19 +24,19 @@ export default class MilestoneSelect { $els.each((i, dropdown) => { let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault; const $dropdown = $(dropdown); - const projectId = $dropdown.data('project-id'); + const projectId = $dropdown.data('projectId'); const milestonesUrl = $dropdown.data('milestones'); const issueUpdateURL = $dropdown.data('issueUpdate'); - const showNo = $dropdown.data('show-no'); - const showAny = $dropdown.data('show-any'); + const showNo = $dropdown.data('showNo'); + const showAny = $dropdown.data('showAny'); const showMenuAbove = $dropdown.data('showMenuAbove'); - const showUpcoming = $dropdown.data('show-upcoming'); - const showStarted = $dropdown.data('show-started'); - const useId = $dropdown.data('use-id'); - const defaultLabel = $dropdown.data('default-label'); - const defaultNo = $dropdown.data('default-no'); - const issuableId = $dropdown.data('issuable-id'); - const abilityName = $dropdown.data('ability-name'); + const showUpcoming = $dropdown.data('showUpcoming'); + const showStarted = $dropdown.data('showStarted'); + const useId = $dropdown.data('useId'); + const defaultLabel = $dropdown.data('defaultLabel'); + const defaultNo = $dropdown.data('defaultNo'); + const issuableId = $dropdown.data('issuableId'); + const abilityName = $dropdown.data('abilityName'); const $selectBox = $dropdown.closest('.selectbox'); const $block = $selectBox.closest('.block'); const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon'); @@ -114,7 +114,7 @@ export default class MilestoneSelect { } }, defaultLabel: defaultLabel, - fieldName: $dropdown.data('field-name'), + fieldName: $dropdown.data('fieldName'), text: milestone => _.escape(milestone.title), id: (milestone) => { if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) { @@ -166,7 +166,7 @@ export default class MilestoneSelect { } if (boardsStore) { - boardsStore[$dropdown.data('field-name')] = selected.name; + boardsStore[$dropdown.data('fieldName')] = selected.name; e.preventDefault(); } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { return Issuable.filterResults($dropdown.closest('form')); diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 5afae93724b..031badc7026 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -27,6 +27,7 @@ hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics), documentationPath: metricsData.documentationPath, settingsPath: metricsData.settingsPath, + clustersPath: metricsData.clustersPath, tagsPath: metricsData.tagsPath, projectPath: metricsData.projectPath, metricsEndpoint: metricsData.additionalMetrics, @@ -132,6 +133,7 @@ :selected-state="state" :documentation-path="documentationPath" :settings-path="settingsPath" + :clusters-path="clustersPath" :empty-getting-started-svg-path="emptyGettingStartedSvgPath" :empty-loading-svg-path="emptyLoadingSvgPath" :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath" diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index 56cd60c583b..9517b8ccb67 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -10,6 +10,11 @@ required: false, default: '', }, + clustersPath: { + type: String, + required: false, + default: '', + }, selectedState: { type: String, required: true, @@ -35,7 +40,10 @@ title: 'Get started with performance monitoring', description: `Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.`, - buttonText: 'Configure Prometheus', + buttonText: 'Install Prometheus on clusters', + buttonPath: this.clustersPath, + secondaryButtonText: 'Configure existing Prometheus', + secondaryButtonPath: this.settingsPath, }, loading: { svgUrl: this.emptyLoadingSvgPath, @@ -43,6 +51,7 @@ description: `Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.`, buttonText: 'View documentation', + buttonPath: this.documentationPath, }, noData: { svgUrl: this.emptyUnableToConnectSvgPath, @@ -50,12 +59,14 @@ description: `You are connected to the Prometheus server, but there is currently no data to display.`, buttonText: 'Configure Prometheus', + buttonPath: this.settingsPath, }, unableToConnect: { svgUrl: this.emptyUnableToConnectSvgPath, title: 'Unable to connect to Prometheus server', description: 'Ensure connectivity is available from the GitLab server to the ', buttonText: 'View documentation', + buttonPath: this.documentationPath, }, }, }; @@ -65,13 +76,6 @@ return this.states[this.selectedState]; }, - buttonPath() { - if (this.selectedState === 'gettingStarted') { - return this.settingsPath; - } - return this.documentationPath; - }, - showButtonDescription() { if (this.selectedState === 'unableToConnect') return true; return false; @@ -99,11 +103,21 @@ </p> <div class="state-button"> <a + v-if="currentState.buttonPath" class="btn btn-success" - :href="buttonPath" + :href="currentState.buttonPath" > {{ currentState.buttonText }} </a> </div> + <div class="state-button"> + <a + v-if="currentState.secondaryButtonPath" + class="btn" + :href="currentState.secondaryButtonPath" + > + {{ currentState.secondaryButtonText }} + </a> + </div> </div> </template> diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js new file mode 100644 index 00000000000..f4cba998fa7 --- /dev/null +++ b/app/assets/javascripts/mr_notes/index.js @@ -0,0 +1,41 @@ +import Vue from 'vue'; +import notesApp from '../notes/components/notes_app.vue'; +import discussionCounter from '../notes/components/discussion_counter.vue'; +import store from '../notes/stores'; + +document.addEventListener('DOMContentLoaded', () => { + new Vue({ // eslint-disable-line + el: '#js-vue-mr-discussions', + components: { + notesApp, + }, + data() { + const notesDataset = document.getElementById('js-vue-mr-discussions').dataset; + return { + noteableData: JSON.parse(notesDataset.noteableData), + currentUserData: JSON.parse(notesDataset.currentUserData), + notesData: JSON.parse(notesDataset.notesData), + }; + }, + render(createElement) { + return createElement('notes-app', { + props: { + noteableData: this.noteableData, + notesData: this.notesData, + userData: this.currentUserData, + }, + }); + }, + }); + + new Vue({ // eslint-disable-line + el: '#js-vue-discussion-counter', + components: { + discussionCounter, + }, + store, + render(createElement) { + return createElement('discussion-counter'); + }, + }); +}); diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js deleted file mode 100644 index 129f1724cb8..00000000000 --- a/app/assets/javascripts/network/network_bundle.js +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */ - -import ShortcutsNetwork from '../shortcuts_network'; -import Network from './network'; - -$(function() { - if (!$(".network-graph").length) return; - - var network_graph; - network_graph = new Network({ - url: $(".network-graph").attr('data-url'), - commit_url: $(".network-graph").attr('data-commit-url'), - ref: $(".network-graph").attr('data-ref'), - commit_id: $(".network-graph").attr('data-commit-id') - }); - return new ShortcutsNetwork(network_graph.branch_graph); -}); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 8efb8ac5320..c640003d958 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -24,7 +24,7 @@ import GLForm from './gl_form'; import loadAwardsHandler from './awards_handler'; import Autosave from './autosave'; import TaskList from './task_list'; -import { isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils'; +import { isInViewport, getPagePath, scrollToElement, isMetaKey, hasVueMRDiscussionsCookie } from './lib/utils/common_utils'; import imageDiffHelper from './image_diff/helpers/index'; import { localTimeAgo } from './lib/utils/datetime_utility'; @@ -44,6 +44,10 @@ export default class Notes { } } + static getInstance() { + return this.instance; + } + constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = true) { this.updateTargetButtons = this.updateTargetButtons.bind(this); this.updateComment = this.updateComment.bind(this); @@ -102,67 +106,77 @@ export default class Notes { } addBinding() { + this.$wrapperEl = hasVueMRDiscussionsCookie() ? $(document).find('.diffs') : $(document); + // Edit note link - $(document).on('click', '.js-note-edit', this.showEditForm.bind(this)); - $(document).on('click', '.note-edit-cancel', this.cancelEdit); + this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this)); + this.$wrapperEl.on('click', '.note-edit-cancel', this.cancelEdit); // Reopen and close actions for Issue/MR combined with note form submit - $(document).on('click', '.js-comment-submit-button', this.postComment); - $(document).on('click', '.js-comment-save-button', this.updateComment); - $(document).on('keyup input', '.js-note-text', this.updateTargetButtons); + this.$wrapperEl.on('click', '.js-comment-submit-button', this.postComment); + this.$wrapperEl.on('click', '.js-comment-save-button', this.updateComment); + this.$wrapperEl.on('keyup input', '.js-note-text', this.updateTargetButtons); // resolve a discussion - $(document).on('click', '.js-comment-resolve-button', this.postComment); + this.$wrapperEl.on('click', '.js-comment-resolve-button', this.postComment); // remove a note (in general) - $(document).on('click', '.js-note-delete', this.removeNote); + this.$wrapperEl.on('click', '.js-note-delete', this.removeNote); // delete note attachment - $(document).on('click', '.js-note-attachment-delete', this.removeAttachment); + this.$wrapperEl.on('click', '.js-note-attachment-delete', this.removeAttachment); // reset main target form when clicking discard - $(document).on('click', '.js-note-discard', this.resetMainTargetForm); + this.$wrapperEl.on('click', '.js-note-discard', this.resetMainTargetForm); // update the file name when an attachment is selected - $(document).on('change', '.js-note-attachment-input', this.updateFormAttachment); + this.$wrapperEl.on('change', '.js-note-attachment-input', this.updateFormAttachment); // reply to diff/discussion notes - $(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote); + this.$wrapperEl.on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote); // add diff note - $(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote); + this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote); // add diff note for images - $(document).on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote); + this.$wrapperEl.on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote); // hide diff note form - $(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm); + this.$wrapperEl.on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm); // toggle commit list - $(document).on('click', '.system-note-commit-list-toggler', this.toggleCommitList); + this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList); // fetch notes when tab becomes visible - $(document).on('visibilitychange', this.visibilityChange); + this.$wrapperEl.on('visibilitychange', this.visibilityChange); // when issue status changes, we need to refresh data - $(document).on('issuable:change', this.refresh); + this.$wrapperEl.on('issuable:change', this.refresh); // ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs. - $(document).on('ajax:success', '.js-main-target-form', this.addNote); - $(document).on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote); - $(document).on('ajax:success', '.js-main-target-form', this.resetMainTargetForm); - $(document).on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton); + this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.addNote); + this.$wrapperEl.on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote); + this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.resetMainTargetForm); + this.$wrapperEl.on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton); // when a key is clicked on the notes - $(document).on('keydown', '.js-note-text', this.keydownNoteText); + this.$wrapperEl.on('keydown', '.js-note-text', this.keydownNoteText); // When the URL fragment/hash has changed, `#note_xxx` - return $(window).on('hashchange', this.onHashChange); + $(window).on('hashchange', this.onHashChange); + this.boundGetContent = this.getContent.bind(this); + document.addEventListener('refreshLegacyNotes', this.boundGetContent); + this.eventsBound = true; } cleanBinding() { - $(document).off('click', '.js-note-edit'); - $(document).off('click', '.note-edit-cancel'); - $(document).off('click', '.js-note-delete'); - $(document).off('click', '.js-note-attachment-delete'); - $(document).off('click', '.js-discussion-reply-button'); - $(document).off('click', '.js-add-diff-note-button'); - $(document).off('click', '.js-add-image-diff-note-button'); - $(document).off('visibilitychange'); - $(document).off('keyup input', '.js-note-text'); - $(document).off('click', '.js-note-target-reopen'); - $(document).off('click', '.js-note-target-close'); - $(document).off('click', '.js-note-discard'); - $(document).off('keydown', '.js-note-text'); - $(document).off('click', '.js-comment-resolve-button'); - $(document).off('click', '.system-note-commit-list-toggler'); - $(document).off('ajax:success', '.js-main-target-form'); - $(document).off('ajax:success', '.js-discussion-note-form'); - $(document).off('ajax:complete', '.js-main-target-form'); + if (!this.eventsBound) { + return; + } + + this.$wrapperEl.off('click', '.js-note-edit'); + this.$wrapperEl.off('click', '.note-edit-cancel'); + this.$wrapperEl.off('click', '.js-note-delete'); + this.$wrapperEl.off('click', '.js-note-attachment-delete'); + this.$wrapperEl.off('click', '.js-discussion-reply-button'); + this.$wrapperEl.off('click', '.js-add-diff-note-button'); + this.$wrapperEl.off('click', '.js-add-image-diff-note-button'); + this.$wrapperEl.off('visibilitychange'); + this.$wrapperEl.off('keyup input', '.js-note-text'); + this.$wrapperEl.off('click', '.js-note-target-reopen'); + this.$wrapperEl.off('click', '.js-note-target-close'); + this.$wrapperEl.off('click', '.js-note-discard'); + this.$wrapperEl.off('keydown', '.js-note-text'); + this.$wrapperEl.off('click', '.js-comment-resolve-button'); + this.$wrapperEl.off('click', '.system-note-commit-list-toggler'); + this.$wrapperEl.off('ajax:success', '.js-main-target-form'); + this.$wrapperEl.off('ajax:success', '.js-discussion-note-form'); + this.$wrapperEl.off('ajax:complete', '.js-main-target-form'); + document.removeEventListener('refreshLegacyNotes', this.boundGetContent); $(window).off('hashchange', this.onHashChange); } @@ -219,7 +233,7 @@ export default class Notes { } editNote = $textarea.closest('.note'); if (editNote.length) { - originalText = $textarea.closest('form').data('original-note'); + originalText = $textarea.closest('form').data('originalNote'); newText = $textarea.val(); if (originalText !== newText) { if (!confirm('Are you sure you want to cancel editing this comment?')) { @@ -252,8 +266,10 @@ export default class Notes { if (this.refreshing) { return; } + this.refreshing = true; - axios.get(this.notes_url, { + + axios.get(`${this.notes_url}?html=true`, { headers: { 'X-Last-Fetched-At': this.last_fetched_at, }, @@ -350,7 +366,7 @@ export default class Notes { } if (!noteEntity.valid) { - if (noteEntity.errors.commands_only) { + if (noteEntity.errors && noteEntity.errors.commands_only) { if (noteEntity.commands_changes && Object.keys(noteEntity.commands_changes).length > 0) { $notesList.find('.system-note.being-posted').remove(); @@ -363,6 +379,10 @@ export default class Notes { const $note = $notesList.find(`#note_${noteEntity.id}`); if (Notes.isNewNote(noteEntity, this.note_ids)) { + if (hasVueMRDiscussionsCookie()) { + return; + } + this.note_ids.push(noteEntity.id); if ($notesList.length) { @@ -399,6 +419,8 @@ export default class Notes { this.setupNewNote($updatedNote); } } + + Notes.refreshVueNotes(); } isParallelView() { @@ -406,12 +428,11 @@ export default class Notes { } /** - * Render note in discussion area. - * - * Note: for rendering inline notes use renderDiscussionNote + * Render note in discussion area. To render inline notes use renderDiscussionNote. */ renderDiscussionNote(noteEntity, $form) { var discussionContainer, form, row, lineType, diffAvatarContainer; + if (!Notes.isNewNote(noteEntity, this.note_ids)) { return; } @@ -452,7 +473,9 @@ export default class Notes { // Init discussion on 'Discussion' page if it is merge request page const page = $('body').attr('data-page'); if ((page && page.indexOf('projects:merge_request') !== -1) || !noteEntity.diff_discussion_html) { - Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list')); + if (!hasVueMRDiscussionsCookie()) { + Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list')); + } } } else { // append new note to all matching discussions @@ -609,9 +632,9 @@ export default class Notes { */ addDiscussionNote($form, note, isNewDiffComment) { if ($form.attr('data-resolve-all') != null) { - var projectPath = $form.data('project-path'); - var discussionId = $form.data('discussion-id'); - var mergeRequestId = $form.data('noteable-iid'); + var projectPath = $form.data('projectPath'); + var discussionId = $form.data('discussionId'); + var mergeRequestId = $form.data('noteableIid'); if (ResolveService != null) { ResolveService.toggleResolveForDiscussion(mergeRequestId, discussionId); @@ -634,7 +657,6 @@ export default class Notes { var $noteEntityEl, $note_li; // Convert returned HTML to a jQuery object so we can modify it further $noteEntityEl = $(noteEntity.html); - $noteEntityEl.addClass('fade-in-full'); this.revertNoteEditForm($targetNote); $noteEntityEl.renderGFM(); // Find the note's `li` element by ID and replace it with the updated HTML @@ -730,7 +752,7 @@ export default class Notes { var selector = this.getEditFormSelector($target); var $editForm = $(selector); - $editForm.insertBefore('.notes-form'); + $editForm.insertBefore('.diffs'); $editForm.find('.js-comment-save-button').enable(); $editForm.find('.js-finish-edit-warning').hide(); } @@ -746,12 +768,13 @@ export default class Notes { } removeNoteEditForm($note) { - var form = $note.find('.current-note-edit-form'); + var form = $note.find('.diffs .current-note-edit-form'); + $note.removeClass('is-editing'); form.removeClass('current-note-edit-form'); form.find('.js-finish-edit-warning').hide(); // Replace markdown textarea text with original note text. - return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note')); + return form.find('.js-note-text').val(form.find('form.edit-note').data('originalNote')); } /** @@ -776,7 +799,7 @@ export default class Notes { var $note, $notes; $note = $(el); $notes = $note.closest('.discussion-notes'); - const discussionId = $('.notes', $notes).data('discussion-id'); + const discussionId = $('.notes', $notes).data('discussionId'); if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (gl.diffNoteApps[noteElId]) { @@ -818,6 +841,7 @@ export default class Notes { }; })(this)); + Notes.refreshVueNotes(); Notes.checkMergeRequestStatus(); return this.updateNotesCount(-1); } @@ -897,7 +921,7 @@ export default class Notes { // DiffNote form.find('#note_position').val(dataHolder.attr('data-position')); - form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text')); + form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancelText')); form.find('.js-note-target-close').remove(); form.find('.js-note-new-discussion').remove(); this.setupNoteForm(form); @@ -1037,7 +1061,7 @@ export default class Notes { removeDiscussionNoteForm(form) { var glForm, row; row = form.closest('tr'); - glForm = form.data('gl-form'); + glForm = form.data('glForm'); glForm.destroy(); form.find('.js-note-text').data('autosave').reset(); // show the reply button (will only work for replies) @@ -1122,8 +1146,8 @@ export default class Notes { return discardbtn.show(); } } else { - reopentext = reopenbtn.data('original-text'); - closetext = closebtn.data('original-text'); + reopentext = reopenbtn.data('originalText'); + closetext = closebtn.data('originalText'); if (reopenbtn.text() !== reopentext) { reopenbtn.text(reopentext); } @@ -1150,14 +1174,14 @@ export default class Notes { var $originalContentEl = $note.find('.original-note-content'); var originalContent = $originalContentEl.text().trim(); - var postUrl = $originalContentEl.data('post-url'); - var targetId = $originalContentEl.data('target-id'); - var targetType = $originalContentEl.data('target-type'); + var postUrl = $originalContentEl.data('postUrl'); + var targetId = $originalContentEl.data('targetId'); + var targetType = $originalContentEl.data('targetType'); this.glForm = new GLForm($editForm.find('form'), this.enableGFM); $editForm.find('form') - .attr('action', postUrl) + .attr('action', `${postUrl}?html=true`) .attr('data-remote', 'true'); $editForm.find('.js-form-target-id').val(targetId); $editForm.find('.js-form-target-type').val(targetType); @@ -1280,6 +1304,10 @@ export default class Notes { return $updatedNote; } + static refreshVueNotes() { + document.dispatchEvent(new CustomEvent('refreshVueNotes')); + } + /** * Get data from Form attributes to use for saving/submitting comment. */ @@ -1481,7 +1509,7 @@ export default class Notes { /* eslint-disable promise/catch-or-return */ // Make request to submit comment on server - axios.post(formAction, formData) + axios.post(`${formAction}?html=true`, formData) .then((res) => { const note = res.data; @@ -1513,9 +1541,9 @@ export default class Notes { // If comment intends to resolve discussion, do the same. if (isDiscussionResolve) { $form - .attr('data-discussion-id', $submitBtn.data('discussion-id')) + .attr('data-discussion-id', $submitBtn.data('discussionId')) .attr('data-resolve-all', 'true') - .attr('data-project-path', $submitBtn.data('project-path')); + .attr('data-project-path', $submitBtn.data('projectPath')); } // Show final note element on UI @@ -1546,6 +1574,8 @@ export default class Notes { if ($notesContainer.length) { $notesContainer.append('<div class="flash-container" style="display: none;"></div>'); } + + Notes.refreshVueNotes(); } else if (isMainForm) { // Check if this was main thread comment // Show final note element on UI and perform form and action buttons cleanup this.addNote($form, note); @@ -1587,7 +1617,7 @@ export default class Notes { this.addNoteError($form); }); - return $closeBtn.text($closeBtn.data('original-text')); + return $closeBtn.text($closeBtn.data('originalText')); } /** @@ -1627,7 +1657,7 @@ export default class Notes { /* eslint-disable promise/catch-or-return */ // Make request to update comment on server - axios.post(formAction, formData) + axios.post(`${formAction}?html=true`, formData) .then(({ data }) => { // Submission successful! render final note element this.updateNote(data, $editingNote); @@ -1642,7 +1672,7 @@ export default class Notes { this.updateNoteError(); }); - return $closeBtn.text($closeBtn.data('original-text')); + return $closeBtn.text($closeBtn.data('originalText')); } } diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 3c8452ac808..b85c1a6ad72 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -2,16 +2,19 @@ import { mapActions, mapGetters } from 'vuex'; import _ from 'underscore'; import Autosize from 'autosize'; + import { __, sprintf } from '~/locale'; import Flash from '../../flash'; import Autosave from '../../autosave'; import TaskList from '../../task_list'; + import { capitalizeFirstCharacter, convertToCamelCase } from '../../lib/utils/text_utility'; import * as constants from '../constants'; import eventHub from '../event_hub'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; - import noteSignedOutWidget from './note_signed_out_widget.vue'; - import discussionLockedWidget from './discussion_locked_widget.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + import loadingButton from '../../vue_shared/components/loading_button.vue'; + import noteSignedOutWidget from './note_signed_out_widget.vue'; + import discussionLockedWidget from './discussion_locked_widget.vue'; import issuableStateMixin from '../mixins/issuable_state'; export default { @@ -22,17 +25,21 @@ discussionLockedWidget, markdownField, userAvatarLink, + loadingButton, }, mixins: [ issuableStateMixin, ], + props: { + noteableType: { + type: String, + required: true, + }, + }, data() { return { note: '', noteType: constants.COMMENT, - // Can't use mapGetters, - // this needs to be in the data object because it belongs to the state - issueState: this.$store.getters.getNoteableData.state, isSubmitting: false, isSubmitButtonDisabled: true, }; @@ -43,36 +50,51 @@ 'getUserData', 'getNoteableData', 'getNotesData', + 'openState', ]), + noteableDisplayName() { + return this.noteableType.replace(/_/g, ' '); + }, isLoggedIn() { return this.getUserData.id; }, commentButtonTitle() { return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion'; }, - isIssueOpen() { - return this.issueState === constants.OPENED || this.issueState === constants.REOPENED; + isOpen() { + return this.openState === constants.OPENED || this.openState === constants.REOPENED; }, canCreateNote() { return this.getNoteableData.current_user.can_create_note; }, issueActionButtonTitle() { - if (this.note.length) { - const actionText = this.isIssueOpen ? 'close' : 'reopen'; + const openOrClose = this.isOpen ? 'close' : 'reopen'; - return this.noteType === constants.COMMENT ? - `Comment & ${actionText} issue` : - `Start discussion & ${actionText} issue`; + if (this.note.length) { + return sprintf( + __('%{actionText} & %{openOrClose} %{noteable}'), + { + actionText: this.commentButtonTitle, + openOrClose, + noteable: this.noteableDisplayName, + }, + ); } - return this.isIssueOpen ? 'Close issue' : 'Reopen issue'; + return sprintf( + __('%{openOrClose} %{noteable}'), + { + openOrClose: capitalizeFirstCharacter(openOrClose), + noteable: this.noteableDisplayName, + }, + ); }, actionButtonClassNames() { return { - 'btn-reopen': !this.isIssueOpen, - 'btn-close': this.isIssueOpen, - 'js-note-target-close': this.isIssueOpen, - 'js-note-target-reopen': !this.isIssueOpen, + 'btn-reopen': !this.isOpen, + 'btn-close': this.isOpen, + 'js-note-target-close': this.isOpen, + 'js-note-target-reopen': !this.isOpen, }; }, markdownDocsPath() { @@ -105,7 +127,7 @@ mounted() { // jQuery is needed here because it is a custom event being dispatched with jQuery. $(document).on('issuable:change', (e, isClosed) => { - this.issueState = isClosed ? constants.CLOSED : constants.REOPENED; + this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED); }); this.initAutoSave(); @@ -117,6 +139,9 @@ 'stopPolling', 'restartPolling', 'removePlaceholderNotes', + 'closeIssue', + 'reopenIssue', + 'toggleIssueLocalState', ]), setIsSubmitButtonDisabled(note, isSubmitting) { if (!_.isEmpty(note) && !isSubmitting) { @@ -126,13 +151,15 @@ } }, handleSave(withIssueAction) { + this.isSubmitting = true; + if (this.note.length) { const noteData = { endpoint: this.endpoint, flashContainer: this.$el, data: { note: { - noteable_type: constants.NOTEABLE_TYPE, + noteable_type: this.noteableType, noteable_id: this.getNoteableData.id, note: this.note, }, @@ -142,7 +169,6 @@ if (this.noteType === constants.DISCUSSION) { noteData.data.note.type = constants.DISCUSSION_NOTE; } - this.isSubmitting = true; this.note = ''; // Empty textarea while being requested. Repopulate in catch this.resizeTextarea(); this.stopPolling(); @@ -184,13 +210,35 @@ Please check your network connection and try again.`; this.toggleIssueState(); } }, + enableButton() { + this.isSubmitting = false; + }, toggleIssueState() { - this.issueState = this.isIssueOpen ? constants.CLOSED : constants.REOPENED; - - // This is out of scope for the Notes Vue component. - // It was the shortest path to update the issue state and relevant places. - const btnClass = this.isIssueOpen ? 'btn-reopen' : 'btn-close'; - $(`.js-btn-issue-action.${btnClass}:visible`).trigger('click'); + if (this.isOpen) { + this.closeIssue() + .then(() => this.enableButton()) + .catch(() => { + this.enableButton(); + Flash( + sprintf( + __('Something went wrong while closing the %{issuable}. Please try again later'), + { issuable: this.noteableDisplayName }, + ), + ); + }); + } else { + this.reopenIssue() + .then(() => this.enableButton()) + .catch(() => { + this.enableButton(); + Flash( + sprintf( + __('Something went wrong while reopening the %{issuable}. Please try again later'), + { issuable: this.noteableDisplayName }, + ), + ); + }); + } }, discard(shouldClear = true) { // `blur` is needed to clear slash commands autocomplete cache if event fired. @@ -204,7 +252,6 @@ Please check your network connection and try again.`; this.$refs.markdownField.previewMarkdown = false; } - // reset autostave this.autosave.reset(); }, setNoteType(type) { @@ -223,10 +270,11 @@ Please check your network connection and try again.`; }, initAutoSave() { if (this.isLoggedIn) { + const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType)); + this.autosave = new Autosave( $(this.$refs.textarea), - ['Note', 'Issue', this.getNoteableData.id], - 'issue', + ['Note', noteableType, this.getNoteableData.id], ); } }, @@ -314,7 +362,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" :disabled="isSubmitButtonDisabled" class="btn btn-create comment-btn js-comment-button js-comment-submit-button" type="submit"> - {{ commentButtonTitle }} + {{ __(commentButtonTitle) }} </button> <button :disabled="isSubmitButtonDisabled" @@ -342,7 +390,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" <div class="description"> <strong>Comment</strong> <p> - Add a general comment to this issue. + Add a general comment to this {{ noteableDisplayName }}. </p> </div> </button> @@ -367,15 +415,19 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" </li> </ul> </div> - <button - type="button" - @click="handleSave(true)" + + <loading-button v-if="canUpdateIssue" - :class="actionButtonClassNames" + :loading="isSubmitting" + @click="handleSave(true)" + :container-class="[ + actionButtonClassNames, + 'btn btn-comment btn-comment-and-close js-action-button' + ]" :disabled="isSubmitting" - class="btn btn-comment btn-comment-and-close js-action-button"> - {{ issueActionButtonTitle }} - </button> + :label="issueActionButtonTitle" + /> + <button type="button" v-if="note.length" diff --git a/app/assets/javascripts/notes/components/diff_file_header.vue b/app/assets/javascripts/notes/components/diff_file_header.vue new file mode 100644 index 00000000000..fe5baa3537f --- /dev/null +++ b/app/assets/javascripts/notes/components/diff_file_header.vue @@ -0,0 +1,92 @@ +<script> + import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + import Icon from '~/vue_shared/components/icon.vue'; + + export default { + components: { + ClipboardButton, + Icon, + }, + props: { + diffFile: { + type: Object, + required: true, + }, + }, + computed: { + titleTag() { + return this.diffFile.discussionPath ? 'a' : 'span'; + }, + }, + }; +</script> + +<template> + <div class="file-header-content"> + <div + v-if="diffFile.submodule" + > + <span> + <icon name="archive" /> + <strong + v-html="diffFile.submoduleLink" + class="file-title-name" + ></strong> + <clipboard-button + title="Copy file path to clipboard" + :text="diffFile.submoduleLink" + /> + </span> + </div> + <template v-else> + <component + ref="titleWrapper" + :is="titleTag" + :href="diffFile.discussionPath" + > + <span v-html="diffFile.blobIcon"></span> + <span v-if="diffFile.renamedFile"> + <strong + class="file-title-name has-tooltip" + :title="diffFile.oldPath" + data-container="body" + > + {{ diffFile.oldPath }} + </strong> + → + <strong + class="file-title-name has-tooltip" + :title="diffFile.newPath" + data-container="body" + > + {{ diffFile.newPath }} + </strong> + </span> + + <strong + v-else + class="file-title-name has-tooltip" + :title="diffFile.oldPath" + data-container="body" + > + {{ diffFile.filePath }} + <span v-if="diffFile.deletedFile"> + deleted + </span> + </strong> + </component> + + <clipboard-button + title="Copy file path to clipboard" + :text="diffFile.filePath" + /> + + <small + v-if="diffFile.modeChanged" + ref="fileMode" + > + {{ diffFile.aMode }} → {{ diffFile.bMode }} + </small> + </template> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue new file mode 100644 index 00000000000..75a32709ad5 --- /dev/null +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -0,0 +1,96 @@ +<script> + import syntaxHighlight from '~/syntax_highlight'; + import imageDiffHelper from '~/image_diff/helpers/index'; + import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; + import DiffFileHeader from './diff_file_header.vue'; + + export default { + components: { + DiffFileHeader, + }, + props: { + discussion: { + type: Object, + required: true, + }, + }, + computed: { + isImageDiff() { + return !this.diffFile.text; + }, + diffFileClass() { + const { text } = this.diffFile; + return text ? 'text-file' : 'js-image-file'; + }, + diffRows() { + return $(this.discussion.truncatedDiffLines); + }, + diffFile() { + return convertObjectPropsToCamelCase(this.discussion.diffFile); + }, + imageDiffHtml() { + return this.discussion.imageDiffHtml; + }, + }, + mounted() { + if (this.isImageDiff) { + const canCreateNote = false; + const renderCommentBadge = true; + imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge); + } else { + const fileHolder = $(this.$refs.fileHolder); + this.$nextTick(() => { + syntaxHighlight(fileHolder); + }); + } + }, + methods: { + rowTag(html) { + return html.outerHTML ? 'tr' : 'template'; + }, + }, + }; +</script> + +<template> + <div + ref="fileHolder" + class="diff-file file-holder" + :class="diffFileClass" + > + <div class="js-file-title file-title file-title-flex-parent"> + <diff-file-header + :diff-file="diffFile" + /> + </div> + <div + v-if="diffFile.text" + class="diff-content code js-syntax-highlight" + > + <table> + <component + :is="rowTag(html)" + :class="html.className" + v-for="(html, index) in diffRows" + v-html="html.outerHTML" + :key="index" + /> + <tr class="notes_holder"> + <td + class="notes_line" + colspan="2" + ></td> + <td class="notes_content"> + <slot></slot> + </td> + </tr> + </table> + </div> + <div + v-else + > + <div v-html="imageDiffHtml"></div> + <slot></slot> + </div> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue new file mode 100644 index 00000000000..0158f58b569 --- /dev/null +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -0,0 +1,119 @@ +<script> + import { mapGetters } from 'vuex'; + import resolveSvg from 'icons/_icon_resolve_discussion.svg'; + import resolvedSvg from 'icons/_icon_status_success_solid.svg'; + import mrIssueSvg from 'icons/_icon_mr_issue.svg'; + import nextDiscussionSvg from 'icons/_next_discussion.svg'; + import { pluralize } from '../../lib/utils/text_utility'; + import { scrollToElement } from '../../lib/utils/common_utils'; + import tooltip from '../../vue_shared/directives/tooltip'; + + export default { + directives: { + tooltip, + }, + computed: { + ...mapGetters([ + 'getUserData', + 'getNoteableData', + 'discussionCount', + 'unresolvedDiscussions', + 'resolvedDiscussionCount', + ]), + isLoggedIn() { + return this.getUserData.id; + }, + hasNextButton() { + return this.isLoggedIn && !this.allResolved; + }, + countText() { + return pluralize('discussion', this.discussionCount); + }, + allResolved() { + return this.resolvedDiscussionCount === this.discussionCount; + }, + resolveAllDiscussionsIssuePath() { + return this.getNoteableData.create_issue_to_resolve_discussions_path; + }, + firstUnresolvedDiscussionId() { + const item = this.unresolvedDiscussions[0] || {}; + + return item.id; + }, + }, + created() { + this.resolveSvg = resolveSvg; + this.resolvedSvg = resolvedSvg; + this.mrIssueSvg = mrIssueSvg; + this.nextDiscussionSvg = nextDiscussionSvg; + }, + methods: { + jumpToFirstDiscussion() { + const el = document.querySelector(`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`); + const activeTab = window.mrTabs.currentAction; + + if (activeTab === 'commits' || activeTab === 'pipelines') { + window.mrTabs.activateTab('show'); + } + + if (el) { + scrollToElement(el); + } + }, + }, + }; +</script> + +<template> + <div class="line-resolve-all-container prepend-top-10"> + <div> + <div + v-if="discussionCount > 0" + :class="{ 'has-next-btn': hasNextButton }" + class="line-resolve-all"> + <span + :class="{ 'is-active': allResolved }" + class="line-resolve-btn is-disabled" + type="button"> + <span + v-if="allResolved" + v-html="resolvedSvg" + ></span> + <span + v-else + v-html="resolveSvg" + ></span> + </span> + <span class=".line-resolve-text"> + {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved + </span> + </div> + <div + v-if="resolveAllDiscussionsIssuePath && !allResolved" + class="btn-group" + role="group"> + <a + :href="resolveAllDiscussionsIssuePath" + v-tooltip + title="Resolve all discussions in new issue" + data-container="body" + class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"> + <span v-html="mrIssueSvg"></span> + </a> + </div> + <div + v-if="isLoggedIn && !allResolved" + class="btn-group" + role="group"> + <button + @click="jumpToFirstDiscussion" + v-tooltip + title="Jump to first unresolved discussion" + data-container="body" + class="btn btn-default discussion-next-btn"> + <span v-html="nextDiscussionSvg"></span> + </button> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 46ffb60aa60..c26aa6fa15d 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -4,6 +4,8 @@ import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg'; import editSvg from 'icons/_icon_pencil.svg'; + import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg'; + import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg'; import ellipsisSvg from 'icons/_ellipsis_v.svg'; import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; @@ -42,6 +44,26 @@ type: Boolean, required: true, }, + resolvable: { + type: Boolean, + required: false, + default: false, + }, + isResolved: { + type: Boolean, + required: false, + default: false, + }, + isResolving: { + type: Boolean, + required: false, + default: false, + }, + resolvedBy: { + type: Object, + required: false, + default: () => ({}), + }, canReportAsAbuse: { type: Boolean, required: true, @@ -63,6 +85,15 @@ currentUserId() { return this.getUserDataByProp('id'); }, + resolveButtonTitle() { + let title = 'Mark as resolved'; + + if (this.resolvedBy) { + title = `Resolved by ${this.resolvedBy.name}`; + } + + return title; + }, }, created() { this.emojiSmiling = emojiSmiling; @@ -70,6 +101,8 @@ this.emojiSmiley = emojiSmiley; this.editSvg = editSvg; this.ellipsisSvg = ellipsisSvg; + this.resolveDiscussionSvg = resolveDiscussionSvg; + this.resolvedDiscussionSvg = resolvedDiscussionSvg; }, methods: { onEdit() { @@ -78,6 +111,9 @@ onDelete() { this.$emit('handleDelete'); }, + onResolve() { + this.$emit('handleResolve'); + }, }, }; </script> @@ -90,6 +126,31 @@ {{ accessLevel }} </span> <div + v-if="resolvable" + class="note-actions-item"> + <button + v-tooltip + @click="onResolve" + :class="{ 'is-disabled': !resolvable, 'is-active': isResolved }" + :title="resolveButtonTitle" + :aria-label="resolveButtonTitle" + type="button" + class="line-resolve-btn note-action-button"> + <template v-if="!isResolving"> + <div + v-if="isResolved" + v-html="resolvedDiscussionSvg"></div> + <div + v-else + v-html="resolveDiscussionSvg"></div> + </template> + <loading-icon + v-else + :inline="true" + /> + </button> + </div> + <div v-if="canAddAwardEmoji" class="note-actions-item"> <a diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index 2d7cd30115d..ca12df9db64 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -41,7 +41,7 @@ this.initTaskList(); if (this.isEditing) { - this.initAutoSave(); + this.initAutoSave(this.note.noteable_type); } }, updated() { @@ -50,7 +50,7 @@ if (this.isEditing) { if (!this.autosave) { - this.initAutoSave(); + this.initAutoSave(this.note.noteable_type); } else { this.setAutoSave(); } diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index d382a9bb642..1a13fdbeb7c 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -1,9 +1,10 @@ <script> - import { mapGetters } from 'vuex'; + import { mapGetters, mapActions } from 'vuex'; import eventHub from '../event_hub'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue'; import issuableStateMixin from '../mixins/issuable_state'; + import resolvable from '../mixins/resolvable'; export default { name: 'IssueNoteForm', @@ -13,6 +14,7 @@ }, mixins: [ issuableStateMixin, + resolvable, ], props: { noteBody: { @@ -30,7 +32,7 @@ required: false, default: 'Save comment', }, - discussion: { + note: { type: Object, required: false, default: () => ({}), @@ -42,9 +44,11 @@ }, data() { return { - note: this.noteBody, + updatedNoteBody: this.noteBody, conflictWhileEditing: false, isSubmitting: false, + isResolving: false, + resolveAsThread: true, }; }, computed: { @@ -71,13 +75,13 @@ return this.getUserDataByProp('id'); }, isDisabled() { - return !this.note.length || this.isSubmitting; + return !this.updatedNoteBody.length || this.isSubmitting; }, }, watch: { noteBody() { - if (this.note === this.noteBody) { - this.note = this.noteBody; + if (this.updatedNoteBody === this.noteBody) { + this.updatedNoteBody = this.noteBody; } else { this.conflictWhileEditing = true; } @@ -87,16 +91,24 @@ this.$refs.textarea.focus(); }, methods: { - handleUpdate() { + ...mapActions([ + 'toggleResolveNote', + ]), + handleUpdate(shouldResolve) { + const beforeSubmitDiscussionState = this.discussionResolved; this.isSubmitting = true; - this.$emit('handleFormUpdate', this.note, this.$refs.editNoteForm, () => { + this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => { this.isSubmitting = false; + + if (shouldResolve) { + this.resolveHandler(beforeSubmitDiscussionState); + } }); }, editMyLastNote() { - if (this.note === '') { - const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion); + if (this.updatedNoteBody === '') { + const lastNoteInDiscussion = this.getDiscussionLastNote(this.updatedNoteBody); if (lastNoteInDiscussion) { eventHub.$emit('enterEditMode', { @@ -107,7 +119,7 @@ }, cancelHandler(shouldConfirm = false) { // Sends information about confirm message and if the textarea has changed - this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note); + this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.updatedNoteBody); }, }, }; @@ -150,7 +162,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" :data-supports-quick-actions="!isEditing" aria-label="Description" - v-model="note" + v-model="updatedNoteBody" ref="textarea" slot="textarea" placeholder="Write a comment or drag your files here..." @@ -169,6 +181,13 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" {{ saveButtonTitle }} </button> <button + v-if="note.resolvable" + @click.prevent="handleUpdate(true)" + class="btn btn-nr btn-default append-right-10 js-comment-resolve-button" + > + {{ resolveButtonTitle }} + </button> + <button @click="cancelHandler()" class="btn btn-cancel note-edit-cancel" type="button"> diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index 5b255d4a710..4743d95b951 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -34,15 +34,15 @@ required: false, default: false, }, - }, - data() { - return { - isExpanded: true, - }; + expanded: { + type: Boolean, + required: false, + default: true, + }, }, computed: { toggleChevronClass() { - return this.isExpanded ? 'fa-chevron-up' : 'fa-chevron-down'; + return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down'; }, noteTimestampLink() { return `#note_${this.noteId}`; @@ -53,7 +53,6 @@ 'setTargetNoteHash', ]), handleToggle() { - this.isExpanded = !this.isExpanded; this.$emit('toggleHandler'); }, updateTargetNoteHash() { diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 98a06c5fc71..76bb53eaf2f 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -1,5 +1,7 @@ <script> import { mapActions, mapGetters } from 'vuex'; + import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg'; + import nextDiscussionsSvg from 'icons/_next_discussion.svg'; import Flash from '../../flash'; import { SYSTEM_NOTE } from '../constants'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -8,13 +10,19 @@ import noteSignedOutWidget from './note_signed_out_widget.vue'; import noteEditedText from './note_edited_text.vue'; import noteForm from './note_form.vue'; + import diffWithNote from './diff_with_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import autosave from '../mixins/autosave'; + import noteable from '../mixins/noteable'; + import resolvable from '../mixins/resolvable'; + import tooltip from '../../vue_shared/directives/tooltip'; + import { scrollToElement } from '../../lib/utils/common_utils'; export default { components: { noteableNote, + diffWithNote, userAvatarLink, noteHeader, noteSignedOutWidget, @@ -23,8 +31,13 @@ placeholderNote, placeholderSystemNote, }, + directives: { + tooltip, + }, mixins: [ autosave, + noteable, + resolvable, ], props: { note: { @@ -35,14 +48,25 @@ data() { return { isReplying: false, + isResolving: false, + resolveAsThread: true, }; }, computed: { ...mapGetters([ 'getNoteableData', + 'discussionCount', + 'resolvedDiscussionCount', + 'unresolvedDiscussions', ]), discussion() { - return this.note.notes[0]; + return { + ...this.note.notes[0], + truncatedDiffLines: this.note.truncated_diff_lines, + diffFile: this.note.diff_file, + diffDiscussion: this.note.diff_discussion, + imageDiffHtml: this.note.image_diff_html, + }; }, author() { return this.discussion.author; @@ -71,26 +95,40 @@ return null; }, + hasUnresolvedDiscussion() { + return this.unresolvedDiscussions.length > 0; + }, + wrapperComponent() { + return (this.discussion.diffDiscussion && this.discussion.diffFile) ? diffWithNote : 'div'; + }, + wrapperClass() { + return this.isDiffDiscussion ? '' : 'panel panel-default'; + }, }, mounted() { if (this.isReplying) { - this.initAutoSave(); + this.initAutoSave(this.discussion.noteable_type); } }, updated() { if (this.isReplying) { if (!this.autosave) { - this.initAutoSave(); + this.initAutoSave(this.discussion.noteable_type); } else { this.setAutoSave(); } } }, + created() { + this.resolveDiscussionsSvg = resolveDiscussionsSvg; + this.nextDiscussionsSvg = nextDiscussionsSvg; + }, methods: { ...mapActions([ 'saveNote', 'toggleDiscussion', 'removePlaceholderNotes', + 'toggleResolveNote', ]), componentName(note) { if (note.isPlaceholderNote) { @@ -103,7 +141,7 @@ return noteableNote; }, componentData(note) { - return note.isPlaceholderNote ? note.notes[0] : note; + return note.isPlaceholderNote ? this.note.notes[0] : note; }, toggleDiscussionHandler() { this.toggleDiscussion({ discussionId: this.note.id }); @@ -128,7 +166,7 @@ flashContainer: this.$el, data: { in_reply_to_discussion_id: this.note.reply_id, - target_type: 'issue', + target_type: this.noteableType, target_id: this.discussion.noteable_id, note: { note: noteText }, }, @@ -152,12 +190,27 @@ Please check your network connection and try again.`; }); }); }, + jumpToDiscussion() { + const unresolvedIds = this.unresolvedDiscussions.map(d => d.id); + const index = unresolvedIds.indexOf(this.note.id); + + if (index >= 0 && index !== unresolvedIds.length) { + const nextId = unresolvedIds[index + 1]; + const el = document.querySelector(`[data-discussion-id="${nextId}"]`); + + if (el) { + scrollToElement(el); + } + } + }, }, }; </script> <template> - <li class="note note-discussion timeline-entry"> + <li + :data-discussion-id="note.id" + class="note note-discussion timeline-entry"> <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link @@ -175,6 +228,7 @@ Please check your network connection and try again.`; :created-at="discussion.created_at" :note-id="discussion.id" :include-toggle="true" + :expanded="note.expanded" @toggleHandler="toggleDiscussionHandler" action-text="started a discussion" class="discussion" @@ -187,43 +241,103 @@ Please check your network connection and try again.`; class-name="discussion-headline-light js-discussion-headline" /> </div> - </div> - <div - v-if="note.expanded" - class="discussion-body"> - <div class="panel panel-default"> - <div class="discussion-notes"> - <ul class="notes"> - <component - v-for="note in note.notes" - :is="componentName(note)" - :note="componentData(note)" - :key="note.id" - /> - </ul> - <div - :class="{ 'is-replying': isReplying }" - class="discussion-reply-holder"> - <button - v-if="canReply && !isReplying" - @click="showReplyForm" - type="button" - class="js-vue-discussion-reply btn btn-text-field" - title="Add a reply"> - Reply... - </button> - <note-form - v-if="isReplying" - save-button-title="Comment" - :discussion="note" - :is-editing="false" - @handleFormUpdate="saveReply" - @cancelFormEdition="cancelReplyForm" - ref="noteForm" - /> - <note-signed-out-widget v-if="!canReply" /> + <div + v-if="note.expanded" + class="discussion-body"> + <component + :is="wrapperComponent" + :discussion="discussion" + :class="wrapperClass" + > + <div class="discussion-notes"> + <ul class="notes"> + <component + v-for="note in note.notes" + :is="componentName(note)" + :note="componentData(note)" + :key="note.id" + /> + </ul> + <div + :class="{ 'is-replying': isReplying }" + class="discussion-reply-holder"> + <template v-if="!isReplying && canReply"> + <div + class="btn-group-justified discussion-with-resolve-btn" + role="group"> + <div + class="btn-group" + role="group"> + <button + @click="showReplyForm" + type="button" + class="js-vue-discussion-reply btn btn-text-field" + title="Add a reply">Reply...</button> + </div> + <div + v-if="note.resolvable" + class="btn-group" + role="group"> + <button + @click="resolveHandler()" + type="button" + class="btn btn-default" + > + <i + v-if="isResolving" + aria-hidden="true" + class="fa fa-spinner fa-spin" + ></i> + {{ resolveButtonTitle }} + </button> + </div> + <div + class="btn-group discussion-actions" + role="group"> + <div + v-if="note.resolvable && !discussionResolved" + class="btn-group" + role="group"> + <a + :href="note.resolve_with_issue_path" + v-tooltip + class="new-issue-for-discussion btn + btn-default discussion-create-issue-btn" + title="Resolve this discussion in a new issue" + data-container="body" + > + <span v-html="resolveDiscussionsSvg"></span> + </a> + </div> + <div + v-if="hasUnresolvedDiscussion" + class="btn-group" + role="group"> + <button + @click="jumpToDiscussion" + v-tooltip + class="btn btn-default discussion-next-btn" + title="Jump to next unresolved discussion" + data-container="body" + > + <span v-html="nextDiscussionsSvg"></span> + </button> + </div> + </div> + </div> + </template> + <note-form + v-if="isReplying" + save-button-title="Comment" + :note="note" + :is-editing="false" + @handleFormUpdate="saveReply" + @cancelFormEdition="cancelReplyForm" + ref="noteForm" /> + <note-signed-out-widget v-if="!canReply" /> + </div> </div> - </div> + </component> </div> </div> </div> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 30e7ccc8229..4d17bd5acc2 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -7,6 +7,8 @@ import noteActions from './note_actions.vue'; import noteBody from './note_body.vue'; import eventHub from '../event_hub'; + import noteable from '../mixins/noteable'; + import resolvable from '../mixins/resolvable'; export default { components: { @@ -15,6 +17,10 @@ noteActions, noteBody, }, + mixins: [ + noteable, + resolvable, + ], props: { note: { type: Object, @@ -26,6 +32,7 @@ isEditing: false, isDeleting: false, isRequesting: false, + isResolving: false, }; }, computed: { @@ -65,6 +72,7 @@ ...mapActions([ 'deleteNote', 'updateNote', + 'toggleResolveNote', 'scrollToNoteIfNeeded', ]), editHandler() { @@ -89,7 +97,7 @@ const data = { endpoint: this.note.path, note: { - target_type: 'issue', + target_type: this.noteableType, target_id: this.note.noteable_id, note: { note: noteText }, }, @@ -102,6 +110,7 @@ .then(() => { this.isEditing = false; this.isRequesting = false; + this.oldContent = null; $(this.$refs.noteBody.$el).renderGFM(); this.$refs.noteBody.resetAutoSave(); callback(); @@ -133,7 +142,7 @@ // we need to do this to prevent noteForm inconsistent content warning // this is something we intentionally do so we need to recover the content this.note.note = noteText; - this.$refs.noteBody.$refs.noteForm.note = noteText; + this.$refs.noteBody.$refs.noteForm.note.note = noteText; }, }, }; @@ -170,8 +179,13 @@ :can-delete="note.current_user.can_edit" :can-report-as-abuse="canReportAsAbuse" :report-abuse-path="note.report_abuse_path" + :resolvable="note.resolvable" + :is-resolved="note.resolved" + :is-resolving="isResolving" + :resolved-by="note.resolved_by" @handleEdit="editHandler" @handleDelete="deleteHandler" + @handleResolve="resolveHandler" /> </div> <note-body diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 92db4830704..74afed5560b 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -11,6 +11,7 @@ import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue'; export default { name: 'NotesApp', @@ -48,7 +49,24 @@ ...mapGetters([ 'notes', 'getNotesDataByProp', + 'discussionCount', ]), + noteableType() { + // FIXME -- @fatihacet Get this from JSON data. + const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants; + + return this.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE : ISSUE_NOTEABLE_TYPE; + }, + allNotes() { + if (this.isLoading) { + const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0; + + return new Array(totalNotes).fill({ + isSkeletonNote: true, + }); + } + return this.notes; + }, }, created() { this.setNotesData(this.notesData); @@ -67,6 +85,10 @@ this.actionToggleAward({ awardName, noteId }); }); } + document.addEventListener('refreshVueNotes', this.fetchNotes); + }, + beforeDestroy() { + document.removeEventListener('refreshVueNotes', this.fetchNotes); }, methods: { ...mapActions({ @@ -81,6 +103,9 @@ setTargetNoteHash: 'setTargetNoteHash', }), getComponentName(note) { + if (note.isSkeletonNote) { + return skeletonLoadingContainer; + } if (note.isPlaceholderNote) { if (note.placeholderType === constants.SYSTEM_NOTE) { return placeholderSystemNote; @@ -109,9 +134,14 @@ }); }, initPolling() { + if (this.isPollingInitialized) { + return; + } + this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt')); this.poll(); + this.isPollingInitialized = true; }, checkLocationHash() { const hash = getLocationHash(); @@ -128,25 +158,20 @@ <template> <div id="notes"> - <div - v-if="isLoading" - class="js-loading loading"> - <loading-icon /> - </div> - <ul - v-if="!isLoading" id="notes-list" class="notes main-notes-list timeline"> <component - v-for="note in notes" + v-for="note in allNotes" :is="getComponentName(note)" :note="getComponentData(note)" :key="note.id" /> </ul> - <comment-form /> + <comment-form + :noteable-type="noteableType" + /> </div> </template> diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index a6961063c01..f4f407ffd8a 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -1,4 +1,5 @@ export const DISCUSSION_NOTE = 'DiscussionNote'; +export const DIFF_NOTE = 'DiffNote'; export const DISCUSSION = 'discussion'; export const NOTE = 'note'; export const SYSTEM_NOTE = 'systemNote'; @@ -8,4 +9,7 @@ export const REOPENED = 'reopened'; export const CLOSED = 'closed'; export const EMOJI_THUMBSUP = 'thumbsup'; export const EMOJI_THUMBSDOWN = 'thumbsdown'; -export const NOTEABLE_TYPE = 'Issue'; +export const ISSUE_NOTEABLE_TYPE = 'issue'; +export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request'; +export const UNRESOLVE_NOTE_METHOD_NAME = 'delete'; +export const RESOLVE_NOTE_METHOD_NAME = 'post'; diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index d250dd8d25b..545bf2c99a7 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -20,15 +20,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ return { noteableData: JSON.parse(notesDataset.noteableData), currentUserData, - notesData: { - lastFetchedAt: notesDataset.lastFetchedAt, - discussionsPath: notesDataset.discussionsPath, - newSessionPath: notesDataset.newSessionPath, - registerPath: notesDataset.registerPath, - notesPath: notesDataset.notesPath, - markdownDocsPath: notesDataset.markdownDocsPath, - quickActionsDocsPath: notesDataset.quickActionsDocsPath, - }, + notesData: JSON.parse(notesDataset.notesData), }; }, render(createElement) { diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js index a008171beda..a3d897f2f12 100644 --- a/app/assets/javascripts/notes/mixins/autosave.js +++ b/app/assets/javascripts/notes/mixins/autosave.js @@ -1,9 +1,10 @@ import Autosave from '../../autosave'; +import { capitalizeFirstCharacter } from '../../lib/utils/text_utility'; export default { methods: { - initAutoSave() { - this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', 'Issue', this.note.id], 'issue'); + initAutoSave(noteableType) { + this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', capitalizeFirstCharacter(noteableType), this.note.id]); }, resetAutoSave() { this.autosave.reset(); diff --git a/app/assets/javascripts/notes/mixins/noteable.js b/app/assets/javascripts/notes/mixins/noteable.js new file mode 100644 index 00000000000..0da4ff49f08 --- /dev/null +++ b/app/assets/javascripts/notes/mixins/noteable.js @@ -0,0 +1,22 @@ +import * as constants from '../constants'; + +export default { + props: { + note: { + type: Object, + required: true, + }, + }, + computed: { + noteableType() { + switch (this.note.noteable_type) { + case 'MergeRequest': + return constants.MERGE_REQUEST_NOTEABLE_TYPE; + case 'Issue': + return constants.ISSUE_NOTEABLE_TYPE; + default: + return ''; + } + }, + }, +}; diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js new file mode 100644 index 00000000000..ab1ae115e52 --- /dev/null +++ b/app/assets/javascripts/notes/mixins/resolvable.js @@ -0,0 +1,50 @@ +import Flash from '~/flash'; +import { __ } from '~/locale'; + +export default { + props: { + note: { + type: Object, + required: true, + }, + }, + computed: { + discussionResolved() { + const { notes, resolved } = this.note; + + if (notes) { // Decide resolved state using store. Only valid for discussions. + return notes.every(note => note.resolved && !note.system); + } + + return resolved; + }, + resolveButtonTitle() { + if (this.updatedNoteBody) { + if (this.discussionResolved) { + return __('Comment and unresolve discussion'); + } + + return __('Comment and resolve discussion'); + } + return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion'); + }, + }, + methods: { + resolveHandler(resolvedState = false) { + this.isResolving = true; + const endpoint = this.note.resolve_path || `${this.note.path}/resolve`; + const isResolved = this.discussionResolved || resolvedState; + const discussion = this.resolveAsThread; + + this.toggleResolveNote({ endpoint, isResolved, discussion }) + .then(() => { + this.isResolving = false; + }) + .catch(() => { + this.isResolving = false; + const msg = __('Something went wrong while resolving this discussion. Please try again.'); + Flash(msg, 'alert', this.$el); + }); + }, + }, +}; diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js index b51b0cb2013..4766351dfc5 100644 --- a/app/assets/javascripts/notes/services/notes_service.js +++ b/app/assets/javascripts/notes/services/notes_service.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import VueResource from 'vue-resource'; +import * as constants from '../constants'; Vue.use(VueResource); @@ -19,6 +20,12 @@ export default { createNewNote(endpoint, data) { return Vue.http.post(endpoint, data, { emulateJSON: true }); }, + toggleResolveNote(endpoint, isResolved) { + const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants; + const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME; + + return Vue.http[method](endpoint); + }, poll(data = {}) { const { endpoint, lastFetchedAt } = data; const options = { @@ -32,4 +39,7 @@ export default { toggleAward(endpoint, data) { return Vue.http.post(endpoint, data, { emulateJSON: true }); }, + toggleIssueState(endpoint, data) { + return Vue.http.put(endpoint, data); + }, }; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 085b18642ba..42fc2a131b8 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -61,6 +61,48 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES); +export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) => service + .toggleResolveNote(endpoint, isResolved) + .then(res => res.json()) + .then((res) => { + const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE; + + commit(mutationType, res); + }); + +export const closeIssue = ({ commit, dispatch, state }) => service + .toggleIssueState(state.notesData.closePath) + .then(res => res.json()) + .then((data) => { + commit(types.CLOSE_ISSUE); + dispatch('emitStateChangedEvent', data); + }); + +export const reopenIssue = ({ commit, dispatch, state }) => service + .toggleIssueState(state.notesData.reopenPath) + .then(res => res.json()) + .then((data) => { + commit(types.REOPEN_ISSUE); + dispatch('emitStateChangedEvent', data); + }); + +export const emitStateChangedEvent = ({ commit, getters }, data) => { + const event = new CustomEvent('issuable_vue_app:change', { detail: { + data, + isClosed: getters.openState === constants.CLOSED, + } }); + + document.dispatchEvent(event); +}; + +export const toggleIssueLocalState = ({ commit }, newState) => { + if (newState === constants.CLOSED) { + commit(types.CLOSE_ISSUE); + } else if (newState === constants.REOPENED) { + commit(types.REOPEN_ISSUE); + } +}; + export const saveNote = ({ commit, dispatch }, noteData) => { const { note } = noteData.data.note; let placeholderText = note; @@ -141,7 +183,7 @@ const pollSuccessCallBack = (resp, commit, state, getters) => { resp.notes.forEach((note) => { if (notesById[note.id]) { commit(types.UPDATE_NOTE, note); - } else if (note.type === constants.DISCUSSION_NOTE) { + } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) { const discussion = utils.findNoteObjectById(state.notes, note.discussion_id); if (discussion) { diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index e18b277119e..e6180101c58 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -8,6 +8,7 @@ export const getNotesDataByProp = state => prop => state.notesData[prop]; export const getNoteableData = state => state.noteableData; export const getNoteableDataByProp = state => prop => state.noteableData[prop]; +export const openState = state => state.noteableData.state; export const getUserData = state => state.userData || {}; export const getUserDataByProp = state => prop => state.userData && state.userData[prop]; @@ -29,3 +30,37 @@ export const getCurrentUserLastNote = state => _.flatten( export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes) .find(el => isLastNote(el, state)); + +export const discussionCount = (state) => { + const discussions = state.notes.filter(n => !n.individual_note); + + return discussions.length; +}; + +export const unresolvedDiscussions = (state, getters) => { + const resolvedMap = getters.resolvedDiscussionsById; + + return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]); +}; + +export const resolvedDiscussionsById = (state) => { + const map = {}; + + state.notes.forEach((n) => { + if (n.notes) { + const resolved = n.notes.every(note => note.resolved && !note.system); + + if (resolved) { + map[n.id] = n; + } + } + }); + + return map; +}; + +export const resolvedDiscussionCount = (state, getters) => { + const resolvedMap = getters.resolvedDiscussionsById; + + return Object.keys(resolvedMap).length; +}; diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index d520c197407..da1b5a9e51a 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -12,3 +12,8 @@ export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE'; export const TOGGLE_AWARD = 'TOGGLE_AWARD'; export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION'; export const UPDATE_NOTE = 'UPDATE_NOTE'; +export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION'; + +// Issue +export const CLOSE_ISSUE = 'CLOSE_ISSUE'; +export const REOPEN_ISSUE = 'REOPEN_ISSUE'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 20f81a430c2..963b40be3fd 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -1,22 +1,32 @@ import * as utils from './utils'; import * as types from './mutation_types'; import * as constants from '../constants'; +import { isInMRPage } from '../../lib/utils/common_utils'; export default { [types.ADD_NEW_NOTE](state, note) { const { discussion_id, type } = note; const [exists] = state.notes.filter(n => n.id === note.discussion_id); + const isDiscussion = (type === constants.DISCUSSION_NOTE); if (!exists) { const noteData = { expanded: true, id: discussion_id, - individual_note: !(type === constants.DISCUSSION_NOTE), + individual_note: !isDiscussion, notes: [note], reply_id: discussion_id, }; + if (isDiscussion && isInMRPage()) { + noteData.resolvable = note.resolvable; + noteData.resolved = false; + noteData.resolve_path = note.resolve_path; + noteData.resolve_with_issue_path = note.resolve_with_issue_path; + } + state.notes.push(noteData); + document.dispatchEvent(new CustomEvent('refreshLegacyNotes')); } }, @@ -25,6 +35,7 @@ export default { if (noteObj) { noteObj.notes.push(note); + document.dispatchEvent(new CustomEvent('refreshLegacyNotes')); } }, @@ -41,6 +52,8 @@ export default { state.notes.splice(state.notes.indexOf(noteObj), 1); } } + + document.dispatchEvent(new CustomEvent('refreshLegacyNotes')); }, [types.REMOVE_PLACEHOLDER_NOTES](state) { @@ -77,15 +90,19 @@ export default { const notes = []; notesData.forEach((note) => { + const nn = Object.assign({}, note); + // To support legacy notes, should be very rare case. if (note.individual_note && note.notes.length > 1) { note.notes.forEach((n) => { - const nn = Object.assign({}, note); nn.notes = [n]; // override notes array to only have one item to mimick individual_note notes.push(nn); }); } else { - notes.push(note); + const oldNote = utils.findNoteObjectById(state.notes, note.id); + nn.expanded = oldNote ? oldNote.expanded : note.expanded; + + notes.push(nn); } }); @@ -134,6 +151,8 @@ export default { user: { id, name, username }, }); } + + document.dispatchEvent(new CustomEvent('refreshLegacyNotes')); }, [types.TOGGLE_DISCUSSION](state, { discussionId }) { @@ -151,5 +170,31 @@ export default { const comment = utils.findNoteObjectById(noteObj.notes, note.id); noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note); } + + // document.dispatchEvent(new CustomEvent('refreshLegacyNotes')); + }, + + [types.UPDATE_DISCUSSION](state, noteData) { + const note = noteData; + let index = 0; + + state.notes.forEach((n, i) => { + if (n.id === note.id) { + index = i; + } + }); + + note.expanded = true; // override expand flag to prevent collapse + state.notes.splice(index, 1, note); + + document.dispatchEvent(new CustomEvent('refreshLegacyNotes')); + }, + + [types.CLOSE_ISSUE](state) { + Object.assign(state.noteableData, { state: constants.CLOSED }); + }, + + [types.REOPEN_ISSUE](state) { + Object.assign(state.noteableData, { state: constants.REOPENED }); }, }; diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js index 6074115e855..275263a2aaa 100644 --- a/app/assets/javascripts/notes/stores/utils.js +++ b/app/assets/javascripts/notes/stores/utils.js @@ -28,4 +28,3 @@ export const getQuickActionText = (note) => { export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note); export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim(); - diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index 9570d1c00aa..479a512ed65 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -3,11 +3,11 @@ import Flash from './flash'; export default function notificationsDropdown() { $(document).on('click', '.update-notification', function updateNotificationCallback(e) { e.preventDefault(); - if ($(this).is('.is-active') && $(this).data('notification-level') === 'custom') { + if ($(this).is('.is-active') && $(this).data('notificationLevel') === 'custom') { return; } - const notificationLevel = $(this).data('notification-level'); + const notificationLevel = $(this).data('notificationLevel'); const form = $(this).parents('.notification-form:first'); form.find('.js-notification-loading').toggleClass('fa-bell fa-spin fa-spinner'); diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index fd3105b1960..7e85bce0d73 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -56,7 +56,7 @@ export default { }, initLoadMore() { - $(document).unbind('scroll'); + $(document).off('scroll'); $(document).endlessScroll({ bottomPixels: ENDLESS_SCROLL_BOTTOM_PX, fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS, diff --git a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js index d87e6304a24..66702ec4ca0 100644 --- a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js +++ b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js @@ -15,21 +15,21 @@ export default class AbuseReports { const $messageCellElement = $(this); const reportMessage = $messageCellElement.text(); if (reportMessage.length > MAX_MESSAGE_LENGTH) { - $messageCellElement.data('original-message', reportMessage); - $messageCellElement.data('message-truncated', 'true'); + $messageCellElement.data('originalMessage', reportMessage); + $messageCellElement.data('messageTruncated', 'true'); $messageCellElement.text(truncate(reportMessage, MAX_MESSAGE_LENGTH)); } } toggleMessageTruncation() { const $messageCellElement = $(this); - const originalMessage = $messageCellElement.data('original-message'); + const originalMessage = $messageCellElement.data('originalMessage'); if (!originalMessage) return; - if ($messageCellElement.data('message-truncated') === 'true') { - $messageCellElement.data('message-truncated', 'false'); + if ($messageCellElement.data('messageTruncated') === 'true') { + $messageCellElement.data('messageTruncated', 'false'); $messageCellElement.text(originalMessage); } else { - $messageCellElement.data('message-truncated', 'true'); + $messageCellElement.data('messageTruncated', 'true'); $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`); } } diff --git a/app/assets/javascripts/pages/admin/abuse_reports/index.js b/app/assets/javascripts/pages/admin/abuse_reports/index.js index c0b6e8d4095..d76b1f174fc 100644 --- a/app/assets/javascripts/pages/admin/abuse_reports/index.js +++ b/app/assets/javascripts/pages/admin/abuse_reports/index.js @@ -1,3 +1,3 @@ import AbuseReports from './abuse_reports'; -export default () => new AbuseReports(); +document.addEventListener('DOMContentLoaded', () => new AbuseReports()); diff --git a/app/assets/javascripts/pages/admin/admin.js b/app/assets/javascripts/pages/admin/admin.js index 135c15c346b..45e05f111a7 100644 --- a/app/assets/javascripts/pages/admin/admin.js +++ b/app/assets/javascripts/pages/admin/admin.js @@ -16,9 +16,9 @@ export default function adminInit() { $('input#user_force_random_password').on('change', function randomPasswordClick() { const $elems = $('#user_password, #user_password_confirmation'); if ($(this).attr('checked')) { - $elems.val('').attr('disabled', true); + $elems.val('').prop('disabled', true); } else { - $elems.removeAttr('disabled'); + $elems.prop('disabled', false); } }); diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js index 885acfac6d0..f92450cbaa7 100644 --- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js +++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js @@ -3,7 +3,7 @@ import axios from '~/lib/utils/axios_utils'; import flash from '~/flash'; import { __ } from '~/locale'; -export default function initBroadcastMessagesForm() { +export default () => { $('input#broadcast_message_color').on('input', function onMessageColorInput() { const previewColor = $(this).val(); $('div.broadcast-message-preview').css('background-color', previewColor); @@ -14,7 +14,7 @@ export default function initBroadcastMessagesForm() { $('div.broadcast-message-preview').css('color', previewColor); }); - const previewPath = $('textarea#broadcast_message_message').data('preview-path'); + const previewPath = $('textarea#broadcast_message_message').data('previewPath'); $('textarea#broadcast_message_message').on('input', _.debounce(function onMessageInput() { const message = $(this).val(); @@ -32,4 +32,4 @@ export default function initBroadcastMessagesForm() { .catch(() => flash(__('An error occurred while rendering preview broadcast message'))); } }, 250)); -} +}; diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/index.js index b548c48282a..d6cc6a850eb 100644 --- a/app/assets/javascripts/pages/admin/broadcast_messages/index.js +++ b/app/assets/javascripts/pages/admin/broadcast_messages/index.js @@ -1,3 +1,3 @@ import initBroadcastMessagesForm from './broadcast_message'; -export default () => initBroadcastMessagesForm(); +document.addEventListener('DOMContentLoaded', initBroadcastMessagesForm); diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/admin/cohorts/index.js index 42ef9d38ef7..2d5020dbef4 100644 --- a/app/assets/javascripts/pages/admin/cohorts/index.js +++ b/app/assets/javascripts/pages/admin/cohorts/index.js @@ -1,3 +1,3 @@ import initUsagePing from './usage_ping'; -export default () => initUsagePing(); +document.addEventListener('DOMContentLoaded', initUsagePing); diff --git a/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js b/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js index 6e66ef69fe1..c1056537f90 100644 --- a/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js +++ b/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js @@ -1,3 +1,3 @@ -import UserCallout from '../../../../user_callout'; +import UserCallout from '~/user_callout'; -export default () => new UserCallout(); +document.addEventListener('DOMContentLoaded', () => new UserCallout()); diff --git a/app/assets/javascripts/pages/admin/groups/edit/index.js b/app/assets/javascripts/pages/admin/groups/edit/index.js index ff9ef8d2449..d3d125a1859 100644 --- a/app/assets/javascripts/pages/admin/groups/edit/index.js +++ b/app/assets/javascripts/pages/admin/groups/edit/index.js @@ -1,3 +1,3 @@ -import groupAvatar from '../../../../group_avatar'; +import groupAvatar from '~/group_avatar'; -export default () => groupAvatar(); +document.addEventListener('DOMContentLoaded', groupAvatar); diff --git a/app/assets/javascripts/pages/admin/groups/new/index.js b/app/assets/javascripts/pages/admin/groups/new/index.js index fb5c46e4729..21f1ce222ac 100644 --- a/app/assets/javascripts/pages/admin/groups/new/index.js +++ b/app/assets/javascripts/pages/admin/groups/new/index.js @@ -2,8 +2,8 @@ import BindInOut from '../../../../behaviors/bind_in_out'; import Group from '../../../../group'; import groupAvatar from '../../../../group_avatar'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { BindInOut.initAll(); new Group(); // eslint-disable-line no-new groupAvatar(); -}; +}); diff --git a/app/assets/javascripts/pages/admin/groups/show/index.js b/app/assets/javascripts/pages/admin/groups/show/index.js index 5defea104d4..b0cdad627a6 100644 --- a/app/assets/javascripts/pages/admin/groups/show/index.js +++ b/app/assets/javascripts/pages/admin/groups/show/index.js @@ -1,3 +1,3 @@ import UsersSelect from '../../../../users_select'; -export default () => new UsersSelect(); +document.addEventListener('DOMContentLoaded', () => new UsersSelect()); diff --git a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js index 030328a1363..78a5c4c27be 100644 --- a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js +++ b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js @@ -1,3 +1,3 @@ -import DueDateSelectors from '../../../due_date_select'; +import DueDateSelectors from '~/due_date_select'; -export default () => new DueDateSelectors(); +document.addEventListener('DOMContentLoaded', () => new DueDateSelectors()); diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js index 8b843037d85..e50b61f09e2 100644 --- a/app/assets/javascripts/pages/admin/index.js +++ b/app/assets/javascripts/pages/admin/index.js @@ -1,3 +1,3 @@ import initAdmin from './admin'; -export default () => initAdmin(); +document.addEventListener('DOMContentLoaded', initAdmin); diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue index 555725cbe12..ba1d8e4d8db 100644 --- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue +++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue @@ -1,13 +1,13 @@ <script> import axios from '~/lib/utils/axios_utils'; - import Flash from '~/flash'; - import modal from '~/vue_shared/components/modal.vue'; - import { s__ } from '~/locale'; + import createFlash from '~/flash'; + import GlModal from '~/vue_shared/components/gl_modal.vue'; import { redirectTo } from '~/lib/utils/url_utility'; + import { s__ } from '~/locale'; export default { components: { - modal, + GlModal, }, props: { url: { @@ -17,7 +17,7 @@ }, computed: { text() { - return s__('AdminArea|You’re about to stop all jobs. This will halt all current jobs that are running.'); + return s__('AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running.'); }, }, methods: { @@ -28,7 +28,7 @@ redirectTo(response.request.responseURL); }) .catch((error) => { - Flash(s__('AdminArea|Stopping jobs failed')); + createFlash(s__('AdminArea|Stopping jobs failed')); throw error; }); }, @@ -37,11 +37,13 @@ </script> <template> - <modal + <gl-modal id="stop-jobs-modal" - :title="s__('AdminArea|Stop all jobs?')" - :text="text" - kind="danger" - :primary-button-label="s__('AdminArea|Stop jobs')" - @submit="onSubmit" /> + :header-title-text="s__('AdminArea|Stop all jobs?')" + footer-primary-button-variant="danger" + :footer-primary-button-text="s__('AdminArea|Stop jobs')" + @submit="onSubmit" + > + {{ text }} + </gl-modal> </template> diff --git a/app/assets/javascripts/pages/admin/jobs/index/index.js b/app/assets/javascripts/pages/admin/jobs/index/index.js index 0e004bd9174..5a4f8c6e745 100644 --- a/app/assets/javascripts/pages/admin/jobs/index/index.js +++ b/app/assets/javascripts/pages/admin/jobs/index/index.js @@ -1,29 +1,28 @@ import Vue from 'vue'; - import Translate from '~/vue_shared/translate'; - import stopJobsModal from './components/stop_jobs_modal.vue'; Vue.use(Translate); -export default () => { +document.addEventListener('DOMContentLoaded', () => { const stopJobsButton = document.getElementById('stop-jobs-button'); - - // eslint-disable-next-line no-new - new Vue({ - el: '#stop-jobs-modal', - components: { - stopJobsModal, - }, - mounted() { - stopJobsButton.classList.remove('disabled'); - }, - render(createElement) { - return createElement('stop-jobs-modal', { - props: { - url: stopJobsButton.dataset.url, - }, - }); - }, - }); -}; + if (stopJobsButton) { + // eslint-disable-next-line no-new + new Vue({ + el: '#stop-jobs-modal', + components: { + stopJobsModal, + }, + mounted() { + stopJobsButton.classList.remove('disabled'); + }, + render(createElement) { + return createElement('stop-jobs-modal', { + props: { + url: stopJobsButton.dataset.url, + }, + }); + }, + }); + } +}); diff --git a/app/assets/javascripts/pages/admin/labels/edit/index.js b/app/assets/javascripts/pages/admin/labels/edit/index.js index d7ec6e47f67..5de1d4d6344 100644 --- a/app/assets/javascripts/pages/admin/labels/edit/index.js +++ b/app/assets/javascripts/pages/admin/labels/edit/index.js @@ -1,3 +1,3 @@ import Labels from '../../../../labels'; -export default () => new Labels(); +document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/admin/labels/new/index.js b/app/assets/javascripts/pages/admin/labels/new/index.js index d7ec6e47f67..5de1d4d6344 100644 --- a/app/assets/javascripts/pages/admin/labels/new/index.js +++ b/app/assets/javascripts/pages/admin/labels/new/index.js @@ -1,3 +1,3 @@ import Labels from '../../../../labels'; -export default () => new Labels(); +document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js index 71e0ddcd7b6..31c96eb87af 100644 --- a/app/assets/javascripts/pages/admin/projects/index.js +++ b/app/assets/javascripts/pages/admin/projects/index.js @@ -1,9 +1,9 @@ import ProjectsList from '../../../projects_list'; import NamespaceSelect from '../../../namespace_select'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new ProjectsList(); // eslint-disable-line no-new document.querySelectorAll('.js-namespace-select') .forEach(dropdown => new NamespaceSelect({ dropdown })); -}; +}); diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue new file mode 100644 index 00000000000..14315d5492e --- /dev/null +++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue @@ -0,0 +1,125 @@ +<script> + import _ from 'underscore'; + import modal from '~/vue_shared/components/modal.vue'; + import { s__, sprintf } from '~/locale'; + + export default { + components: { + modal, + }, + props: { + deleteProjectUrl: { + type: String, + required: false, + default: '', + }, + projectName: { + type: String, + required: false, + default: '', + }, + csrfToken: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + enteredProjectName: '', + }; + }, + computed: { + title() { + return sprintf(s__('AdminProjects|Delete Project %{projectName}?'), + { + projectName: `'${_.escape(this.projectName)}'`, + }, + false, + ); + }, + text() { + return sprintf(s__(`AdminProjects| + You’re about to permanently delete the project %{projectName}, its repository, + and all related resources including issues, merge requests, etc.. Once you confirm and press + %{strong_start}Delete project%{strong_end}, it cannot be undone or recovered.`), + { + projectName: `<strong>${_.escape(this.projectName)}</strong>`, + strong_start: '<strong>', + strong_end: '</strong>', + }, + false, + ); + }, + confirmationTextLabel() { + return sprintf(s__('AdminUsers|To confirm, type %{projectName}'), + { + projectName: `<code>${_.escape(this.projectName)}</code>`, + }, + false, + ); + }, + primaryButtonLabel() { + return s__('AdminProjects|Delete project'); + }, + canSubmit() { + return this.enteredProjectName === this.projectName; + }, + }, + methods: { + onCancel() { + this.enteredProjectName = ''; + }, + onSubmit() { + this.$refs.form.submit(); + this.enteredProjectName = ''; + }, + }, + }; +</script> + +<template> + <modal + id="delete-project-modal" + :title="title" + :text="text" + kind="danger" + :primary-button-label="primaryButtonLabel" + :submit-disabled="!canSubmit" + @submit="onSubmit" + @cancel="onCancel" + > + <template + slot="body" + slot-scope="props" + > + <p v-html="props.text"></p> + <p v-html="confirmationTextLabel"></p> + <form + ref="form" + :action="deleteProjectUrl" + method="post" + > + <input + ref="method" + type="hidden" + name="_method" + value="delete" + /> + <input + type="hidden" + name="authenticity_token" + :value="csrfToken" + /> + <input + name="projectName" + class="form-control" + type="text" + v-model="enteredProjectName" + aria-labelledby="input-label" + autocomplete="off" + /> + </form> + </template> + </modal> +</template> diff --git a/app/assets/javascripts/pages/admin/projects/index/index.js b/app/assets/javascripts/pages/admin/projects/index/index.js new file mode 100644 index 00000000000..3c597a1093e --- /dev/null +++ b/app/assets/javascripts/pages/admin/projects/index/index.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; + +import Translate from '~/vue_shared/translate'; +import csrf from '~/lib/utils/csrf'; + +import deleteProjectModal from './components/delete_project_modal.vue'; + +document.addEventListener('DOMContentLoaded', () => { + Vue.use(Translate); + + const deleteProjectModalEl = document.getElementById('delete-project-modal'); + + const deleteModal = new Vue({ + el: deleteProjectModalEl, + data: { + deleteProjectUrl: '', + projectName: '', + }, + render(createElement) { + return createElement(deleteProjectModal, { + props: { + deleteProjectUrl: this.deleteProjectUrl, + projectName: this.projectName, + csrfToken: csrf.token, + }, + }); + }, + }); + + $(document).on('shown.bs.modal', (event) => { + if (event.relatedTarget.classList.contains('delete-project-button')) { + const buttonProps = event.relatedTarget.dataset; + deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl; + deleteModal.projectName = buttonProps.projectName; + } + }); +}); diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue new file mode 100644 index 00000000000..7b5e333011e --- /dev/null +++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue @@ -0,0 +1,174 @@ +<script> + import _ from 'underscore'; + import modal from '~/vue_shared/components/modal.vue'; + import { s__, sprintf } from '~/locale'; + + export default { + components: { + modal, + }, + props: { + deleteUserUrl: { + type: String, + required: false, + default: '', + }, + blockUserUrl: { + type: String, + required: false, + default: '', + }, + deleteContributions: { + type: Boolean, + required: false, + default: false, + }, + username: { + type: String, + required: false, + default: '', + }, + csrfToken: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + enteredUsername: '', + }; + }, + computed: { + title() { + const keepContributionsTitle = s__('AdminUsers|Delete User %{username}?'); + const deleteContributionsTitle = s__('AdminUsers|Delete User %{username} and contributions?'); + + return sprintf( + this.deleteContributions ? deleteContributionsTitle : keepContributionsTitle, { + username: `'${_.escape(this.username)}'`, + }, false); + }, + text() { + const keepContributionsText = s__(`AdminArea| + You are about to permanently delete the user %{username}. + This will delete all of the issues, merge requests, and groups linked to them. + To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. + Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`); + + const deleteContributionsText = s__(`AdminArea| + You are about to permanently delete the user %{username}. + Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user". + To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. + Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`); + + return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText, + { + username: `<strong>${_.escape(this.username)}</strong>`, + strong_start: '<strong>', + strong_end: '</strong>', + }, + false, + ); + }, + confirmationTextLabel() { + return sprintf(s__('AdminUsers|To confirm, type %{username}'), + { + username: `<code>${_.escape(this.username)}</code>`, + }, + false, + ); + }, + primaryButtonLabel() { + const keepContributionsLabel = s__('AdminUsers|Delete user'); + const deleteContributionsLabel = s__('AdminUsers|Delete user and contributions'); + + return this.deleteContributions ? deleteContributionsLabel : keepContributionsLabel; + }, + secondaryButtonLabel() { + return s__('AdminUsers|Block user'); + }, + canSubmit() { + return this.enteredUsername === this.username; + }, + }, + methods: { + onCancel() { + this.enteredUsername = ''; + }, + onSecondaryAction() { + const form = this.$refs.form; + + form.action = this.blockUserUrl; + this.$refs.method.value = 'put'; + + form.submit(); + }, + onSubmit() { + this.$refs.form.submit(); + this.enteredUsername = ''; + }, + }, + }; +</script> + +<template> + <modal + id="delete-user-modal" + :title="title" + :text="text" + kind="danger" + :primary-button-label="primaryButtonLabel" + :secondary-button-label="secondaryButtonLabel" + :submit-disabled="!canSubmit" + @submit="onSubmit" + @cancel="onCancel" + > + <template + slot="body" + slot-scope="props" + > + <p v-html="props.text"></p> + <p v-html="confirmationTextLabel"></p> + <form + ref="form" + :action="deleteUserUrl" + method="post" + > + <input + ref="method" + type="hidden" + name="_method" + value="delete" + /> + <input + type="hidden" + name="authenticity_token" + :value="csrfToken" + /> + <input + type="text" + name="username" + class="form-control" + v-model="enteredUsername" + aria-labelledby="input-label" + autocomplete="off" + /> + </form> + </template> + <template + slot="secondary-button" + slot-scope="props" + > + <button + type="button" + class="btn js-secondary-button btn-warning" + :disabled="!canSubmit" + @click="onSecondaryAction" + data-dismiss="modal" + > + {{ secondaryButtonLabel }} + </button> + </template> + </modal> +</template> diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js new file mode 100644 index 00000000000..4f5d6b55031 --- /dev/null +++ b/app/assets/javascripts/pages/admin/users/index.js @@ -0,0 +1,43 @@ +import Vue from 'vue'; + +import Translate from '~/vue_shared/translate'; +import csrf from '~/lib/utils/csrf'; + +import deleteUserModal from './components/delete_user_modal.vue'; + +document.addEventListener('DOMContentLoaded', () => { + Vue.use(Translate); + + const deleteUserModalEl = document.getElementById('delete-user-modal'); + + const deleteModal = new Vue({ + el: deleteUserModalEl, + data: { + deleteUserUrl: '', + blockUserUrl: '', + deleteContributions: '', + username: '', + }, + render(createElement) { + return createElement(deleteUserModal, { + props: { + deleteUserUrl: this.deleteUserUrl, + blockUserUrl: this.blockUserUrl, + deleteContributions: this.deleteContributions, + username: this.username, + csrfToken: csrf.token, + }, + }); + }, + }); + + $(document).on('shown.bs.modal', (event) => { + if (event.relatedTarget.classList.contains('delete-user-button')) { + const buttonProps = event.relatedTarget.dataset; + deleteModal.deleteUserUrl = buttonProps.deleteUserUrl; + deleteModal.blockUserUrl = buttonProps.blockUserUrl; + deleteModal.deleteContributions = event.relatedTarget.hasAttribute('data-delete-contributions'); + deleteModal.username = buttonProps.username; + } + }); +}); diff --git a/app/assets/javascripts/pages/ci/lints/ci_lint_editor.js b/app/assets/javascripts/pages/ci/lints/ci_lint_editor.js index b9469e5b7cb..9ab73be80a0 100644 --- a/app/assets/javascripts/pages/ci/lints/ci_lint_editor.js +++ b/app/assets/javascripts/pages/ci/lints/ci_lint_editor.js @@ -2,11 +2,18 @@ export default class CILintEditor { constructor() { this.editor = window.ace.edit('ci-editor'); this.textarea = document.querySelector('#content'); + this.clearYml = document.querySelector('.clear-yml'); this.editor.getSession().setMode('ace/mode/yaml'); this.editor.on('input', () => { const content = this.editor.getSession().getValue(); this.textarea.value = content; }); + + this.clearYml.addEventListener('click', this.clear.bind(this)); + } + + clear() { + this.editor.setValue(''); } } diff --git a/app/assets/javascripts/pages/ci/lints/create/index.js b/app/assets/javascripts/pages/ci/lints/create/index.js new file mode 100644 index 00000000000..8e8a843da0b --- /dev/null +++ b/app/assets/javascripts/pages/ci/lints/create/index.js @@ -0,0 +1,3 @@ +import CILintEditor from '../ci_lint_editor'; + +document.addEventListener('DOMContentLoaded', () => new CILintEditor()); diff --git a/app/assets/javascripts/pages/ci/lints/index.js b/app/assets/javascripts/pages/ci/lints/index.js deleted file mode 100644 index 5cc66546109..00000000000 --- a/app/assets/javascripts/pages/ci/lints/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import CILintEditor from './ci_lint_editor'; - -export default () => new CILintEditor(); diff --git a/app/assets/javascripts/pages/ci/lints/show/index.js b/app/assets/javascripts/pages/ci/lints/show/index.js new file mode 100644 index 00000000000..8e8a843da0b --- /dev/null +++ b/app/assets/javascripts/pages/ci/lints/show/index.js @@ -0,0 +1,3 @@ +import CILintEditor from '../ci_lint_editor'; + +document.addEventListener('DOMContentLoaded', () => new CILintEditor()); diff --git a/app/assets/javascripts/pages/dashboard/activity/index.js b/app/assets/javascripts/pages/dashboard/activity/index.js index 95faf1f1e98..1b887cad496 100644 --- a/app/assets/javascripts/pages/dashboard/activity/index.js +++ b/app/assets/javascripts/pages/dashboard/activity/index.js @@ -1,3 +1,3 @@ import Activities from '~/activities'; -export default () => new Activities(); +document.addEventListener('DOMContentLoaded', () => new Activities()); diff --git a/app/assets/javascripts/pages/dashboard/groups/index/index.js b/app/assets/javascripts/pages/dashboard/groups/index/index.js index 8a2aae706c0..79987642796 100644 --- a/app/assets/javascripts/pages/dashboard/groups/index/index.js +++ b/app/assets/javascripts/pages/dashboard/groups/index/index.js @@ -1,5 +1,3 @@ -import initGroupsList from '../../../../groups'; +import initGroupsList from '~/groups'; -export default () => { - initGroupsList(); -}; +document.addEventListener('DOMContentLoaded', initGroupsList); diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js index b7353669e65..c4901dd1cb6 100644 --- a/app/assets/javascripts/pages/dashboard/issues/index.js +++ b/app/assets/javascripts/pages/dashboard/issues/index.js @@ -1,7 +1,7 @@ import projectSelect from '~/project_select'; import initLegacyFilters from '~/init_legacy_filters'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { projectSelect(); initLegacyFilters(); -}; +}); diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js index b7353669e65..c4901dd1cb6 100644 --- a/app/assets/javascripts/pages/dashboard/merge_requests/index.js +++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js @@ -1,7 +1,7 @@ import projectSelect from '~/project_select'; import initLegacyFilters from '~/init_legacy_filters'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { projectSelect(); initLegacyFilters(); -}; +}); diff --git a/app/assets/javascripts/pages/dashboard/milestones/show/index.js b/app/assets/javascripts/pages/dashboard/milestones/show/index.js index 2e7a08a369c..397149aaa9e 100644 --- a/app/assets/javascripts/pages/dashboard/milestones/show/index.js +++ b/app/assets/javascripts/pages/dashboard/milestones/show/index.js @@ -1,7 +1,9 @@ import Milestone from '~/milestone'; import Sidebar from '~/right_sidebar'; +import MountMilestoneSidebar from '~/sidebar/mount_milestone_sidebar'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new Milestone(); // eslint-disable-line no-new new Sidebar(); // eslint-disable-line no-new -}; + new MountMilestoneSidebar(); // eslint-disable-line no-new +}); diff --git a/app/assets/javascripts/pages/dashboard/projects/index.js b/app/assets/javascripts/pages/dashboard/projects/index.js index c88cbf1a6ba..0c585e162cb 100644 --- a/app/assets/javascripts/pages/dashboard/projects/index.js +++ b/app/assets/javascripts/pages/dashboard/projects/index.js @@ -1,3 +1,3 @@ import ProjectsList from '~/projects_list'; -export default () => new ProjectsList(); +document.addEventListener('DOMContentLoaded', () => new ProjectsList()); diff --git a/app/assets/javascripts/pages/dashboard/todos/index/index.js b/app/assets/javascripts/pages/dashboard/todos/index/index.js index 77c23685943..9d2c2f2994f 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/index.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/index.js @@ -1,3 +1,3 @@ import Todos from './todos'; -export default () => new Todos(); +document.addEventListener('DOMContentLoaded', () => new Todos()); diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index b3f6a72fdcb..42f7460ad55 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -2,9 +2,9 @@ import { visitUrl } from '~/lib/utils/url_utility'; import UsersSelect from '~/users_select'; import { isMetaClick } from '~/lib/utils/common_utils'; -import { __ } from '../../../../locale'; -import flash from '../../../../flash'; -import axios from '../../../../lib/utils/axios_utils'; +import { __ } from '~/locale'; +import flash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; export default class Todos { constructor() { diff --git a/app/assets/javascripts/pages/explore/groups/index.js b/app/assets/javascripts/pages/explore/groups/index.js index e59c38b8bc4..3c7edbdd7c7 100644 --- a/app/assets/javascripts/pages/explore/groups/index.js +++ b/app/assets/javascripts/pages/explore/groups/index.js @@ -2,7 +2,7 @@ import GroupsList from '~/groups_list'; import Landing from '~/landing'; import initGroupsList from '../../../groups'; -export default function () { +document.addEventListener('DOMContentLoaded', () => { new GroupsList(); // eslint-disable-line no-new initGroupsList(); const landingElement = document.querySelector('.js-explore-groups-landing'); @@ -13,4 +13,4 @@ export default function () { 'explore_groups_landing_dismissed', ); exploreGroupsLanding.toggle(); -} +}); diff --git a/app/assets/javascripts/pages/explore/projects/index.js b/app/assets/javascripts/pages/explore/projects/index.js index c88cbf1a6ba..0c585e162cb 100644 --- a/app/assets/javascripts/pages/explore/projects/index.js +++ b/app/assets/javascripts/pages/explore/projects/index.js @@ -1,3 +1,3 @@ import ProjectsList from '~/projects_list'; -export default () => new ProjectsList(); +document.addEventListener('DOMContentLoaded', () => new ProjectsList()); diff --git a/app/assets/javascripts/pages/groups/activity/index.js b/app/assets/javascripts/pages/groups/activity/index.js index 95faf1f1e98..1b887cad496 100644 --- a/app/assets/javascripts/pages/groups/activity/index.js +++ b/app/assets/javascripts/pages/groups/activity/index.js @@ -1,3 +1,3 @@ import Activities from '~/activities'; -export default () => new Activities(); +document.addEventListener('DOMContentLoaded', () => new Activities()); diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js index 1aeec55a4be..d44874c8741 100644 --- a/app/assets/javascripts/pages/groups/edit/index.js +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -1,7 +1,7 @@ import groupAvatar from '~/group_avatar'; import TransferDropdown from '~/groups/transfer_dropdown'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { groupAvatar(); new TransferDropdown(); // eslint-disable-line no-new -}; +}); diff --git a/app/assets/javascripts/pages/groups/group_members/index/index.js b/app/assets/javascripts/pages/groups/group_members/index/index.js index 29319b97ae2..c22a164cd4e 100644 --- a/app/assets/javascripts/pages/groups/group_members/index/index.js +++ b/app/assets/javascripts/pages/groups/group_members/index/index.js @@ -4,8 +4,8 @@ import memberExpirationDate from '~/member_expiration_date'; import Members from '~/members'; import UsersSelect from '~/users_select'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { memberExpirationDate(); new Members(); new UsersSelect(); -}; +}); diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js index 78db543a64d..d149b307e7f 100644 --- a/app/assets/javascripts/pages/groups/issues/index.js +++ b/app/assets/javascripts/pages/groups/issues/index.js @@ -2,7 +2,9 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; import { FILTERED_SEARCH } from '~/pages/constants'; -export default () => { - initFilteredSearch(FILTERED_SEARCH.ISSUES); +document.addEventListener('DOMContentLoaded', () => { + initFilteredSearch({ + page: FILTERED_SEARCH.ISSUES, + }); projectSelect(); -}; +}); diff --git a/app/assets/javascripts/pages/groups/labels/edit/index.js b/app/assets/javascripts/pages/groups/labels/edit/index.js index 72c5e4744ac..fa81ad914ba 100644 --- a/app/assets/javascripts/pages/groups/labels/edit/index.js +++ b/app/assets/javascripts/pages/groups/labels/edit/index.js @@ -1,3 +1,3 @@ import Labels from '~/labels'; -export default () => new Labels(); +document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/groups/labels/index/index.js b/app/assets/javascripts/pages/groups/labels/index/index.js index 018345fa112..6e45de2a724 100644 --- a/app/assets/javascripts/pages/groups/labels/index/index.js +++ b/app/assets/javascripts/pages/groups/labels/index/index.js @@ -1,3 +1,3 @@ import initLabels from '~/init_labels'; -export default initLabels; +document.addEventListener('DOMContentLoaded', initLabels); diff --git a/app/assets/javascripts/pages/groups/labels/new/index.js b/app/assets/javascripts/pages/groups/labels/new/index.js index 72c5e4744ac..fa81ad914ba 100644 --- a/app/assets/javascripts/pages/groups/labels/new/index.js +++ b/app/assets/javascripts/pages/groups/labels/new/index.js @@ -1,3 +1,3 @@ import Labels from '~/labels'; -export default () => new Labels(); +document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js index 9b3af4537e7..a5cc1f34b63 100644 --- a/app/assets/javascripts/pages/groups/merge_requests/index.js +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -2,7 +2,9 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; import { FILTERED_SEARCH } from '~/pages/constants'; -export default () => { - initFilteredSearch(FILTERED_SEARCH.MERGE_REQUESTS); +document.addEventListener('DOMContentLoaded', () => { + initFilteredSearch({ + page: FILTERED_SEARCH.MERGE_REQUESTS, + }); projectSelect(); -}; +}); diff --git a/app/assets/javascripts/pages/groups/milestones/edit/index.js b/app/assets/javascripts/pages/groups/milestones/edit/index.js index 5c99c90e24d..ddd10fe5062 100644 --- a/app/assets/javascripts/pages/groups/milestones/edit/index.js +++ b/app/assets/javascripts/pages/groups/milestones/edit/index.js @@ -1,3 +1,3 @@ import initForm from '../../../../shared/milestones/form'; -export default () => initForm(false); +document.addEventListener('DOMContentLoaded', () => initForm(false)); diff --git a/app/assets/javascripts/pages/groups/milestones/new/index.js b/app/assets/javascripts/pages/groups/milestones/new/index.js index 5c99c90e24d..ddd10fe5062 100644 --- a/app/assets/javascripts/pages/groups/milestones/new/index.js +++ b/app/assets/javascripts/pages/groups/milestones/new/index.js @@ -1,3 +1,3 @@ import initForm from '../../../../shared/milestones/form'; -export default () => initForm(false); +document.addEventListener('DOMContentLoaded', () => initForm(false)); diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js index c9a18353f2e..88f40b5278e 100644 --- a/app/assets/javascripts/pages/groups/milestones/show/index.js +++ b/app/assets/javascripts/pages/groups/milestones/show/index.js @@ -1,3 +1,3 @@ import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; -export default initMilestonesShow; +document.addEventListener('DOMContentLoaded', initMilestonesShow); diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js index 7850b90d3d2..b2f275dc5ea 100644 --- a/app/assets/javascripts/pages/groups/new/index.js +++ b/app/assets/javascripts/pages/groups/new/index.js @@ -2,8 +2,8 @@ import BindInOut from '~/behaviors/bind_in_out'; import Group from '~/group'; import groupAvatar from '~/group_avatar'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { BindInOut.initAll(); new Group(); // eslint-disable-line no-new groupAvatar(); -}; +}); diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js index ad79f7e09ac..04a0d8117cc 100644 --- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js @@ -1,6 +1,6 @@ import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { const variableListEl = document.querySelector('.js-ci-variable-list-section'); // eslint-disable-next-line no-new new AjaxVariableList({ @@ -9,4 +9,4 @@ export default () => { errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), saveEndpoint: variableListEl.dataset.saveEndpoint, }); -}; +}); diff --git a/app/assets/javascripts/pages/groups/show/index.js b/app/assets/javascripts/pages/groups/show/index.js index 5c763986da3..d7b35d2b26b 100644 --- a/app/assets/javascripts/pages/groups/show/index.js +++ b/app/assets/javascripts/pages/groups/show/index.js @@ -5,7 +5,7 @@ import notificationsDropdown from '~/notifications_dropdown'; import NotificationsForm from '~/notifications_form'; import ProjectsList from '~/projects_list'; import ShortcutsNavigation from '~/shortcuts_navigation'; -import initGroupsList from '../../../groups'; +import initGroupsList from '~/groups'; document.addEventListener('DOMContentLoaded', () => { const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup'); diff --git a/app/assets/javascripts/pages/help/index.js b/app/assets/javascripts/pages/help/index.js deleted file mode 100644 index 4cf8afc4b7e..00000000000 --- a/app/assets/javascripts/pages/help/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import VersionCheckImage from '../../version_check_image'; - -export default () => VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); diff --git a/app/assets/javascripts/pages/help/index/index.js b/app/assets/javascripts/pages/help/index/index.js new file mode 100644 index 00000000000..05c81fc618b --- /dev/null +++ b/app/assets/javascripts/pages/help/index/index.js @@ -0,0 +1,7 @@ +import VersionCheckImage from '~/version_check_image'; +import docs from '~/docs/docs_bundle'; + +document.addEventListener('DOMContentLoaded', () => { + docs(); + VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); +}); diff --git a/app/assets/javascripts/pages/help/show/index.js b/app/assets/javascripts/pages/help/show/index.js new file mode 100644 index 00000000000..ec426a850b6 --- /dev/null +++ b/app/assets/javascripts/pages/help/show/index.js @@ -0,0 +1,3 @@ +import initHelp from '~/help/help'; + +document.addEventListener('DOMContentLoaded', initHelp); diff --git a/app/assets/javascripts/pages/help/ui/index.js b/app/assets/javascripts/pages/help/ui/index.js new file mode 100644 index 00000000000..709ca2f3828 --- /dev/null +++ b/app/assets/javascripts/pages/help/ui/index.js @@ -0,0 +1,3 @@ +import initUIKit from '~/ui_development_kit'; + +document.addEventListener('DOMContentLoaded', initUIKit); diff --git a/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js index 5defea104d4..68d4c1f049f 100644 --- a/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js +++ b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js @@ -1,3 +1,3 @@ -import UsersSelect from '../../../../users_select'; +import UsersSelect from '~/users_select'; -export default () => new UsersSelect(); +document.addEventListener('DOMContentLoaded', () => new UsersSelect()); diff --git a/app/assets/javascripts/pages/import/gitlab_projects/new/index.js b/app/assets/javascripts/pages/import/gitlab_projects/new/index.js new file mode 100644 index 00000000000..bb86f72b95b --- /dev/null +++ b/app/assets/javascripts/pages/import/gitlab_projects/new/index.js @@ -0,0 +1,3 @@ +import initGitLabImportProject from '~/projects/project_import_gitlab_project'; + +document.addEventListener('DOMContentLoaded', initGitLabImportProject); diff --git a/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js b/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js index 7aa5be0d5b9..b2a896a3265 100644 --- a/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js +++ b/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js @@ -2,8 +2,10 @@ import Milestone from '~/milestone'; import Sidebar from '~/right_sidebar'; +import MountMilestoneSidebar from '~/sidebar/mount_milestone_sidebar'; export default () => { new Milestone(); new Sidebar(); + new MountMilestoneSidebar(); }; diff --git a/app/assets/javascripts/pages/omniauth_callbacks/index.js b/app/assets/javascripts/pages/omniauth_callbacks/index.js index 54f4e56359a..c2c069d1ca8 100644 --- a/app/assets/javascripts/pages/omniauth_callbacks/index.js +++ b/app/assets/javascripts/pages/omniauth_callbacks/index.js @@ -1,5 +1,3 @@ import initU2F from '../../shared/sessions/u2f'; -export default () => { - initU2F(); -}; +document.addEventListener('DOMContentLoaded', initU2F); diff --git a/app/assets/javascripts/pages/profiles/accounts/show/index.js b/app/assets/javascripts/pages/profiles/accounts/show/index.js new file mode 100644 index 00000000000..96c3d725780 --- /dev/null +++ b/app/assets/javascripts/pages/profiles/accounts/show/index.js @@ -0,0 +1,3 @@ +import initProfileAccount from '~/profile/account'; + +document.addEventListener('DOMContentLoaded', initProfileAccount); diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js new file mode 100644 index 00000000000..c52ad7bc335 --- /dev/null +++ b/app/assets/javascripts/pages/profiles/index.js @@ -0,0 +1,16 @@ +import '~/profile/gl_crop'; +import Profile from '~/profile/profile'; + +document.addEventListener('DOMContentLoaded', () => { + $(document).on('input.ssh_key', '#key_key', function () { // eslint-disable-line func-names + const $title = $('#key_title'); + const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); + + // Extract the SSH Key title from its comment + if (comment && comment.length > 1) { + $title.val(comment[1]).change(); + } + }); + + new Profile(); // eslint-disable-line no-new +}); diff --git a/app/assets/javascripts/pages/profiles/index/index.js b/app/assets/javascripts/pages/profiles/index/index.js index 90eed38777a..9bd430f4f11 100644 --- a/app/assets/javascripts/pages/profiles/index/index.js +++ b/app/assets/javascripts/pages/profiles/index/index.js @@ -1,7 +1,7 @@ import NotificationsForm from '../../../notifications_form'; import notificationsDropdown from '../../../notifications_dropdown'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new NotificationsForm(); // eslint-disable-line no-new notificationsDropdown(); -}; +}); diff --git a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js index 030328a1363..78a5c4c27be 100644 --- a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js +++ b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js @@ -1,3 +1,3 @@ -import DueDateSelectors from '../../../due_date_select'; +import DueDateSelectors from '~/due_date_select'; -export default () => new DueDateSelectors(); +document.addEventListener('DOMContentLoaded', () => new DueDateSelectors()); diff --git a/app/assets/javascripts/pages/projects/activity/index.js b/app/assets/javascripts/pages/projects/activity/index.js index 7af95127fd5..5543ad82428 100644 --- a/app/assets/javascripts/pages/projects/activity/index.js +++ b/app/assets/javascripts/pages/projects/activity/index.js @@ -1,7 +1,7 @@ import Activities from '~/activities'; import ShortcutsNavigation from '~/shortcuts_navigation'; -export default function () { +document.addEventListener('DOMContentLoaded', () => { new Activities(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new -} +}); diff --git a/app/assets/javascripts/pages/projects/artifacts/browse/index.js b/app/assets/javascripts/pages/projects/artifacts/browse/index.js index 02456071086..ea7458fe9b8 100644 --- a/app/assets/javascripts/pages/projects/artifacts/browse/index.js +++ b/app/assets/javascripts/pages/projects/artifacts/browse/index.js @@ -1,7 +1,7 @@ import BuildArtifacts from '~/build_artifacts'; import ShortcutsNavigation from '~/shortcuts_navigation'; -export default function () { +document.addEventListener('DOMContentLoaded', () => { new ShortcutsNavigation(); // eslint-disable-line no-new new BuildArtifacts(); // eslint-disable-line no-new -} +}); diff --git a/app/assets/javascripts/pages/projects/artifacts/file/index.js b/app/assets/javascripts/pages/projects/artifacts/file/index.js index 4cd67ac76e3..8484e5e9848 100644 --- a/app/assets/javascripts/pages/projects/artifacts/file/index.js +++ b/app/assets/javascripts/pages/projects/artifacts/file/index.js @@ -1,7 +1,7 @@ import BlobViewer from '~/blob/viewer/index'; import ShortcutsNavigation from '~/shortcuts_navigation'; -export default function () { +document.addEventListener('DOMContentLoaded', () => { new ShortcutsNavigation(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new -} +}); diff --git a/app/assets/javascripts/pages/projects/blame/show/index.js b/app/assets/javascripts/pages/projects/blame/show/index.js index 480357a309c..80d0bff92fa 100644 --- a/app/assets/javascripts/pages/projects/blame/show/index.js +++ b/app/assets/javascripts/pages/projects/blame/show/index.js @@ -1,3 +1,3 @@ import initBlob from '~/pages/projects/init_blob'; -export default initBlob; +document.addEventListener('DOMContentLoaded', initBlob); diff --git a/app/assets/javascripts/pages/projects/blob/edit/index.js b/app/assets/javascripts/pages/projects/blob/edit/index.js new file mode 100644 index 00000000000..189053f3ed7 --- /dev/null +++ b/app/assets/javascripts/pages/projects/blob/edit/index.js @@ -0,0 +1,3 @@ +import initBlobBundle from '~/blob_edit/blob_bundle'; + +document.addEventListener('DOMContentLoaded', initBlobBundle); diff --git a/app/assets/javascripts/pages/projects/blob/new/index.js b/app/assets/javascripts/pages/projects/blob/new/index.js new file mode 100644 index 00000000000..189053f3ed7 --- /dev/null +++ b/app/assets/javascripts/pages/projects/blob/new/index.js @@ -0,0 +1,3 @@ +import initBlobBundle from '~/blob_edit/blob_bundle'; + +document.addEventListener('DOMContentLoaded', initBlobBundle); diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js index a3eeb1cefb6..26cbb279d4a 100644 --- a/app/assets/javascripts/pages/projects/blob/show/index.js +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -1,7 +1,7 @@ import BlobViewer from '~/blob/viewer/index'; import initBlob from '~/pages/projects/init_blob'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new BlobViewer(); // eslint-disable-line no-new initBlob(); -}; +}); diff --git a/app/assets/javascripts/pages/projects/boards/index.js b/app/assets/javascripts/pages/projects/boards/index.js index 3aeeedbb45d..5cfe8723204 100644 --- a/app/assets/javascripts/pages/projects/boards/index.js +++ b/app/assets/javascripts/pages/projects/boards/index.js @@ -1,7 +1,9 @@ import UsersSelect from '~/users_select'; import ShortcutsNavigation from '~/shortcuts_navigation'; +import initBoards from '~/boards'; document.addEventListener('DOMContentLoaded', () => { new UsersSelect(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new + initBoards(); }); diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js index cee0f19bf2a..8fa266a37ce 100644 --- a/app/assets/javascripts/pages/projects/branches/index/index.js +++ b/app/assets/javascripts/pages/projects/branches/index/index.js @@ -1,7 +1,7 @@ import AjaxLoadingSpinner from '~/ajax_loading_spinner'; import DeleteModal from '~/branches/branches_delete_modal'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { AjaxLoadingSpinner.init(); new DeleteModal(); // eslint-disable-line no-new -}; +}); diff --git a/app/assets/javascripts/pages/projects/branches/new/index.js b/app/assets/javascripts/pages/projects/branches/new/index.js index ae5e033e97e..d32d5c6cb29 100644 --- a/app/assets/javascripts/pages/projects/branches/new/index.js +++ b/app/assets/javascripts/pages/projects/branches/new/index.js @@ -1,3 +1,5 @@ import NewBranchForm from '~/new_branch_form'; -export default () => new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)); +document.addEventListener('DOMContentLoaded', () => ( + new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)) +)); diff --git a/app/assets/javascripts/pages/projects/clusters/destroy/index.js b/app/assets/javascripts/pages/projects/clusters/destroy/index.js new file mode 100644 index 00000000000..8001d2dd1da --- /dev/null +++ b/app/assets/javascripts/pages/projects/clusters/destroy/index.js @@ -0,0 +1,5 @@ +import ClustersBundle from '~/clusters/clusters_bundle'; + +document.addEventListener('DOMContentLoaded', () => { + new ClustersBundle(); // eslint-disable-line no-new +}); diff --git a/app/assets/javascripts/pages/projects/clusters/index/index.js b/app/assets/javascripts/pages/projects/clusters/index/index.js index d531ab81dc7..e4b8baede58 100644 --- a/app/assets/javascripts/pages/projects/clusters/index/index.js +++ b/app/assets/javascripts/pages/projects/clusters/index/index.js @@ -1,5 +1,5 @@ import ClustersIndex from '~/clusters/clusters_index'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new ClustersIndex(); // eslint-disable-line no-new -}; +}); diff --git a/app/assets/javascripts/pages/projects/clusters/show/index.js b/app/assets/javascripts/pages/projects/clusters/show/index.js index 0458c02a66f..8001d2dd1da 100644 --- a/app/assets/javascripts/pages/projects/clusters/show/index.js +++ b/app/assets/javascripts/pages/projects/clusters/show/index.js @@ -1,5 +1,5 @@ import ClustersBundle from '~/clusters/clusters_bundle'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new ClustersBundle(); // eslint-disable-line no-new -}; +}); diff --git a/app/assets/javascripts/pages/projects/clusters/update/index.js b/app/assets/javascripts/pages/projects/clusters/update/index.js new file mode 100644 index 00000000000..8001d2dd1da --- /dev/null +++ b/app/assets/javascripts/pages/projects/clusters/update/index.js @@ -0,0 +1,5 @@ +import ClustersBundle from '~/clusters/clusters_bundle'; + +document.addEventListener('DOMContentLoaded', () => { + new ClustersBundle(); // eslint-disable-line no-new +}); diff --git a/app/assets/javascripts/pages/projects/commit/pipelines/index.js b/app/assets/javascripts/pages/projects/commit/pipelines/index.js index 523ad567021..cd923f13ce8 100644 --- a/app/assets/javascripts/pages/projects/commit/pipelines/index.js +++ b/app/assets/javascripts/pages/projects/commit/pipelines/index.js @@ -1,8 +1,10 @@ import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; +import initPipelines from '~/commit/pipelines/pipelines_bundle'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new MiniPipelineGraph({ container: '.js-commit-pipeline-graph', }).bindEvents(); $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); -}; + initPipelines(); +}); diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js index 5ac38e6f278..1aeed197385 100644 --- a/app/assets/javascripts/pages/projects/commit/show/index.js +++ b/app/assets/javascripts/pages/projects/commit/show/index.js @@ -5,9 +5,10 @@ import ShortcutsNavigation from '~/shortcuts_navigation'; import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; import initNotes from '~/init_notes'; import initChangesDropdown from '~/init_changes_dropdown'; +import initDiffNotes from '~/diff_notes/diff_notes_bundle'; import { fetchCommitMergeRequests } from '~/commit_merge_requests'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new Diff(); new ZenMode(); new ShortcutsNavigation(); @@ -19,4 +20,5 @@ export default () => { initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop); $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); fetchCommitMergeRequests(); -}; + initDiffNotes(); +}); diff --git a/app/assets/javascripts/pages/projects/commits/show/index.js b/app/assets/javascripts/pages/projects/commits/show/index.js index 90b5882a24f..3682020579b 100644 --- a/app/assets/javascripts/pages/projects/commits/show/index.js +++ b/app/assets/javascripts/pages/projects/commits/show/index.js @@ -2,8 +2,8 @@ import CommitsList from '~/commits'; import GpgBadges from '~/gpg_badges'; import ShortcutsNavigation from '~/shortcuts_navigation'; -export default () => { - CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit); +document.addEventListener('DOMContentLoaded', () => { + new CommitsList(document.querySelector('.js-project-commits-show').dataset.commitsLimit); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new GpgBadges.fetch(); -}; +}); diff --git a/app/assets/javascripts/pages/projects/compare/index.js b/app/assets/javascripts/pages/projects/compare/index.js index 890062eeee6..d1c78bd61db 100644 --- a/app/assets/javascripts/pages/projects/compare/index.js +++ b/app/assets/javascripts/pages/projects/compare/index.js @@ -1,5 +1,3 @@ import initCompareAutocomplete from '~/compare_autocomplete'; -export default () => { - initCompareAutocomplete(); -}; +document.addEventListener('DOMContentLoaded', initCompareAutocomplete); diff --git a/app/assets/javascripts/pages/projects/compare/show/index.js b/app/assets/javascripts/pages/projects/compare/show/index.js index 6b8d4503568..2b4fd3c47c0 100644 --- a/app/assets/javascripts/pages/projects/compare/show/index.js +++ b/app/assets/javascripts/pages/projects/compare/show/index.js @@ -1,8 +1,8 @@ import Diff from '~/diff'; import initChangesDropdown from '~/init_changes_dropdown'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new Diff(); // eslint-disable-line no-new const paddingTop = 16; initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop); -}; +}); diff --git a/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js b/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js new file mode 100644 index 00000000000..df58e9dd072 --- /dev/null +++ b/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js @@ -0,0 +1,3 @@ +import initCycleAnalytics from '~/cycle_analytics/cycle_analytics_bundle'; + +document.addEventListener('DOMContentLoaded', initCycleAnalytics); diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index 9edf36d66b1..064de22dfd6 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -4,11 +4,11 @@ import ProjectNew from '../shared/project_new'; import projectAvatar from '../shared/project_avatar'; import initProjectPermissionsSettings from '../shared/permissions'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new ProjectNew(); // eslint-disable-line no-new setupProjectEdit(); // Initialize expandable settings panels initSettingsPanels(); projectAvatar(); initProjectPermissionsSettings(); -}; +}); diff --git a/app/assets/javascripts/pages/projects/environments/folder/index.js b/app/assets/javascripts/pages/projects/environments/folder/index.js new file mode 100644 index 00000000000..5feaf944038 --- /dev/null +++ b/app/assets/javascripts/pages/projects/environments/folder/index.js @@ -0,0 +1,3 @@ +import initEnvironmentsFolderBundle from '~/environments/folder/environments_folder_bundle'; + +document.addEventListener('DOMContentLoaded', initEnvironmentsFolderBundle); diff --git a/app/assets/javascripts/pages/projects/environments/index.js b/app/assets/javascripts/pages/projects/environments/index.js new file mode 100644 index 00000000000..ace8af00ece --- /dev/null +++ b/app/assets/javascripts/pages/projects/environments/index.js @@ -0,0 +1,3 @@ +import initEnviroments from '~/environments/'; + +document.addEventListener('DOMContentLoaded', initEnviroments); diff --git a/app/assets/javascripts/pages/projects/environments/metrics/index.js b/app/assets/javascripts/pages/projects/environments/metrics/index.js index f4760cb2720..0b644780ad4 100644 --- a/app/assets/javascripts/pages/projects/environments/metrics/index.js +++ b/app/assets/javascripts/pages/projects/environments/metrics/index.js @@ -1,3 +1,3 @@ import monitoringBundle from '~/monitoring/monitoring_bundle'; -export default monitoringBundle; +document.addEventListener('DOMContentLoaded', monitoringBundle); diff --git a/app/assets/javascripts/pages/projects/find_file/show/index.js b/app/assets/javascripts/pages/projects/find_file/show/index.js index 42bde0ff779..23d857d69ec 100644 --- a/app/assets/javascripts/pages/projects/find_file/show/index.js +++ b/app/assets/javascripts/pages/projects/find_file/show/index.js @@ -1,7 +1,7 @@ import ProjectFindFile from '~/project_find_file'; import ShortcutsFindFile from '~/shortcuts_find_file'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { const findElement = document.querySelector('.js-file-finder'); const projectFindFile = new ProjectFindFile($('.file-finder-holder'), { url: findElement.dataset.fileFindUrl, @@ -9,4 +9,4 @@ export default () => { blobUrlTemplate: findElement.dataset.blobUrlTemplate, }); new ShortcutsFindFile(projectFindFile); // eslint-disable-line no-new -}; +}); diff --git a/app/assets/javascripts/pages/projects/forks/new/index.js b/app/assets/javascripts/pages/projects/forks/new/index.js index 7825eb01949..d80e27e9156 100644 --- a/app/assets/javascripts/pages/projects/forks/new/index.js +++ b/app/assets/javascripts/pages/projects/forks/new/index.js @@ -1,5 +1,3 @@ import ProjectFork from '~/project_fork'; -export default () => { - new ProjectFork(); // eslint-disable-line no-new -}; +document.addEventListener('DOMContentLoaded', () => new ProjectFork()); diff --git a/app/assets/javascripts/graphs/graphs_charts.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index ec6eab34989..42df19c2968 100644 --- a/app/assets/javascripts/graphs/graphs_charts.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -1,4 +1,4 @@ -import Chart from 'vendor/Chart'; +import Chart from 'chart.js'; import _ from 'underscore'; document.addEventListener('DOMContentLoaded', () => { diff --git a/app/assets/javascripts/graphs/graphs_show.js b/app/assets/javascripts/pages/projects/graphs/show/index.js index b670e907a5c..f516ff20995 100644 --- a/app/assets/javascripts/graphs/graphs_show.js +++ b/app/assets/javascripts/pages/projects/graphs/show/index.js @@ -1,6 +1,6 @@ -import flash from '../flash'; -import { __ } from '../locale'; -import axios from '../lib/utils/axios_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; import ContributorsStatGraph from './stat_graph_contributors'; document.addEventListener('DOMContentLoaded', () => { diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js index 151a4ce012c..9ac0b4c07e5 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js @@ -1,9 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, no-shadow */ import _ from 'underscore'; +import { n__, s__, createDateTimeFormat, sprintf } from '~/locale'; import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; import ContributorsStatGraphUtil from './stat_graph_contributors_util'; -import { n__, s__, createDateTimeFormat, sprintf } from '../locale'; export default (function() { function ContributorsStatGraph() { diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js index 9a4012232a0..6ffaa277a0a 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js @@ -7,7 +7,7 @@ import { axisLeft, axisBottom } from 'd3-axis'; import { area } from 'd3-shape'; import { brushX } from 'd3-brush'; import { timeParse } from 'd3-time-format'; -import { dateTickFormat } from '../lib/utils/tick_formats'; +import { dateTickFormat } from '~/lib/utils/tick_formats'; const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse }; diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js index 77135ad1f0e..77135ad1f0e 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js diff --git a/app/assets/javascripts/pages/projects/imports/show/index.js b/app/assets/javascripts/pages/projects/imports/show/index.js index 378f7b3f38b..d5f92baf054 100644 --- a/app/assets/javascripts/pages/projects/imports/show/index.js +++ b/app/assets/javascripts/pages/projects/imports/show/index.js @@ -1,5 +1,5 @@ import ProjectImport from '~/project_import'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new ProjectImport(); // eslint-disable-line no-new -}; +}); diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index 9b1d52692a3..de1e13de7e9 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -1,7 +1,7 @@ import Project from './project'; import ShortcutsNavigation from '../../shortcuts_navigation'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new Project(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new -}; +}); diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js index 26f0ad46114..82143fa875a 100644 --- a/app/assets/javascripts/pages/projects/init_blob.js +++ b/app/assets/javascripts/pages/projects/init_blob.js @@ -3,6 +3,7 @@ import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater'; import ShortcutsNavigation from '~/shortcuts_navigation'; import ShortcutsBlob from '~/shortcuts_blob'; import BlobForkSuggestion from '~/blob/blob_fork_suggestion'; +import initBlobBundle from '~/blob_edit/blob_bundle'; export default () => { new LineHighlighter(); // eslint-disable-line no-new @@ -30,4 +31,6 @@ export default () => { suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'), actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'), }).init(); + + initBlobBundle(); }; diff --git a/app/assets/javascripts/pages/projects/issues/edit/index.js b/app/assets/javascripts/pages/projects/issues/edit/index.js index 7f27f379d8c..ffc84dc106b 100644 --- a/app/assets/javascripts/pages/projects/issues/edit/index.js +++ b/app/assets/javascripts/pages/projects/issues/edit/index.js @@ -1,5 +1,3 @@ import initForm from '../form'; -export default () => { - initForm(); -}; +document.addEventListener('DOMContentLoaded', initForm); diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js index 39c043edc38..70fdb0ef40d 100644 --- a/app/assets/javascripts/pages/projects/issues/index/index.js +++ b/app/assets/javascripts/pages/projects/issues/index/index.js @@ -8,7 +8,9 @@ import { FILTERED_SEARCH } from '~/pages/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants'; document.addEventListener('DOMContentLoaded', () => { - initFilteredSearch(FILTERED_SEARCH.ISSUES); + initFilteredSearch({ + page: FILTERED_SEARCH.ISSUES, + }); new IssuableIndex(ISSUABLE_INDEX.ISSUE); new ShortcutsNavigation(); diff --git a/app/assets/javascripts/pages/projects/issues/new/index.js b/app/assets/javascripts/pages/projects/issues/new/index.js index 7f27f379d8c..ffc84dc106b 100644 --- a/app/assets/javascripts/pages/projects/issues/new/index.js +++ b/app/assets/javascripts/pages/projects/issues/new/index.js @@ -1,5 +1,3 @@ import initForm from '../form'; -export default () => { - initForm(); -}; +document.addEventListener('DOMContentLoaded', initForm); diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js new file mode 100644 index 00000000000..500fbd27340 --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -0,0 +1,13 @@ +import initIssuableSidebar from '~/init_issuable_sidebar'; +import Issue from '~/issue'; +import ShortcutsIssuable from '~/shortcuts_issuable'; +import ZenMode from '~/zen_mode'; +import '~/notes/index'; +import '~/issue_show/index'; + +export default function () { + new Issue(); // eslint-disable-line no-new + new ShortcutsIssuable(); // eslint-disable-line no-new + new ZenMode(); // eslint-disable-line no-new + initIssuableSidebar(); +} diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js index da312c1f1b7..7968dfd7a12 100644 --- a/app/assets/javascripts/pages/projects/issues/show/index.js +++ b/app/assets/javascripts/pages/projects/issues/show/index.js @@ -1,13 +1,7 @@ -/* eslint-disable no-new */ - -import initIssuableSidebar from '~/init_issuable_sidebar'; -import Issue from '~/issue'; -import ShortcutsIssuable from '~/shortcuts_issuable'; -import ZenMode from '~/zen_mode'; +import initSidebarBundle from '~/sidebar/sidebar_bundle'; +import initShow from '../show'; document.addEventListener('DOMContentLoaded', () => { - new Issue(); - new ShortcutsIssuable(); - new ZenMode(); - initIssuableSidebar(); + initShow(); + initSidebarBundle(); }); diff --git a/app/assets/javascripts/pages/projects/jobs/show/index.js b/app/assets/javascripts/pages/projects/jobs/show/index.js new file mode 100644 index 00000000000..3626f3ffec6 --- /dev/null +++ b/app/assets/javascripts/pages/projects/jobs/show/index.js @@ -0,0 +1,3 @@ +import initJobDetails from '~/jobs/job_details_bundle'; + +document.addEventListener('DOMContentLoaded', initJobDetails); diff --git a/app/assets/javascripts/pages/projects/labels/edit/index.js b/app/assets/javascripts/pages/projects/labels/edit/index.js index 72c5e4744ac..fa81ad914ba 100644 --- a/app/assets/javascripts/pages/projects/labels/edit/index.js +++ b/app/assets/javascripts/pages/projects/labels/edit/index.js @@ -1,3 +1,3 @@ import Labels from '~/labels'; -export default () => new Labels(); +document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js index 018345fa112..6e45de2a724 100644 --- a/app/assets/javascripts/pages/projects/labels/index/index.js +++ b/app/assets/javascripts/pages/projects/labels/index/index.js @@ -1,3 +1,3 @@ import initLabels from '~/init_labels'; -export default initLabels; +document.addEventListener('DOMContentLoaded', initLabels); diff --git a/app/assets/javascripts/pages/projects/labels/new/index.js b/app/assets/javascripts/pages/projects/labels/new/index.js index 72c5e4744ac..fa81ad914ba 100644 --- a/app/assets/javascripts/pages/projects/labels/new/index.js +++ b/app/assets/javascripts/pages/projects/labels/new/index.js @@ -1,3 +1,3 @@ import Labels from '~/labels'; -export default () => new Labels(); +document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js b/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js new file mode 100644 index 00000000000..28641104c58 --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js @@ -0,0 +1,7 @@ +import initSidebarBundle from '~/sidebar/sidebar_bundle'; +import initMergeConflicts from '~/merge_conflicts/merge_conflicts_bundle'; + +document.addEventListener('DOMContentLoaded', () => { + initSidebarBundle(); + initMergeConflicts(); +}); diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/index.js index 734d01ae6f2..febfecebbd2 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/index.js @@ -1,3 +1,3 @@ import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request'; -export default initMergeRequest; +document.addEventListener('DOMContentLoaded', initMergeRequest); diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js index ccd0b54c5ed..6c9afddefac 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js @@ -1,7 +1,8 @@ import Compare from '~/compare'; import MergeRequest from '~/merge_request'; +import initPipelines from '~/commit/pipelines/pipelines_bundle'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); if (mrNewCompareNode) { new Compare({ // eslint-disable-line no-new @@ -14,5 +15,6 @@ export default () => { new MergeRequest({ // eslint-disable-line no-new action: mrNewSubmitNode.dataset.mrSubmitAction, }); + initPipelines(); } -}; +}); diff --git a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js index 734d01ae6f2..febfecebbd2 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js @@ -1,3 +1,3 @@ import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request'; -export default initMergeRequest; +document.addEventListener('DOMContentLoaded', initMergeRequest); diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js index adadbf28e49..a7aa616319f 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js @@ -6,7 +6,9 @@ import { FILTERED_SEARCH } from '~/pages/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants'; document.addEventListener('DOMContentLoaded', () => { - initFilteredSearch(FILTERED_SEARCH.MERGE_REQUESTS); + initFilteredSearch({ + page: FILTERED_SEARCH.MERGE_REQUESTS, + }); new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js new file mode 100644 index 00000000000..28d8761b502 --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js @@ -0,0 +1,32 @@ +import MergeRequest from '~/merge_request'; +import ZenMode from '~/zen_mode'; +import initNotes from '~/init_notes'; +import initIssuableSidebar from '~/init_issuable_sidebar'; +import initDiffNotes from '~/diff_notes/diff_notes_bundle'; +import ShortcutsIssuable from '~/shortcuts_issuable'; +import Diff from '~/diff'; +import { handleLocationHash } from '~/lib/utils/common_utils'; +import howToMerge from '~/how_to_merge'; +import initPipelines from '~/commit/pipelines/pipelines_bundle'; +import initWidget from '../../../vue_merge_request_widget'; + +export default function () { + new Diff(); // eslint-disable-line no-new + new ZenMode(); // eslint-disable-line no-new + + initIssuableSidebar(); + initNotes(); + initDiffNotes(); + initPipelines(); + + const mrShowNode = document.querySelector('.merge-request'); + + window.mergeRequest = new MergeRequest({ + action: mrShowNode.dataset.mrAction, + }); + + new ShortcutsIssuable(true); // eslint-disable-line no-new + handleLocationHash(); + howToMerge(); + initWidget(); +} diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js new file mode 100644 index 00000000000..3e72f7a6f37 --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js @@ -0,0 +1,7 @@ +import initSidebarBundle from '~/sidebar/sidebar_bundle'; +import initShow from '../init_merge_request_show'; + +document.addEventListener('DOMContentLoaded', () => { + initShow(); + initSidebarBundle(); +}); diff --git a/app/assets/javascripts/pages/projects/milestones/edit/index.js b/app/assets/javascripts/pages/projects/milestones/edit/index.js index 10e3979a36e..9a4ebf9890d 100644 --- a/app/assets/javascripts/pages/projects/milestones/edit/index.js +++ b/app/assets/javascripts/pages/projects/milestones/edit/index.js @@ -1,3 +1,3 @@ import initForm from '../../../../shared/milestones/form'; -export default () => initForm(); +document.addEventListener('DOMContentLoaded', () => initForm()); diff --git a/app/assets/javascripts/pages/projects/milestones/index/index.js b/app/assets/javascripts/pages/projects/milestones/index/index.js index 8fb4d83d8a3..38789365a67 100644 --- a/app/assets/javascripts/pages/projects/milestones/index/index.js +++ b/app/assets/javascripts/pages/projects/milestones/index/index.js @@ -1,3 +1,3 @@ import milestones from '~/pages/milestones/shared'; -export default milestones; +document.addEventListener('DOMContentLoaded', milestones); diff --git a/app/assets/javascripts/pages/projects/milestones/new/index.js b/app/assets/javascripts/pages/projects/milestones/new/index.js index 10e3979a36e..9a4ebf9890d 100644 --- a/app/assets/javascripts/pages/projects/milestones/new/index.js +++ b/app/assets/javascripts/pages/projects/milestones/new/index.js @@ -1,3 +1,3 @@ import initForm from '../../../../shared/milestones/form'; -export default () => initForm(); +document.addEventListener('DOMContentLoaded', () => initForm()); diff --git a/app/assets/javascripts/pages/projects/milestones/show/index.js b/app/assets/javascripts/pages/projects/milestones/show/index.js index 35b5c9c2ced..84a52421598 100644 --- a/app/assets/javascripts/pages/projects/milestones/show/index.js +++ b/app/assets/javascripts/pages/projects/milestones/show/index.js @@ -1,7 +1,7 @@ import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; import milestones from '~/pages/milestones/shared'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { initMilestonesShow(); milestones(); -}; +}); diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/pages/projects/network/network.js index a3fd22aff2a..7354243e4c8 100644 --- a/app/assets/javascripts/network/network.js +++ b/app/assets/javascripts/pages/projects/network/network.js @@ -1,6 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */ -import BranchGraph from './branch_graph'; +import BranchGraph from '../../../network/branch_graph'; export default (function() { function Network(opts) { diff --git a/app/assets/javascripts/pages/projects/network/show/index.js b/app/assets/javascripts/pages/projects/network/show/index.js new file mode 100644 index 00000000000..e7dfd2d0128 --- /dev/null +++ b/app/assets/javascripts/pages/projects/network/show/index.js @@ -0,0 +1,16 @@ +import ShortcutsNetwork from '../../../../shortcuts_network'; +import Network from '../network'; + +document.addEventListener('DOMContentLoaded', () => { + if (!$('.network-graph').length) return; + + const networkGraph = new Network({ + url: $('.network-graph').attr('data-url'), + commit_url: $('.network-graph').attr('data-commit-url'), + ref: $('.network-graph').attr('data-ref'), + commit_id: $('.network-graph').attr('data-commit-id'), + }); + + // eslint-disable-next-line no-new + new ShortcutsNetwork(networkGraph.branch_graph); +}); diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js index 71c49deb9d0..ea6fd961393 100644 --- a/app/assets/javascripts/pages/projects/new/index.js +++ b/app/assets/javascripts/pages/projects/new/index.js @@ -2,8 +2,8 @@ import ProjectNew from '../shared/project_new'; import initProjectVisibilitySelector from '../../../project_visibility'; import initProjectNew from '../../../projects/project_new'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new ProjectNew(); // eslint-disable-line no-new initProjectVisibilitySelector(); initProjectNew.bindEvents(); -}; +}); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js new file mode 100644 index 00000000000..d65be6bc69e --- /dev/null +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js @@ -0,0 +1,3 @@ +import initForm from '../shared/init_form'; + +document.addEventListener('DOMContentLoaded', initForm); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js new file mode 100644 index 00000000000..d65be6bc69e --- /dev/null +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js @@ -0,0 +1,3 @@ +import initForm from '../shared/init_form'; + +document.addEventListener('DOMContentLoaded', initForm); diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js index a6c945e22b0..544360dcd51 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import PipelineSchedulesCallout from './components/pipeline_schedules_callout.vue'; +import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue'; document.addEventListener('DOMContentLoaded', () => new Vue({ el: '#pipeline-schedules-callout', diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js new file mode 100644 index 00000000000..d65be6bc69e --- /dev/null +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js @@ -0,0 +1,3 @@ +import initForm from '../shared/init_form'; + +document.addEventListener('DOMContentLoaded', initForm); diff --git a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue index 2d18fa2044b..2d18fa2044b 100644 --- a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue index aa04a0ac47a..77508e62cef 100644 --- a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue @@ -1,7 +1,7 @@ <script> import Vue from 'vue'; import Cookies from 'js-cookie'; - import Translate from '../../vue_shared/translate'; + import Translate from '../../../../../vue_shared/translate'; import illustrationSvg from '../icons/intro_illustration.svg'; Vue.use(Translate); diff --git a/app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js index 0c3926d76b5..0c3926d76b5 100644 --- a/app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js diff --git a/app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js index 95ed9c7dc21..95ed9c7dc21 100644 --- a/app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js diff --git a/app/assets/javascripts/pipeline_schedules/icons/intro_illustration.svg b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg index 26d1ff97b3e..26d1ff97b3e 100644 --- a/app/assets/javascripts/pipeline_schedules/icons/intro_illustration.svg +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js index 0b1a81bae13..cfd30d6053f 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js @@ -1,10 +1,10 @@ import Vue from 'vue'; -import Translate from '../vue_shared/translate'; -import GlFieldErrors from '../gl_field_errors'; +import Translate from '../../../../vue_shared/translate'; +import GlFieldErrors from '../../../../gl_field_errors'; import intervalPatternInput from './components/interval_pattern_input.vue'; import TimezoneDropdown from './components/timezone_dropdown'; import TargetBranchDropdown from './components/target_branch_dropdown'; -import setupNativeFormVariableList from '../ci_variable_list/native_form_variable_list'; +import setupNativeFormVariableList from '../../../../ci_variable_list/native_form_variable_list'; Vue.use(Translate); @@ -27,7 +27,7 @@ function initIntervalPatternInput() { }); } -document.addEventListener('DOMContentLoaded', () => { +export default () => { /* Most of the form is written in haml, but for fields with more complex behaviors, * you should mount individual Vue components here. If at some point components need * to share state, it may make sense to refactor the whole form to Vue */ @@ -46,4 +46,4 @@ document.addEventListener('DOMContentLoaded', () => { container: $('.js-ci-variable-list-section'), formField: 'schedule', }); -}); +}; diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js new file mode 100644 index 00000000000..d65be6bc69e --- /dev/null +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js @@ -0,0 +1,3 @@ +import initForm from '../shared/init_form'; + +document.addEventListener('DOMContentLoaded', initForm); diff --git a/app/assets/javascripts/pages/projects/pipelines/builds/index.js b/app/assets/javascripts/pages/projects/pipelines/builds/index.js index 060a78b427e..7a57e417b41 100644 --- a/app/assets/javascripts/pages/projects/pipelines/builds/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/builds/index.js @@ -1,16 +1,7 @@ -import Pipelines from '../../../../pipelines'; +import initPipelineDetails from '~/pipelines/pipeline_details_bundle'; +import initPipelines from '../init_pipelines'; -export default () => { - const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; - const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`; - - new Pipelines({ // eslint-disable-line no-new - initTabs: true, - pipelineStatusUrl, - tabsOptions: { - action: controllerAction, - defaultAction: 'pipelines', - parentEl: '.pipelines-tabs', - }, - }); -}; +document.addEventListener('DOMContentLoaded', () => { + initPipelines(); + initPipelineDetails(); +}); diff --git a/app/assets/javascripts/pages/projects/pipelines/charts/index.js b/app/assets/javascripts/pages/projects/pipelines/charts/index.js new file mode 100644 index 00000000000..bb92f4e1459 --- /dev/null +++ b/app/assets/javascripts/pages/projects/pipelines/charts/index.js @@ -0,0 +1,56 @@ +import Chart from 'chart.js'; + +const options = { + scaleOverlay: true, + responsive: true, + maintainAspectRatio: false, +}; + +const buildChart = (chartScope) => { + const data = { + labels: chartScope.labels, + datasets: [{ + fillColor: '#707070', + strokeColor: '#707070', + pointColor: '#707070', + pointStrokeColor: '#EEE', + data: chartScope.totalValues, + }, + { + fillColor: '#1aaa55', + strokeColor: '#1aaa55', + pointColor: '#1aaa55', + pointStrokeColor: '#fff', + data: chartScope.successValues, + }, + ], + }; + const ctx = $(`#${chartScope.scope}Chart`).get(0).getContext('2d'); + + new Chart(ctx).Line(data, options); +}; + +document.addEventListener('DOMContentLoaded', () => { + const chartTimesData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML); + const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML); + const data = { + labels: chartTimesData.labels, + datasets: [{ + fillColor: 'rgba(220,220,220,0.5)', + strokeColor: 'rgba(220,220,220,1)', + barStrokeWidth: 1, + barValueSpacing: 1, + barDatasetSpacing: 1, + data: chartTimesData.values, + }], + }; + + if (window.innerWidth < 768) { + // Scale fonts if window width lower than 768px (iPad portrait) + options.scaleFontSize = 8; + } + + new Chart($('#build_timesChart').get(0).getContext('2d')).Bar(data, options); + + chartsData.forEach(scope => buildChart(scope)); +}); diff --git a/app/assets/javascripts/pages/projects/pipelines/failures/index.js b/app/assets/javascripts/pages/projects/pipelines/failures/index.js new file mode 100644 index 00000000000..fbe9824c34b --- /dev/null +++ b/app/assets/javascripts/pages/projects/pipelines/failures/index.js @@ -0,0 +1,3 @@ +import initPipelines from '../init_pipelines'; + +document.addEventListener('DOMContentLoaded', initPipelines); diff --git a/app/assets/javascripts/pipelines/pipelines_bundle.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js index ab5596e70f0..25dfa99ad9c 100644 --- a/app/assets/javascripts/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js @@ -1,7 +1,7 @@ import Vue from 'vue'; -import PipelinesStore from './stores/pipelines_store'; -import pipelinesComponent from './components/pipelines.vue'; -import Translate from '../vue_shared/translate'; +import PipelinesStore from '../../../../pipelines/stores/pipelines_store'; +import pipelinesComponent from '../../../../pipelines/components/pipelines.vue'; +import Translate from '../../../../vue_shared/translate'; Vue.use(Translate); diff --git a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js new file mode 100644 index 00000000000..94dfeb96e8c --- /dev/null +++ b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js @@ -0,0 +1,16 @@ +import Pipelines from '~/pipelines'; + +export default () => { + const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; + const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`; + + new Pipelines({ // eslint-disable-line no-new + initTabs: true, + pipelineStatusUrl, + tabsOptions: { + action: controllerAction, + defaultAction: 'pipelines', + parentEl: '.pipelines-tabs', + }, + }); +}; diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js index c54cc62bf05..da20bd995e9 100644 --- a/app/assets/javascripts/pages/projects/pipelines/new/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js @@ -1,5 +1,5 @@ -import NewBranchForm from '../../../../new_branch_form'; +import NewBranchForm from '~/new_branch_form'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new -}; +}); diff --git a/app/assets/javascripts/pages/projects/pipelines/show/index.js b/app/assets/javascripts/pages/projects/pipelines/show/index.js new file mode 100644 index 00000000000..7a57e417b41 --- /dev/null +++ b/app/assets/javascripts/pages/projects/pipelines/show/index.js @@ -0,0 +1,7 @@ +import initPipelineDetails from '~/pipelines/pipeline_details_bundle'; +import initPipelines from '../init_pipelines'; + +document.addEventListener('DOMContentLoaded', () => { + initPipelines(); + initPipelineDetails(); +}); diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index 863dac0d20e..d23ad9a92f4 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -50,7 +50,7 @@ export default class Project { Project.projectSelectDropdown(); } - static projectSelectDropdown () { + static projectSelectDropdown() { projectSelect(); $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val())); } @@ -71,7 +71,7 @@ export default class Project { selected = $dropdown.data('selected'); return $dropdown.glDropdown({ data(term, callback) { - axios.get($dropdown.data('refs-url'), { + axios.get($dropdown.data('refsUrl'), { params: { ref: $dropdown.data('ref'), search: term, @@ -84,8 +84,8 @@ export default class Project { filterable: true, filterRemote: true, filterByText: true, - inputFieldName: $dropdown.data('input-field-name'), - fieldName: $dropdown.data('field-name'), + inputFieldName: $dropdown.data('inputFieldName'), + fieldName: $dropdown.data('fieldName'), renderRow: function(ref) { var li = refListItem.cloneNode(false); diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js index f4643e7dba0..adbe744290a 100644 --- a/app/assets/javascripts/pages/projects/project_members/index.js +++ b/app/assets/javascripts/pages/projects/project_members/index.js @@ -3,10 +3,10 @@ import UsersSelect from '../../../users_select'; import groupsSelect from '../../../groups_select'; import Members from '../../../members'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { memberExpirationDate('.js-access-expiration-date-groups'); groupsSelect(); memberExpirationDate(); new Members(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new -}; +}); diff --git a/app/assets/javascripts/pages/projects/registry/repositories/index.js b/app/assets/javascripts/pages/projects/registry/repositories/index.js new file mode 100644 index 00000000000..35564754ee0 --- /dev/null +++ b/app/assets/javascripts/pages/projects/registry/repositories/index.js @@ -0,0 +1,3 @@ +import initRegistryImages from '~/registry/index'; + +document.addEventListener('DOMContentLoaded', initRegistryImages); diff --git a/app/assets/javascripts/pages/projects/releases/edit/index.js b/app/assets/javascripts/pages/projects/releases/edit/index.js index 3d997cdfff0..0bf53a8de09 100644 --- a/app/assets/javascripts/pages/projects/releases/edit/index.js +++ b/app/assets/javascripts/pages/projects/releases/edit/index.js @@ -1,3 +1,3 @@ import initForm from '~/pages/projects/init_form'; -export default initForm($('.release-form')); +document.addEventListener('DOMContentLoaded', () => initForm($('.release-form'))); diff --git a/app/assets/javascripts/pages/projects/services/edit/index.js b/app/assets/javascripts/pages/projects/services/edit/index.js new file mode 100644 index 00000000000..ba4b271f09e --- /dev/null +++ b/app/assets/javascripts/pages/projects/services/edit/index.js @@ -0,0 +1,13 @@ +import IntegrationSettingsForm from '~/integrations/integration_settings_form'; +import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics'; + +document.addEventListener('DOMContentLoaded', () => { + const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring'); + const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); + integrationSettingsForm.init(); + + if (prometheusSettingsWrapper) { + const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); + prometheusMetrics.loadActiveMetrics(); + } +}); diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js index a563d0f9961..6c2a785c0af 100644 --- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -2,7 +2,7 @@ import initSettingsPanels from '~/settings_panels'; import SecretValues from '~/behaviors/secret_values'; import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; -export default function () { +document.addEventListener('DOMContentLoaded', () => { // Initialize expandable settings panels initSettingsPanels(); @@ -22,4 +22,4 @@ export default function () { errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), saveEndpoint: variableListEl.dataset.saveEndpoint, }); -} +}); diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js index 83b5467fbc0..001128ead59 100644 --- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js @@ -1,3 +1,13 @@ +/* eslint-disable no-new */ + +import ProtectedTagCreate from '~/protected_tags/protected_tag_create'; +import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list'; import initSettingsPanels from '~/settings_panels'; +import initDeployKeys from '~/deploy_keys'; -export default initSettingsPanels; +document.addEventListener('DOMContentLoaded', () => { + new ProtectedTagCreate(); + new ProtectedTagEditList(); + initDeployKeys(); + initSettingsPanels(); +}); diff --git a/app/assets/javascripts/pages/projects/snippets/edit/index.js b/app/assets/javascripts/pages/projects/snippets/edit/index.js index 9edb16dc73b..c15f798b630 100644 --- a/app/assets/javascripts/pages/projects/snippets/edit/index.js +++ b/app/assets/javascripts/pages/projects/snippets/edit/index.js @@ -1,3 +1,7 @@ +import initSnippet from '~/snippet/snippet_bundle'; import initForm from '~/pages/projects/init_form'; -export default initForm($('.snippet-form')); +document.addEventListener('DOMContentLoaded', () => { + initSnippet(); + initForm($('.snippet-form')); +}); diff --git a/app/assets/javascripts/pages/projects/snippets/new/index.js b/app/assets/javascripts/pages/projects/snippets/new/index.js index 9edb16dc73b..c15f798b630 100644 --- a/app/assets/javascripts/pages/projects/snippets/new/index.js +++ b/app/assets/javascripts/pages/projects/snippets/new/index.js @@ -1,3 +1,7 @@ +import initSnippet from '~/snippet/snippet_bundle'; import initForm from '~/pages/projects/init_form'; -export default initForm($('.snippet-form')); +document.addEventListener('DOMContentLoaded', () => { + initSnippet(); + initForm($('.snippet-form')); +}); diff --git a/app/assets/javascripts/pages/projects/snippets/show/index.js b/app/assets/javascripts/pages/projects/snippets/show/index.js index a3cf75c385b..a134599cb04 100644 --- a/app/assets/javascripts/pages/projects/snippets/show/index.js +++ b/app/assets/javascripts/pages/projects/snippets/show/index.js @@ -3,9 +3,9 @@ import ZenMode from '~/zen_mode'; import LineHighlighter from '../../../../line_highlighter'; import BlobViewer from '../../../../blob/viewer'; -export default function () { +document.addEventListener('DOMContentLoaded', () => { new LineHighlighter(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new initNotes(); new ZenMode(); // eslint-disable-line no-new -} +}); diff --git a/app/assets/javascripts/pages/projects/tags/new/index.js b/app/assets/javascripts/pages/projects/tags/new/index.js index dacc2875c8c..191c98b36bb 100644 --- a/app/assets/javascripts/pages/projects/tags/new/index.js +++ b/app/assets/javascripts/pages/projects/tags/new/index.js @@ -2,8 +2,8 @@ import RefSelectDropdown from '../../../../ref_select_dropdown'; import ZenMode from '../../../../zen_mode'; import GLForm from '../../../../gl_form'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new ZenMode(); // eslint-disable-line no-new new GLForm($('.tag-form'), true); // eslint-disable-line no-new new RefSelectDropdown($('.js-branch-select')); // eslint-disable-line no-new -}; +}); diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js index c4b3356e478..ed7d3f1747c 100644 --- a/app/assets/javascripts/pages/projects/tree/show/index.js +++ b/app/assets/javascripts/pages/projects/tree/show/index.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import initBlob from '~/blob_edit/blob_bundle'; import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; import TreeView from '../../../../tree'; import ShortcutsNavigation from '../../../../shortcuts_navigation'; @@ -6,7 +7,7 @@ import BlobViewer from '../../../../blob/viewer'; import NewCommitForm from '../../../../new_commit_form'; import { ajaxGet } from '../../../../lib/utils/common_utils'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new ShortcutsNavigation(); // eslint-disable-line no-new new TreeView(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new @@ -14,7 +15,8 @@ export default () => { $('#tree-slider').waitForImages(() => ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath)); - const commitPipelineStatusEl = document.getElementById('commit-pipeline-status'); + initBlob(); + const commitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status'); const statusLink = document.querySelector('.commit-actions .ci-status-link'); if (statusLink != null) { statusLink.remove(); @@ -33,5 +35,4 @@ export default () => { }, }); } -}; - +}); diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js index eb14c7a0e78..b9f8707fd6e 100644 --- a/app/assets/javascripts/pages/projects/wikis/index.js +++ b/app/assets/javascripts/pages/projects/wikis/index.js @@ -3,9 +3,9 @@ import ShortcutsWiki from '../../../shortcuts_wiki'; import ZenMode from '../../../zen_mode'; import GLForm from '../../../gl_form'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new Wikis(); // eslint-disable-line no-new new ShortcutsWiki(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new new GLForm($('.wiki-form'), true); // eslint-disable-line no-new -}; +}); diff --git a/app/assets/javascripts/pages/search/init_filtered_search.js b/app/assets/javascripts/pages/search/init_filtered_search.js index 44853636aea..57f08701a4f 100644 --- a/app/assets/javascripts/pages/search/init_filtered_search.js +++ b/app/assets/javascripts/pages/search/init_filtered_search.js @@ -1,7 +1,21 @@ -export default (page) => { - const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); +import FilteredSearchManager from '~/filtered_search/filtered_search_manager'; + +export default ({ + page, + filteredSearchTokenKeys, + isGroup, + isGroupAncestor, + stateFiltersSelector, +}) => { + const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search'); if (filteredSearchEnabled) { - const filteredSearchManager = new gl.FilteredSearchManager(page); + const filteredSearchManager = new FilteredSearchManager({ + page, + isGroup, + isGroupAncestor, + filteredSearchTokenKeys, + stateFiltersSelector, + }); filteredSearchManager.setup(); } }; diff --git a/app/assets/javascripts/pages/search/show/index.js b/app/assets/javascripts/pages/search/show/index.js index 4264c5c9dbe..85aaaa2c9da 100644 --- a/app/assets/javascripts/pages/search/show/index.js +++ b/app/assets/javascripts/pages/search/show/index.js @@ -1,3 +1,3 @@ import Search from './search'; -export default () => new Search(); +document.addEventListener('DOMContentLoaded', () => new Search()); diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js index dc621bc87c0..cf44e291199 100644 --- a/app/assets/javascripts/pages/search/show/search.js +++ b/app/assets/javascripts/pages/search/show/search.js @@ -9,7 +9,7 @@ export default class Search { this.searchInput = '.js-search-input'; this.searchClear = '.js-search-clear'; - this.groupId = $groupDropdown.data('group-id'); + this.groupId = $groupDropdown.data('groupId'); this.eventListeners(); $groupDropdown.glDropdown({ @@ -36,7 +36,7 @@ export default class Search { return obj.full_name; }, toggleLabel(obj) { - return `${($groupDropdown.data('default-label'))} ${obj.full_name}`; + return `${($groupDropdown.data('defaultLabel'))} ${obj.full_name}`; }, clicked: () => Search.submitSearch(), }); @@ -69,7 +69,7 @@ export default class Search { return obj.name_with_namespace; }, toggleLabel(obj) { - return `${($projectDropdown.data('default-label'))} ${obj.name_with_namespace}`; + return `${($projectDropdown.data('defaultLabel'))} ${obj.name_with_namespace}`; }, clicked: () => Search.submitSearch(), }); diff --git a/app/assets/javascripts/pages/sessions/index.js b/app/assets/javascripts/pages/sessions/index.js index 54f4e56359a..c2c069d1ca8 100644 --- a/app/assets/javascripts/pages/sessions/index.js +++ b/app/assets/javascripts/pages/sessions/index.js @@ -1,5 +1,3 @@ import initU2F from '../../shared/sessions/u2f'; -export default () => { - initU2F(); -}; +document.addEventListener('DOMContentLoaded', initU2F); diff --git a/app/assets/javascripts/pages/snippets/edit/index.js b/app/assets/javascripts/pages/snippets/edit/index.js index 9c664b5f1ff..d86e1632ae5 100644 --- a/app/assets/javascripts/pages/snippets/edit/index.js +++ b/app/assets/javascripts/pages/snippets/edit/index.js @@ -1,3 +1,7 @@ +import initSnippet from '~/snippet/snippet_bundle'; import form from '../form'; -export default form; +document.addEventListener('DOMContentLoaded', () => { + initSnippet(); + form(); +}); diff --git a/app/assets/javascripts/pages/snippets/new/index.js b/app/assets/javascripts/pages/snippets/new/index.js index 9c664b5f1ff..d86e1632ae5 100644 --- a/app/assets/javascripts/pages/snippets/new/index.js +++ b/app/assets/javascripts/pages/snippets/new/index.js @@ -1,3 +1,7 @@ +import initSnippet from '~/snippet/snippet_bundle'; import form from '../form'; -export default form; +document.addEventListener('DOMContentLoaded', () => { + initSnippet(); + form(); +}); diff --git a/app/assets/javascripts/pages/snippets/show/index.js b/app/assets/javascripts/pages/snippets/show/index.js index 04c9562bfbb..f548b9fad65 100644 --- a/app/assets/javascripts/pages/snippets/show/index.js +++ b/app/assets/javascripts/pages/snippets/show/index.js @@ -1,12 +1,11 @@ -/* eslint-disable no-new */ import LineHighlighter from '../../../line_highlighter'; import BlobViewer from '../../../blob/viewer'; import ZenMode from '../../../zen_mode'; import initNotes from '../../../init_notes'; -export default () => { - new LineHighlighter(); - new BlobViewer(); +document.addEventListener('DOMContentLoaded', () => { + new LineHighlighter(); // eslint-disable-line no-new + new BlobViewer(); // eslint-disable-line no-new initNotes(); - new ZenMode(); -}; + new ZenMode(); // eslint-disable-line no-new +}); diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index 57306322aa4..57306322aa4 100644 --- a/app/assets/javascripts/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js diff --git a/app/assets/javascripts/users/index.js b/app/assets/javascripts/pages/users/index.js index 9fd8452a2b6..899dcd42e37 100644 --- a/app/assets/javascripts/users/index.js +++ b/app/assets/javascripts/pages/users/index.js @@ -1,3 +1,4 @@ +import UserCallout from '~/user_callout'; import Cookies from 'js-cookie'; import UserTabs from './user_tabs'; @@ -22,4 +23,5 @@ document.addEventListener('DOMContentLoaded', () => { const page = $('body').attr('data-page'); const action = page.split(':')[1]; initUserProfile(action); + new UserCallout(); // eslint-disable-line no-new }); diff --git a/app/assets/javascripts/pages/users/show/index.js b/app/assets/javascripts/pages/users/show/index.js deleted file mode 100644 index f18f98b4e9a..00000000000 --- a/app/assets/javascripts/pages/users/show/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import UserCallout from '~/user_callout'; - -export default () => new UserCallout(); diff --git a/app/assets/javascripts/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index e13b9839a20..c1217623467 100644 --- a/app/assets/javascripts/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -1,9 +1,9 @@ -import axios from '../lib/utils/axios_utils'; -import Activities from '../activities'; +import axios from '~/lib/utils/axios_utils'; +import Activities from '~/activities'; +import { localTimeAgo } from '~/lib/utils/datetime_utility'; +import { __ } from '~/locale'; +import flash from '~/flash'; import ActivityCalendar from './activity_calendar'; -import { localTimeAgo } from '../lib/utils/datetime_utility'; -import { __ } from '../locale'; -import flash from '../flash'; /** * UserTabs diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue index 77553ca67cc..0cdffbde05b 100644 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -31,10 +31,13 @@ type: String, required: true, }, - confirmActionMessage: { + pipelineId: { + type: Number, + required: true, + }, + type: { type: String, - required: false, - default: '', + required: true, }, }, data() { @@ -47,18 +50,27 @@ return `btn ${this.cssClass}`; }, }, + created() { + // We're using eventHub to listen to the modal here instead of + // using props because it would would make the parent components + // much more complex to keep track of the loading state of each button + eventHub.$on('postAction', this.setLoading); + }, + beforeDestroy() { + eventHub.$off('postAction', this.setLoading); + }, methods: { onClick() { - if (this.confirmActionMessage !== '' && confirm(this.confirmActionMessage)) { - this.makeRequest(); - } else if (this.confirmActionMessage === '') { - this.makeRequest(); - } + eventHub.$emit('openConfirmationModal', { + pipelineId: this.pipelineId, + endpoint: this.endpoint, + type: this.type, + }); }, - makeRequest() { - this.isLoading = true; - - eventHub.$emit('postAction', this.endpoint); + setLoading(endpoint) { + if (endpoint === this.endpoint) { + this.isLoading = true; + } }, }, }; diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index a1f58580318..ab84711d4a2 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -52,7 +52,7 @@ </script> <template> <div class="build-content middle-block js-pipeline-graph"> - <div class="pipeline-visualization pipeline-graph"> + <div class="pipeline-visualization pipeline-graph pipeline-tab-content"> <div class="text-center"> <loading-icon v-if="isLoading" diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index e027f08ff5c..7adcf4017b8 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -7,7 +7,6 @@ jobComponent, dropdownJobComponent, }, - props: { title: { type: String, diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 6681b89e629..c9028952ddd 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -1,5 +1,8 @@ <script> + import modal from '~/vue_shared/components/modal.vue'; + import { s__, sprintf } from '~/locale'; import pipelinesTableRowComponent from './pipelines_table_row.vue'; + import eventHub from '../event_hub'; /** * Pipelines Table Component. @@ -9,6 +12,7 @@ export default { components: { pipelinesTableRowComponent, + modal, }, props: { pipelines: { @@ -29,6 +33,52 @@ required: true, }, }, + data() { + return { + pipelineId: '', + endpoint: '', + type: '', + }; + }, + computed: { + modalTitle() { + return this.type === 'stop' ? + sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), { + pipelineId: `'${this.pipelineId}'`, + }, false) : + sprintf(s__('Pipeline|Retry pipeline #%{pipelineId}?'), { + pipelineId: `'${this.pipelineId}'`, + }, false); + }, + modalText() { + return this.type === 'stop' ? + sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), { + pipelineId: `<strong>#${this.pipelineId}</strong>`, + }, false) : + sprintf(s__('Pipeline|You’re about to retry pipeline %{pipelineId}.'), { + pipelineId: `<strong>#${this.pipelineId}</strong>`, + }, false); + }, + primaryButtonLabel() { + return this.type === 'stop' ? s__('Pipeline|Stop pipeline') : s__('Pipeline|Retry pipeline'); + }, + }, + created() { + eventHub.$on('openConfirmationModal', this.setModalData); + }, + beforeDestroy() { + eventHub.$off('openConfirmationModal', this.setModalData); + }, + methods: { + setModalData(data) { + this.pipelineId = data.pipelineId; + this.endpoint = data.endpoint; + this.type = data.type; + }, + onSubmit() { + eventHub.$emit('postAction', this.endpoint); + }, + }, }; </script> <template> @@ -70,5 +120,20 @@ :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" /> + <modal + id="confirmation-modal" + :title="modalTitle" + :text="modalText" + kind="danger" + :primary-button-label="primaryButtonLabel" + @submit="onSubmit" + > + <template + slot="body" + slot-scope="props" + > + <p v-html="props.text"></p> + </template> + </modal> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index d0e4cf7ff40..4cbd67e0372 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -223,7 +223,8 @@ <div class="table-section section-10 commit-link"> <div class="table-mobile-header" - role="rowheader"> + role="rowheader" + > Status </div> <div class="table-mobile-content"> @@ -305,15 +306,22 @@ css-class="js-pipelines-retry-button btn-default btn-retry" title="Retry" icon="repeat" + :pipeline-id="pipeline.id" + data-toggle="modal" + data-target="#confirmation-modal" + type="retry" /> <async-button-component v-if="pipeline.flags.cancelable" :endpoint="pipeline.cancel_path" css-class="js-pipelines-cancel-button btn-remove" - title="Cancel" + title="Stop" icon="close" - confirm-action-message="Are you sure you want to cancel this pipeline?" + :pipeline-id="pipeline.id" + data-toggle="modal" + data-target="#confirmation-modal" + type="stop" /> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index 58806aa114a..ecf2b10486e 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -50,9 +50,7 @@ computed: { dropdownClass() { - return this.dropdownContent.length > 0 ? - 'js-builds-dropdown-container' : - 'js-builds-dropdown-loading'; + return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading'; }, triggerButtonClass() { diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index d88d280cb3f..6b26708148c 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -1,11 +1,15 @@ import Vue from 'vue'; -import Flash from '../flash'; -import PipelinesMediator from './pipeline_details_mediatior'; +import Flash from '~/flash'; +import Translate from '~/vue_shared/translate'; +import { __ } from '~/locale'; +import PipelinesMediator from './pipeline_details_mediator'; import pipelineGraph from './components/graph/graph_component.vue'; import pipelineHeader from './components/header_component.vue'; import eventHub from './event_hub'; -document.addEventListener('DOMContentLoaded', () => { +Vue.use(Translate); + +export default () => { const dataset = document.querySelector('.js-pipeline-details-vue').dataset; const mediator = new PipelinesMediator({ endpoint: dataset.endpoint }); @@ -54,7 +58,7 @@ document.addEventListener('DOMContentLoaded', () => { postAction(action) { this.mediator.service.postAction(action.path) .then(() => this.mediator.refreshPipeline()) - .catch(() => new Flash('An error occurred while making the request.')); + .catch(() => Flash(__('An error occurred while making the request.'))); }, }, render(createElement) { @@ -66,4 +70,4 @@ document.addEventListener('DOMContentLoaded', () => { }); }, }); -}); +}; diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js index 823ccd849f4..10f238fe73b 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js +++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js @@ -1,6 +1,7 @@ import Visibility from 'visibilityjs'; import Flash from '../flash'; import Poll from '../lib/utils/poll'; +import { __ } from '../locale'; import PipelineStore from './stores/pipeline_store'; import PipelineService from './services/pipeline_service'; @@ -47,7 +48,7 @@ export default class pipelinesMediator { errorCallback() { this.state.isLoading = false; - return new Flash('An error occurred while fetching the pipeline.'); + Flash(__('An error occurred while fetching the pipeline.')); } refreshPipeline() { diff --git a/app/assets/javascripts/pipelines/pipelines_charts.js b/app/assets/javascripts/pipelines/pipelines_charts.js deleted file mode 100644 index 821aa7e229f..00000000000 --- a/app/assets/javascripts/pipelines/pipelines_charts.js +++ /dev/null @@ -1,38 +0,0 @@ -import Chart from 'vendor/Chart'; - -document.addEventListener('DOMContentLoaded', () => { - const chartData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML); - const buildChart = (chartScope) => { - const data = { - labels: chartScope.labels, - datasets: [{ - fillColor: '#707070', - strokeColor: '#707070', - pointColor: '#707070', - pointStrokeColor: '#EEE', - data: chartScope.totalValues, - }, - { - fillColor: '#1aaa55', - strokeColor: '#1aaa55', - pointColor: '#1aaa55', - pointStrokeColor: '#fff', - data: chartScope.successValues, - }, - ], - }; - const ctx = $(`#${chartScope.scope}Chart`).get(0).getContext('2d'); - const options = { - scaleOverlay: true, - responsive: true, - maintainAspectRatio: false, - }; - if (window.innerWidth < 768) { - // Scale fonts if window width lower than 768px (iPad portrait) - options.scaleFontSize = 8; - } - new Chart(ctx).Line(data, options); - }; - - chartData.forEach(scope => buildChart(scope)); -}); diff --git a/app/assets/javascripts/pipelines/pipelines_times.js b/app/assets/javascripts/pipelines/pipelines_times.js deleted file mode 100644 index b5e7a0e53d9..00000000000 --- a/app/assets/javascripts/pipelines/pipelines_times.js +++ /dev/null @@ -1,27 +0,0 @@ -import Chart from 'vendor/Chart'; - -document.addEventListener('DOMContentLoaded', () => { - const chartData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML); - const data = { - labels: chartData.labels, - datasets: [{ - fillColor: 'rgba(220,220,220,0.5)', - strokeColor: 'rgba(220,220,220,1)', - barStrokeWidth: 1, - barValueSpacing: 1, - barDatasetSpacing: 1, - data: chartData.values, - }], - }; - const ctx = $('#build_timesChart').get(0).getContext('2d'); - const options = { - scaleOverlay: true, - responsive: true, - maintainAspectRatio: false, - }; - if (window.innerWidth < 768) { - // Scale fonts if window width lower than 768px (iPad portrait) - options.scaleFontSize = 8; - } - new Chart(ctx).Bar(data, options); -}); diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js index e2285494e62..47736fc5f42 100644 --- a/app/assets/javascripts/pipelines/services/pipelines_service.js +++ b/app/assets/javascripts/pipelines/services/pipelines_service.js @@ -1,6 +1,7 @@ /* eslint-disable class-methods-use-this */ import Vue from 'vue'; import VueResource from 'vue-resource'; +import '../../vue_shared/vue_resource_interceptor'; Vue.use(VueResource); diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js index a93bc935dd0..84049a1f0b7 100644 --- a/app/assets/javascripts/profile/account/index.js +++ b/app/assets/javascripts/profile/account/index.js @@ -1,29 +1,29 @@ import Vue from 'vue'; - import Translate from '~/vue_shared/translate'; - import deleteAccountModal from './components/delete_account_modal.vue'; -Vue.use(Translate); +export default () => { + Vue.use(Translate); -const deleteAccountButton = document.getElementById('delete-account-button'); -const deleteAccountModalEl = document.getElementById('delete-account-modal'); -// eslint-disable-next-line no-new -new Vue({ - el: deleteAccountModalEl, - components: { - deleteAccountModal, - }, - mounted() { - deleteAccountButton.classList.remove('disabled'); - }, - render(createElement) { - return createElement('delete-account-modal', { - props: { - actionUrl: deleteAccountModalEl.dataset.actionUrl, - confirmWithPassword: !!deleteAccountModalEl.dataset.confirmWithPassword, - username: deleteAccountModalEl.dataset.username, - }, - }); - }, -}); + const deleteAccountButton = document.getElementById('delete-account-button'); + const deleteAccountModalEl = document.getElementById('delete-account-modal'); + // eslint-disable-next-line no-new + new Vue({ + el: deleteAccountModalEl, + components: { + deleteAccountModal, + }, + mounted() { + deleteAccountButton.classList.remove('disabled'); + }, + render(createElement) { + return createElement('delete-account-modal', { + props: { + actionUrl: deleteAccountModalEl.dataset.actionUrl, + confirmWithPassword: !!deleteAccountModalEl.dataset.confirmWithPassword, + username: deleteAccountModalEl.dataset.username, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index ba4ac850346..a811781853b 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -1,122 +1,85 @@ /* 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 */ import Cookies from 'js-cookie'; -import Flash from '../flash'; -import { getPagePath } from '../lib/utils/common_utils'; +import axios from '~/lib/utils/axios_utils'; +import { __ } from '~/locale'; +import flash from '../flash'; -((global) => { - class Profile { - constructor({ form } = {}) { - this.onSubmitForm = this.onSubmitForm.bind(this); - this.form = form || $('.edit-user'); - this.newRepoActivated = Cookies.get('new_repo'); - this.setRepoRadio(); - this.bindEvents(); - this.initAvatarGlCrop(); - } - - initAvatarGlCrop() { - const cropOpts = { - filename: '.js-avatar-filename', - previewImage: '.avatar-image .avatar', - modalCrop: '.modal-profile-crop', - pickImageEl: '.js-choose-user-avatar-button', - uploadImageBtn: '.js-upload-user-avatar', - modalCropImg: '.modal-profile-crop-image' - }; - this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop'); - } - - bindEvents() { - $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); - $('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie); - $('#user_notification_email').on('change', this.submitForm); - $('#user_notified_of_own_activity').on('change', this.submitForm); - $('.update-username').on('ajax:before', this.beforeUpdateUsername); - $('.update-username').on('ajax:complete', this.afterUpdateUsername); - $('.update-notifications').on('ajax:success', this.onUpdateNotifs); - this.form.on('submit', this.onSubmitForm); - } - - submitForm() { - return $(this).parents('form').submit(); - } - - onSubmitForm(e) { - e.preventDefault(); - return this.saveForm(); - } +export default class Profile { + constructor({ form } = {}) { + this.onSubmitForm = this.onSubmitForm.bind(this); + this.form = form || $('.edit-user'); + this.newRepoActivated = Cookies.get('new_repo'); + this.setRepoRadio(); + this.bindEvents(); + this.initAvatarGlCrop(); + } - beforeUpdateUsername() { - $('.loading-username', this).removeClass('hidden'); - } + initAvatarGlCrop() { + const cropOpts = { + filename: '.js-avatar-filename', + previewImage: '.avatar-image .avatar', + modalCrop: '.modal-profile-crop', + pickImageEl: '.js-choose-user-avatar-button', + uploadImageBtn: '.js-upload-user-avatar', + modalCropImg: '.modal-profile-crop-image' + }; + this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop'); + } - afterUpdateUsername() { - $('.loading-username', this).addClass('hidden'); - $('button[type=submit]', this).enable(); - } + bindEvents() { + $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); + $('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie); + $('#user_notification_email').on('change', this.submitForm); + $('#user_notified_of_own_activity').on('change', this.submitForm); + this.form.on('submit', this.onSubmitForm); + } - onUpdateNotifs(e, data) { - return data.saved ? - new Flash("Notification settings saved", "notice") : - new Flash("Failed to save new settings", "alert"); - } + submitForm() { + return $(this).parents('form').submit(); + } - saveForm() { - const self = this; - const formData = new FormData(this.form[0]); - const avatarBlob = this.avatarGlCrop.getBlob(); + onSubmitForm(e) { + e.preventDefault(); + return this.saveForm(); + } - if (avatarBlob != null) { - formData.append('user[avatar]', avatarBlob, 'avatar.png'); - } + saveForm() { + const self = this; + const formData = new FormData(this.form[0]); + const avatarBlob = this.avatarGlCrop.getBlob(); - return $.ajax({ - url: this.form.attr('action'), - type: this.form.attr('method'), - data: formData, - dataType: "json", - processData: false, - contentType: false, - success: response => new Flash(response.message, 'notice'), - error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'), - complete: () => { - window.scrollTo(0, 0); - // Enable submit button after requests ends - return self.form.find(':input[disabled]').enable(); - } - }); + if (avatarBlob != null) { + formData.append('user[avatar]', avatarBlob, 'avatar.png'); } - setNewRepoCookie() { - if (this.value === 'off') { - Cookies.remove('new_repo'); - } else { - Cookies.set('new_repo', true, { expires_in: 365 }); - } - } + axios({ + method: this.form.attr('method'), + url: this.form.attr('action'), + data: formData, + }) + .then(({ data }) => flash(data.message, 'notice')) + .then(() => { + window.scrollTo(0, 0); + // Enable submit button after requests ends + self.form.find(':input[disabled]').enable(); + }) + .catch(error => flash(error.message)); + } - setRepoRadio() { - const multiEditRadios = $('input[name="user[multi_file]"]'); - if (this.newRepoActivated || this.newRepoActivated === 'true') { - multiEditRadios.filter('[value=on]').prop('checked', true); - } else { - multiEditRadios.filter('[value=off]').prop('checked', true); - } + setNewRepoCookie() { + if (this.value === 'off') { + Cookies.remove('new_repo'); + } else { + Cookies.set('new_repo', true, { expires_in: 365 }); } } - $(function() { - $(document).on('input.ssh_key', '#key_key', function() { - const $title = $('#key_title'); - const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); - - // Extract the SSH Key title from its comment - if (comment && comment.length > 1) { - return $title.val(comment[1]).change(); - } - }); - if (getPagePath() === 'profiles') { - return new Profile(); + setRepoRadio() { + const multiEditRadios = $('input[name="user[multi_file]"]'); + if (this.newRepoActivated || this.newRepoActivated === 'true') { + multiEditRadios.filter('[value=on]').prop('checked', true); + } else { + multiEditRadios.filter('[value=off]').prop('checked', true); } - }); -})(window.gl || (window.gl = {})); + } +} diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js deleted file mode 100644 index ff35a9bcb83..00000000000 --- a/app/assets/javascripts/profile/profile_bundle.js +++ /dev/null @@ -1,2 +0,0 @@ -import './gl_crop'; -import './profile'; diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 586d188350f..4fd639cce8e 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -73,7 +73,7 @@ export default class ProjectFindFile { // find file } - // files pathes load + // files pathes load load(url) { axios.get(url) .then(({ data }) => { @@ -85,7 +85,7 @@ export default class ProjectFindFile { .catch(() => flash(__('An error occurred while loading filenames'))); } - // render result + // render result renderList(filePaths, searchText) { var blobItemUrl, filePath, html, i, j, len, matches, results; this.element.find(".tree-table > tbody").empty(); diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 07a49d1506c..412aca7bfed 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -5,13 +5,13 @@ import ProjectSelectComboButton from './project_select_combo_button'; export default function projectSelect() { $('.ajax-project-select').each(function(i, select) { var placeholder; - const simpleFilter = $(select).data('simple-filter') || false; - this.groupId = $(select).data('group-id'); - this.includeGroups = $(select).data('include-groups'); - this.allProjects = $(select).data('all-projects') || false; - this.orderBy = $(select).data('order-by') || 'id'; - this.withIssuesEnabled = $(select).data('with-issues-enabled'); - this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled'); + const simpleFilter = $(select).data('simpleFilter') || false; + this.groupId = $(select).data('groupId'); + this.includeGroups = $(select).data('includeGroups'); + this.allProjects = $(select).data('allProjects') || false; + this.orderBy = $(select).data('orderBy') || 'id'; + this.withIssuesEnabled = $(select).data('withIssuesEnabled'); + this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled'); placeholder = "Search for project"; if (this.includeGroups) { diff --git a/app/assets/javascripts/projects/project_import_gitlab_project.js b/app/assets/javascripts/projects/project_import_gitlab_project.js index cec6f0dd5a3..d2c7d77bb2d 100644 --- a/app/assets/javascripts/projects/project_import_gitlab_project.js +++ b/app/assets/javascripts/projects/project_import_gitlab_project.js @@ -1,14 +1,8 @@ import { getParameterValues } from '../lib/utils/url_utility'; -const bindEvents = () => { +export default () => { const path = getParameterValues('path')[0]; // get the path url and append it in the inputS $('.js-path-name').val(path); }; - -document.addEventListener('DOMContentLoaded', bindEvents); - -export default { - bindEvents, -}; diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index f5133111d04..8da37d14f0b 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -1,3 +1,5 @@ +import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils'; + let hasUserDefinedProjectPath = false; const deriveProjectPathFromUrl = ($projectImportUrl) => { @@ -36,6 +38,7 @@ const bindEvents = () => { const $changeTemplateBtn = $('.change-template'); const $selectedIcon = $('.selected-icon svg'); const $templateProjectNameInput = $('#template-project-name #project_path'); + const $pushNewProjectTipTrigger = $('.push-new-project-tip'); if ($newProjectForm.length !== 1) { return; @@ -55,6 +58,34 @@ const bindEvents = () => { $('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`); }); + if ($pushNewProjectTipTrigger) { + $pushNewProjectTipTrigger + .removeAttr('rel') + .removeAttr('target') + .on('click', (e) => { e.preventDefault(); }) + .popover({ + title: $pushNewProjectTipTrigger.data('title'), + placement: 'auto bottom', + html: 'true', + content: $('.push-new-project-tip-template').html(), + }) + .on('shown.bs.popover', () => { + $(document).on('click.popover touchstart.popover', (event) => { + if ($(event.target).closest('.popover').length === 0) { + $pushNewProjectTipTrigger.trigger('click'); + } + }); + + const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find('.js-select-on-focus'); + addSelectOnFocusBehaviour(target); + + target.focus(); + }) + .on('hide.bs.popover', () => { + $(document).off('click.popover touchstart.popover'); + }); + } + function chooseTemplate() { $('.template-option').hide(); $projectFieldsForm.addClass('selected'); diff --git a/app/assets/javascripts/prometheus_metrics/index.js b/app/assets/javascripts/prometheus_metrics/index.js deleted file mode 100644 index a0c43c5abe1..00000000000 --- a/app/assets/javascripts/prometheus_metrics/index.js +++ /dev/null @@ -1,6 +0,0 @@ -import PrometheusMetrics from './prometheus_metrics'; - -$(() => { - const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); - prometheusMetrics.loadActiveMetrics(); -}); diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js index 59ad5b45855..e8126ac573d 100644 --- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -19,7 +19,7 @@ export default class PrometheusMetrics { this.$missingEnvVarMetricCount = this.$missingEnvVarPanel.find('.js-env-var-count'); this.$missingEnvVarMetricsList = this.$missingEnvVarPanel.find('.js-missing-var-metrics-list'); - this.activeMetricsEndpoint = this.$monitoredMetricsPanel.data('active-metrics'); + this.activeMetricsEndpoint = this.$monitoredMetricsPanel.data('activeMetrics'); this.$panelToggle.on('click', e => this.handlePanelToggle(e)); } diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js index 38b1406a99f..40a873833e1 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js +++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js @@ -9,8 +9,8 @@ export default class ProtectedBranchAccessDropdown { $dropdown.glDropdown({ data, selectable: true, - inputId: $dropdown.data('input-id'), - fieldName: $dropdown.data('field-name'), + inputId: $dropdown.data('inputId'), + fieldName: $dropdown.data('fieldName'), toggleLabel(item, $el) { if ($el.is('.is-active')) { return item.text; diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js index 2948baeab11..8fc87633e18 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -59,7 +59,7 @@ export default class ProtectedBranchCreate { ); this.savePreviousSelection($allowedToMergeInput.val(), $allowedToPushInput.val()); - this.$form.find('input[type="submit"]').attr('disabled', completedForm); + this.$form.find('input[type="submit"]').prop('disabled', completedForm); } static getProtectedBranches(term, callback) { diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js index b51b3e9a6ff..54560d08ad7 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -41,11 +41,11 @@ export default class ProtectedBranchEdit { axios.patch(this.$wrap.data('url'), { protected_branch: { merge_access_levels_attributes: [{ - id: this.$allowedToMergeDropdown.data('access-level-id'), + id: this.$allowedToMergeDropdown.data('accessLevelId'), access_level: $allowedToMergeInput.val(), }], push_access_levels_attributes: [{ - id: this.$allowedToPushDropdown.data('access-level-id'), + id: this.$allowedToPushDropdown.data('accessLevelId'), access_level: $allowedToPushInput.val(), }], }, diff --git a/app/assets/javascripts/protected_tags/index.js b/app/assets/javascripts/protected_tags/index.js deleted file mode 100644 index b1618e24e49..00000000000 --- a/app/assets/javascripts/protected_tags/index.js +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint-disable no-unused-vars */ - -import ProtectedTagCreate from './protected_tag_create'; -import ProtectedTagEditList from './protected_tag_edit_list'; - -$(() => { - const protectedtTagCreate = new ProtectedTagCreate(); - const protectedtTagEditList = new ProtectedTagEditList(); -}); diff --git a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js index d4c9a91a74a..b803da798d5 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js +++ b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js @@ -9,8 +9,8 @@ export default class ProtectedTagAccessDropdown { this.options.$dropdown.glDropdown({ data: this.options.data, selectable: true, - inputId: this.options.$dropdown.data('input-id'), - fieldName: this.options.$dropdown.data('field-name'), + inputId: this.options.$dropdown.data('inputId'), + fieldName: this.options.$dropdown.data('fieldName'), toggleLabel(item, $el) { if ($el.is('.is-active')) { return item.text; diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js index d1e4a75c17b..2f94ffe2507 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_create.js +++ b/app/assets/javascripts/protected_tags/protected_tag_create.js @@ -39,7 +39,7 @@ export default class ProtectedTagCreate { const $tagInput = this.$form.find('input[name="protected_tag[name]"]'); const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes'); - this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToCreateInput.length)); + this.$form.find('input[type="submit"]').prop('disabled', !($tagInput.val() && $allowedToCreateInput.length)); } static getProtectedTags(term, callback) { diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js index 21a258cf93c..8687b2a4044 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js @@ -31,7 +31,7 @@ export default class ProtectedTagEdit { axios.patch(this.$wrap.data('url'), { protected_tag: { create_access_levels_attributes: [{ - id: this.$allowedToCreateDropdownButton.data('access-level-id'), + id: this.$allowedToCreateDropdownButton.data('accessLevelId'), access_level: $allowedToCreateInput.val(), }], }, diff --git a/app/assets/javascripts/ref_select_dropdown.js b/app/assets/javascripts/ref_select_dropdown.js index 65e4101352c..56c25a35e6d 100644 --- a/app/assets/javascripts/ref_select_dropdown.js +++ b/app/assets/javascripts/ref_select_dropdown.js @@ -6,7 +6,7 @@ class RefSelectDropdown { filterable: true, filterByText: true, remote: false, - fieldName: $dropdownButton.data('field-name'), + fieldName: $dropdownButton.data('fieldName'), filterInput: 'input[type="search"]', selectable: true, isSelectable(branch, $el) { @@ -24,7 +24,7 @@ class RefSelectDropdown { }); const $dropdownContainer = $dropdownButton.closest('.dropdown'); - const $fieldInput = $(`input[name="${$dropdownButton.data('field-name')}"]`, $dropdownContainer); + const $fieldInput = $(`input[name="${$dropdownButton.data('fieldName')}"]`, $dropdownContainer); const $filterInput = $('input[type="search"]', $dropdownContainer); $filterInput.on('keyup', (e) => { diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js index d8edff73f72..6fb125192b2 100644 --- a/app/assets/javascripts/registry/index.js +++ b/app/assets/javascripts/registry/index.js @@ -4,7 +4,7 @@ import Translate from '../vue_shared/translate'; Vue.use(Translate); -document.addEventListener('DOMContentLoaded', () => new Vue({ +export default () => new Vue({ el: '#js-vue-registry-images', components: { registryApp, @@ -22,4 +22,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ }, }); }, -})); +}); diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js index 5482c55f8bb..05a623ca6d9 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/render_gfm.js @@ -1,6 +1,7 @@ import renderMath from './render_math'; import renderMermaid from './render_mermaid'; import syntaxHighlight from './syntax_highlight'; + // Render Gitlab flavoured Markdown // // Delegates to syntax highlight and render math & mermaid diagrams. diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js index 73b6aafdd12..eabdb01b2a9 100644 --- a/app/assets/javascripts/render_math.js +++ b/app/assets/javascripts/render_math.js @@ -1,4 +1,5 @@ -/* global katex */ +import { __ } from './locale'; +import flash from './flash'; // Renders math using KaTeX in any element with the // `js-render-math` class @@ -8,15 +9,8 @@ // <code class="js-render-math"></div> // -import { __ } from './locale'; -import axios from './lib/utils/axios_utils'; -import flash from './flash'; - -// Only load once -let katexLoaded = false; - // Loop over all math elements and render math -function renderWithKaTeX(elements) { +function renderWithKaTeX(elements, katex) { elements.each(function katexElementsLoop() { const mathNode = $('<span></span>'); const $this = $(this); @@ -34,30 +28,10 @@ function renderWithKaTeX(elements) { export default function renderMath($els) { if (!$els.length) return; - - if (katexLoaded) { - renderWithKaTeX($els); - } else { - axios.get(gon.katex_css_url) - .then(() => { - const css = $('<link>', { - rel: 'stylesheet', - type: 'text/css', - href: gon.katex_css_url, - }); - css.appendTo('head'); - }) - .then(() => axios.get(gon.katex_js_url, { - responseType: 'text', - })) - .then(({ data }) => { - // Add katex js to our document - $.globalEval(data); - }) - .then(() => { - katexLoaded = true; - renderWithKaTeX($els); // Run KaTeX - }) - .catch(() => flash(__('An error occurred while rendering KaTeX'))); - } + Promise.all([ + import(/* webpackChunkName: 'katex' */ 'katex'), + import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.css'), + ]).then(([katex]) => { + renderWithKaTeX($els, katex); + }).catch(() => flash(__('An error occurred while rendering KaTeX'))); } diff --git a/app/assets/javascripts/render_mermaid.js b/app/assets/javascripts/render_mermaid.js index 31c7a772cf4..d4f18955bd2 100644 --- a/app/assets/javascripts/render_mermaid.js +++ b/app/assets/javascripts/render_mermaid.js @@ -30,6 +30,9 @@ export default function renderMermaid($els) { $els.each((i, el) => { const source = el.textContent; + // Remove any extra spans added by the backend syntax highlighting. + Object.assign(el, { textContent: source }); + mermaid.init(undefined, el, (id) => { const svg = document.getElementById(id); diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 01c3be5411f..8d3cc849f81 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -76,8 +76,8 @@ Sidebar.prototype.toggleTodo = function(e) { $('.js-issuable-todo').disable().addClass('is-loading'); axios[ajaxType](url, { - issuable_id: $this.data('issuable-id'), - issuable_type: $this.data('issuable-type'), + issuable_id: $this.data('issuableId'), + issuable_type: $this.data('issuableType'), }).then(({ data }) => { this.todoUpdateDone(data); }).catch(() => flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`)); @@ -96,18 +96,18 @@ Sidebar.prototype.todoUpdateDone = function(data) { $el.removeClass('is-loading') .enable() - .attr('aria-label', $el.data(`${attrPrefix}-text`)) + .attr('aria-label', $el.data(`${attrPrefix}Text`)) .attr('data-delete-path', deletePath) - .attr('title', $el.data(`${attrPrefix}-text`)); + .attr('title', $el.data(`${attrPrefix}Text`)); if ($el.hasClass('has-tooltip')) { $el.tooltip('fixTitle'); } - if ($el.data(`${attrPrefix}-icon`)) { - $elText.html($el.data(`${attrPrefix}-icon`)); + if ($el.data(`${attrPrefix}Icon`)) { + $elText.html($el.data(`${attrPrefix}Icon`)); } else { - $elText.text($el.data(`${attrPrefix}-text`)); + $elText.text($el.data(`${attrPrefix}Text`)); } }); }; diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 8f4a8704c3b..fdfa4f28aba 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -1,5 +1,6 @@ /* eslint-disable 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-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */ import axios from './lib/utils/axios_utils'; +import DropdownUtils from './filtered_search/dropdown_utils'; import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils'; /** @@ -25,32 +26,32 @@ function setSearchOptions() { if ($projectOptionsDataEl.length) { gl.projectOptions = gl.projectOptions || {}; - var projectPath = $projectOptionsDataEl.data('project-path'); + var projectPath = $projectOptionsDataEl.data('projectPath'); gl.projectOptions[projectPath] = { name: $projectOptionsDataEl.data('name'), - issuesPath: $projectOptionsDataEl.data('issues-path'), - issuesDisabled: $projectOptionsDataEl.data('issues-disabled'), - mrPath: $projectOptionsDataEl.data('mr-path'), + issuesPath: $projectOptionsDataEl.data('issuesPath'), + issuesDisabled: $projectOptionsDataEl.data('issuesDisabled'), + mrPath: $projectOptionsDataEl.data('mrPath'), }; } if ($groupOptionsDataEl.length) { gl.groupOptions = gl.groupOptions || {}; - var groupPath = $groupOptionsDataEl.data('group-path'); + var groupPath = $groupOptionsDataEl.data('groupPath'); gl.groupOptions[groupPath] = { name: $groupOptionsDataEl.data('name'), - issuesPath: $groupOptionsDataEl.data('issues-path'), - mrPath: $groupOptionsDataEl.data('mr-path'), + issuesPath: $groupOptionsDataEl.data('issuesPath'), + mrPath: $groupOptionsDataEl.data('mrPath'), }; } if ($dashboardOptionsDataEl.length) { gl.dashboardOptions = { - issuesPath: $dashboardOptionsDataEl.data('issues-path'), - mrPath: $dashboardOptionsDataEl.data('mr-path'), + issuesPath: $dashboardOptionsDataEl.data('issuesPath'), + mrPath: $dashboardOptionsDataEl.data('mrPath'), }; } } @@ -61,9 +62,9 @@ export default class SearchAutocomplete { this.bindEventContext(); this.wrap = wrap || $('.search'); this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts'); - this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path'); - this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || ''); - this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || ''); + this.autocompletePath = autocompletePath || this.optsEl.data('autocompletePath'); + this.projectId = projectId || (this.optsEl.data('autocompleteProjectId') || ''); + this.projectRef = projectRef || (this.optsEl.data('autocompleteProjectRef') || ''); this.dropdown = this.wrap.find('.dropdown'); this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle'); this.dropdownContent = this.dropdown.find('.dropdown-content'); diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index d34a21b37e1..d0e4f533d8a 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -42,7 +42,7 @@ export default function initSettingsPanels() { if (location.hash) { const $target = $(location.hash); - if ($target.length && $target.hasClass('.settings')) { + if ($target.length && $target.hasClass('settings')) { expandSection($target); } } diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 689befc742e..14545824e74 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -9,13 +9,12 @@ export default class ShortcutsIssuable extends Shortcuts { super(); this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form'); - this.editBtn = document.querySelector('.js-issuable-edit'); Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee')); Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone')); Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels')); Mousetrap.bind('r', this.replyWithSelectedText.bind(this)); - Mousetrap.bind('e', this.editIssue.bind(this)); + Mousetrap.bind('e', ShortcutsIssuable.editIssue); if (isMergeRequest) { this.enabledHelp.push('.hidden-shortcut.merge_requests'); @@ -58,10 +57,10 @@ export default class ShortcutsIssuable extends Shortcuts { return false; } - editIssue() { + static editIssue() { // Need to click the element as on issues, editing is inline // on merge request, editing is on a different page - this.editBtn.click(); + document.querySelector('.js-issuable-edit').click(); return false; } diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.js b/app/assets/javascripts/sidebar/components/assignees/assignees.js deleted file mode 100644 index 643877b9d47..00000000000 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.js +++ /dev/null @@ -1,224 +0,0 @@ -export default { - name: 'Assignees', - data() { - return { - defaultRenderCount: 5, - defaultMaxCounter: 99, - showLess: true, - }; - }, - props: { - rootPath: { - type: String, - required: true, - }, - users: { - type: Array, - required: true, - }, - editable: { - type: Boolean, - required: true, - }, - }, - computed: { - firstUser() { - return this.users[0]; - }, - hasMoreThanTwoAssignees() { - return this.users.length > 2; - }, - hasMoreThanOneAssignee() { - return this.users.length > 1; - }, - hasAssignees() { - return this.users.length > 0; - }, - hasNoUsers() { - return !this.users.length; - }, - hasOneUser() { - return this.users.length === 1; - }, - renderShowMoreSection() { - return this.users.length > this.defaultRenderCount; - }, - numberOfHiddenAssignees() { - return this.users.length - this.defaultRenderCount; - }, - isHiddenAssignees() { - return this.numberOfHiddenAssignees > 0; - }, - hiddenAssigneesLabel() { - return `+ ${this.numberOfHiddenAssignees} more`; - }, - collapsedTooltipTitle() { - const maxRender = Math.min(this.defaultRenderCount, this.users.length); - const renderUsers = this.users.slice(0, maxRender); - const names = renderUsers.map(u => u.name); - - if (this.users.length > maxRender) { - names.push(`+ ${this.users.length - maxRender} more`); - } - - return names.join(', '); - }, - sidebarAvatarCounter() { - let counter = `+${this.users.length - 1}`; - - if (this.users.length > this.defaultMaxCounter) { - counter = `${this.defaultMaxCounter}+`; - } - - return counter; - }, - }, - methods: { - assignSelf() { - this.$emit('assign-self'); - }, - toggleShowLess() { - this.showLess = !this.showLess; - }, - renderAssignee(index) { - return !this.showLess || (index < this.defaultRenderCount && this.showLess); - }, - avatarUrl(user) { - return user.avatar || user.avatar_url || gon.default_avatar_url; - }, - assigneeUrl(user) { - return `${this.rootPath}${user.username}`; - }, - assigneeAlt(user) { - return `${user.name}'s avatar`; - }, - assigneeUsername(user) { - return `@${user.username}`; - }, - shouldRenderCollapsedAssignee(index) { - const firstTwo = this.users.length <= 2 && index <= 2; - - return index === 0 || firstTwo; - }, - }, - template: ` - <div> - <div - class="sidebar-collapsed-icon sidebar-collapsed-user" - :class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }" - data-container="body" - data-placement="left" - :title="collapsedTooltipTitle" - > - <i - v-if="hasNoUsers" - aria-label="No Assignee" - class="fa fa-user" - /> - <button - type="button" - class="btn-link" - v-for="(user, index) in users" - v-if="shouldRenderCollapsedAssignee(index)" - > - <img - width="24" - class="avatar avatar-inline s24" - :alt="assigneeAlt(user)" - :src="avatarUrl(user)" - /> - <span class="author"> - {{ user.name }} - </span> - </button> - <button - v-if="hasMoreThanTwoAssignees" - class="btn-link" - type="button" - > - <span - class="avatar-counter sidebar-avatar-counter" - > - {{ sidebarAvatarCounter }} - </span> - </button> - </div> - <div class="value hide-collapsed"> - <template v-if="hasNoUsers"> - <span class="assign-yourself no-value"> - No assignee - <template v-if="editable"> - - - <button - type="button" - class="btn-link" - @click="assignSelf" - > - assign yourself - </button> - </template> - </span> - </template> - <template v-else-if="hasOneUser"> - <a - class="author_link bold" - :href="assigneeUrl(firstUser)" - > - <img - width="32" - class="avatar avatar-inline s32" - :alt="assigneeAlt(firstUser)" - :src="avatarUrl(firstUser)" - /> - <span class="author"> - {{ firstUser.name }} - </span> - <span class="username"> - {{ assigneeUsername(firstUser) }} - </span> - </a> - </template> - <template v-else> - <div class="user-list"> - <div - class="user-item" - v-for="(user, index) in users" - v-if="renderAssignee(index)" - > - <a - class="user-link has-tooltip" - data-placement="bottom" - :href="assigneeUrl(user)" - :data-title="user.name" - > - <img - width="32" - class="avatar avatar-inline s32" - :alt="assigneeAlt(user)" - :src="avatarUrl(user)" - /> - </a> - </div> - </div> - <div - v-if="renderShowMoreSection" - class="user-list-more" - > - <button - type="button" - class="btn-link" - @click="toggleShowLess" - > - <template v-if="showLess"> - {{ hiddenAssigneesLabel }} - </template> - <template v-else> - - show less - </template> - </button> - </div> - </template> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue new file mode 100644 index 00000000000..1e7f46454bf --- /dev/null +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -0,0 +1,232 @@ +<script> +export default { + name: 'Assignees', + props: { + rootPath: { + type: String, + required: true, + }, + users: { + type: Array, + required: true, + }, + editable: { + type: Boolean, + required: true, + }, + }, + data() { + return { + defaultRenderCount: 5, + defaultMaxCounter: 99, + showLess: true, + }; + }, + computed: { + firstUser() { + return this.users[0]; + }, + hasMoreThanTwoAssignees() { + return this.users.length > 2; + }, + hasMoreThanOneAssignee() { + return this.users.length > 1; + }, + hasAssignees() { + return this.users.length > 0; + }, + hasNoUsers() { + return !this.users.length; + }, + hasOneUser() { + return this.users.length === 1; + }, + renderShowMoreSection() { + return this.users.length > this.defaultRenderCount; + }, + numberOfHiddenAssignees() { + return this.users.length - this.defaultRenderCount; + }, + isHiddenAssignees() { + return this.numberOfHiddenAssignees > 0; + }, + hiddenAssigneesLabel() { + return `+ ${this.numberOfHiddenAssignees} more`; + }, + collapsedTooltipTitle() { + const maxRender = Math.min(this.defaultRenderCount, this.users.length); + const renderUsers = this.users.slice(0, maxRender); + const names = renderUsers.map(u => u.name); + + if (this.users.length > maxRender) { + names.push(`+ ${this.users.length - maxRender} more`); + } + + return names.join(', '); + }, + sidebarAvatarCounter() { + let counter = `+${this.users.length - 1}`; + + if (this.users.length > this.defaultMaxCounter) { + counter = `${this.defaultMaxCounter}+`; + } + + return counter; + }, + }, + methods: { + assignSelf() { + this.$emit('assign-self'); + }, + toggleShowLess() { + this.showLess = !this.showLess; + }, + renderAssignee(index) { + return !this.showLess || (index < this.defaultRenderCount && this.showLess); + }, + avatarUrl(user) { + return user.avatar || user.avatar_url || gon.default_avatar_url; + }, + assigneeUrl(user) { + return `${this.rootPath}${user.username}`; + }, + assigneeAlt(user) { + return `${user.name}'s avatar`; + }, + assigneeUsername(user) { + return `@${user.username}`; + }, + shouldRenderCollapsedAssignee(index) { + const firstTwo = this.users.length <= 2 && index <= 2; + + return index === 0 || firstTwo; + }, + }, +}; +</script> + +<template> + <div> + <div + class="sidebar-collapsed-icon sidebar-collapsed-user" + :class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }" + data-container="body" + data-placement="left" + :title="collapsedTooltipTitle" + > + <i + v-if="hasNoUsers" + aria-label="No Assignee" + class="fa fa-user" + > + </i> + <button + type="button" + class="btn-link" + v-for="(user, index) in users" + v-if="shouldRenderCollapsedAssignee(index)" + :key="user.id" + > + <img + width="24" + class="avatar avatar-inline s24" + :alt="assigneeAlt(user)" + :src="avatarUrl(user)" + /> + <span class="author"> + {{ user.name }} + </span> + </button> + <button + v-if="hasMoreThanTwoAssignees" + class="btn-link" + type="button" + > + <span + class="avatar-counter sidebar-avatar-counter" + > + {{ sidebarAvatarCounter }} + </span> + </button> + </div> + <div class="value hide-collapsed"> + <template v-if="hasNoUsers"> + <span class="assign-yourself no-value"> + No assignee + <template v-if="editable"> + - + <button + type="button" + class="btn-link" + @click="assignSelf" + > + assign yourself + </button> + </template> + </span> + </template> + <template v-else-if="hasOneUser"> + <a + class="author_link bold" + :href="assigneeUrl(firstUser)" + > + <img + width="32" + class="avatar avatar-inline s32" + :alt="assigneeAlt(firstUser)" + :src="avatarUrl(firstUser)" + /> + <span class="author"> + {{ firstUser.name }} + </span> + <span class="username"> + {{ assigneeUsername(firstUser) }} + </span> + </a> + </template> + <template v-else> + <div class="user-list"> + <div + class="user-item" + v-for="(user, index) in users" + v-if="renderAssignee(index)" + :key="user.id" + > + <a + class="user-link has-tooltip" + data-container="body" + data-placement="bottom" + :href="assigneeUrl(user)" + :data-title="user.name" + > + <img + width="32" + class="avatar avatar-inline s32" + :alt="assigneeAlt(user)" + :src="avatarUrl(user)" + /> + </a> + </div> + </div> + <div + v-if="renderShowMoreSection" + class="user-list-more" + > + <button + type="button" + class="btn-link" + @click="toggleShowLess" + > + <template v-if="showLess"> + {{ hiddenAssigneesLabel }} + </template> + <template v-else> + - show less + </template> + </button> + </div> + </template> + </div> + </div> +</template> + diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js index 9e47039d920..8269fe1281d 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js @@ -1,6 +1,6 @@ import Flash from '../../../flash'; import AssigneeTitle from './assignee_title'; -import Assignees from './assignees'; +import Assignees from './assignees.vue'; import Store from '../../stores/sidebar_store'; import eventHub from '../../event_hub'; @@ -28,8 +28,8 @@ export default { }, }, components: { - 'assignee-title': AssigneeTitle, - assignees: Assignees, + AssigneeTitle, + Assignees, }, methods: { assignSelf() { diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index 02153fb86a5..8a86c409b62 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -2,6 +2,7 @@ import Flash from '../../../flash'; import editForm from './edit_form.vue'; import Icon from '../../../vue_shared/components/icon.vue'; + import { __ } from '../../../locale'; export default { components: { @@ -40,8 +41,7 @@ this.service.update('issue', { confidential }) .then(() => location.reload()) .catch(() => { - Flash(`Something went wrong trying to - change the confidentiality of this issue`); + Flash(__('Something went wrong trying to change the confidentiality of this issue')); }); }, }, @@ -58,7 +58,7 @@ /> </div> <div class="title hide-collapsed"> - Confidentiality + {{ __('Confidentiality') }} <a v-if="isEditable" class="pull-right confidential-edit" @@ -84,7 +84,7 @@ aria-hidden="true" class="sidebar-item-icon inline" /> - Not confidential + {{ __('Not confidential') }} </div> <div v-else @@ -95,7 +95,7 @@ aria-hidden="true" class="sidebar-item-icon inline is-active" /> - This issue is confidential + {{ __('This issue is confidential') }} </div> </div> </div> diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue index 6a81235a1a7..c569843b05f 100644 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue @@ -1,5 +1,6 @@ <script> import editFormButtons from './edit_form_buttons.vue'; + import { s__ } from '../../../locale'; export default { components: { @@ -19,6 +20,14 @@ type: Function, }, }, + computed: { + confidentialityOnWarning() { + return s__('confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.'); + }, + confidentialityOffWarning() { + return s__('confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.'); + }, + }, }; </script> @@ -26,15 +35,13 @@ <div class="dropdown open"> <div class="dropdown-menu sidebar-item-warning-message"> <div> - <p v-if="!isConfidential"> - You are going to turn on the confidentiality. This means that only team members with - <strong>at least Reporter access</strong> - are able to see and leave comments on the issue. + <p + v-if="!isConfidential" + v-html="confidentialityOnWarning"> </p> - <p v-else> - You are going to turn off the confidentiality. This means - <strong>everyone</strong> - will be able to see and leave a comment on this issue. + <p + v-else + v-html="confidentialityOffWarning"> </p> <edit-form-buttons :is-confidential="isConfidential" diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue index 7ed0619ee6b..49d5dfeea1a 100644 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue +++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue @@ -32,7 +32,7 @@ export default { class="btn btn-default append-right-10" @click="toggleForm" > - Cancel + {{ __('Cancel') }} </button> <button type="button" diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue index e7a87636aa7..bc32e974bc3 100644 --- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue @@ -1,6 +1,7 @@ <script> import editFormButtons from './edit_form_buttons.vue'; import issuableMixin from '../../../vue_shared/mixins/issuable'; + import { __, sprintf } from '../../../locale'; export default { components: { @@ -25,6 +26,14 @@ type: Function, }, }, + computed: { + lockWarning() { + return sprintf(__('Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName }); + }, + unlockWarning() { + return sprintf(__('Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName }); + }, + }, }; </script> @@ -33,19 +42,14 @@ <div class="dropdown-menu sidebar-item-warning-message"> <p class="text" - v-if="isLocked"> - Unlock this {{ issuableDisplayName }}? - <strong>Everyone</strong> - will be able to comment. + v-if="isLocked" + v-html="unlockWarning"> </p> <p class="text" - v-else> - Lock this {{ issuableDisplayName }}? - Only - <strong>project members</strong> - will be able to comment. + v-else + v-html="lockWarning"> </p> <edit-form-buttons diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue index 02876a6c175..0686910fc7e 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -1,5 +1,5 @@ <script> - import Flash from '../../../flash'; + import Flash from '~/flash'; import editForm from './edit_form.vue'; import issuableMixin from '../../../vue_shared/mixins/issuable'; import Icon from '../../../vue_shared/components/icon.vue'; @@ -53,8 +53,7 @@ discussion_locked: locked, }) .then(() => location.reload()) - .catch(() => Flash(this.__(`Something went wrong trying to - change the locked state of this ${this.issuableDisplayName}`))); + .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName}`))); }, }, }; @@ -72,10 +71,10 @@ </div> <div class="title hide-collapsed"> - Lock {{ issuableDisplayName }} + {{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }} <button v-if="isEditable" - class="pull-right lock-edit btn btn-blank" + class="pull-right lock-edit" type="button" @click.prevent="toggleForm" > 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 fd0d4570d68..b5ebccd3795 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js @@ -68,7 +68,7 @@ export default { <div class="compare-display-container"> <div class="compare-display pull-left"> <span class="compare-label"> - Spent + {{ s__('TimeTracking|Spent') }} </span> <span class="compare-value spent"> {{ timeSpentHumanReadable }} @@ -76,7 +76,7 @@ export default { </div> <div class="compare-display estimated pull-right"> <span class="compare-label"> - Est + {{ s__('TimeTrackingEstimated|Est') }} </span> <span class="compare-value"> {{ timeEstimateHumanReadable }} diff --git a/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js index ad1b9179db0..2d324c71379 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js @@ -9,7 +9,7 @@ export default { template: ` <div class="time-tracking-estimate-only-pane"> <span class="bold"> - Estimated: + {{ s__('TimeTracking|Estimated:') }} </span> {{ timeEstimateHumanReadable }} </div> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.js b/app/assets/javascripts/sidebar/components/time_tracking/help_state.js index 142ad437509..19f74ad3c6d 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.js @@ -1,3 +1,5 @@ +import { sprintf, s__ } from '../../../locale'; + export default { name: 'time-tracking-help-state', props: { @@ -10,33 +12,39 @@ export default { href() { return `${this.rootPath}help/workflow/time_tracking.md`; }, + estimateText() { + return sprintf( + s__('estimateCommand|%{slash_command} will update the estimated time with the latest command.'), { + slash_command: '<code>/estimate</code>', + }, false, + ); + }, + spendText() { + return sprintf( + s__('spendCommand|%{slash_command} will update the sum of the time spent.'), { + slash_command: '<code>/spend</code>', + }, false, + ); + }, }, template: ` <div class="time-tracking-help-state"> <div class="time-tracking-info"> <h4> - Track time with quick actions + {{ __('Track time with quick actions') }} </h4> <p> - Quick actions can be used in the issues description and comment boxes. + {{ __('Quick actions can be used in the issues description and comment boxes.') }} </p> - <p> - <code> - /estimate - </code> - will update the estimated time with the latest command. + <p v-html="estimateText"> </p> - <p> - <code> - /spend - </code> - will update the sum of the time spent. + <p v-html="spendText"> </p> <a class="btn btn-default learn-more-button" :href="href" > - Learn more + {{ __('Learn more') }} </a> </div> </div> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js index d1dd1dcdd27..38da76c6771 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js @@ -3,7 +3,7 @@ export default { template: ` <div class="time-tracking-no-tracking-pane"> <span class="no-value"> - No estimate or time spent + {{ __('No estimate or time spent') }} </span> </div> `, diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js index d32fe4abc7d..782e4ba4fad 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js @@ -2,7 +2,7 @@ import _ from 'underscore'; import '~/smart_interval'; -import timeTracker from './time_tracker'; +import IssuableTimeTracker from './time_tracker.vue'; import Store from '../../stores/sidebar_store'; import Mediator from '../../sidebar_mediator'; @@ -16,7 +16,7 @@ export default { }; }, components: { - 'issuable-time-tracker': timeTracker, + IssuableTimeTracker, }, methods: { listenForQuickActions() { diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.js b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue index ed0d71a4f79..230736a56b8 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -1,3 +1,4 @@ +<script> import timeTrackingHelpState from './help_state'; import timeTrackingCollapsedState from './collapsed_state'; import timeTrackingSpentOnlyPane from './spent_only_pane'; @@ -8,7 +9,15 @@ import timeTrackingComparisonPane from './comparison_pane'; import eventHub from '../../event_hub'; export default { - name: 'issuable-time-tracker', + name: 'IssuableTimeTracker', + components: { + 'time-tracking-collapsed-state': timeTrackingCollapsedState, + 'time-tracking-estimate-only-pane': timeTrackingEstimateOnlyPane, + 'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane, + 'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane, + 'time-tracking-comparison-pane': timeTrackingComparisonPane, + 'time-tracking-help-state': timeTrackingHelpState, + }, props: { time_estimate: { type: Number, @@ -38,14 +47,6 @@ export default { showHelp: false, }; }, - components: { - 'time-tracking-collapsed-state': timeTrackingCollapsedState, - 'time-tracking-estimate-only-pane': timeTrackingEstimateOnlyPane, - 'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane, - 'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane, - 'time-tracking-comparison-pane': timeTrackingComparisonPane, - 'time-tracking-help-state': timeTrackingHelpState, - }, computed: { timeSpent() { return this.time_spent; @@ -81,6 +82,9 @@ export default { return !!this.showHelp; }, }, + created() { + eventHub.$on('timeTracker:updateData', this.update); + }, methods: { toggleHelpState(show) { this.showHelp = show; @@ -92,72 +96,73 @@ export default { this.human_time_spent = data.human_time_spent; }, }, - created() { - eventHub.$on('timeTracker:updateData', this.update); - }, - template: ` - <div - class="time_tracker time-tracking-component-wrap" - v-cloak - > - <time-tracking-collapsed-state - :show-comparison-state="showComparisonState" - :show-no-time-tracking-state="showNoTimeTrackingState" - :show-help-state="showHelpState" - :show-spent-only-state="showSpentOnlyState" - :show-estimate-only-state="showEstimateOnlyState" +}; +</script> + +<template> + <div + class="time_tracker time-tracking-component-wrap" + v-cloak + > + <time-tracking-collapsed-state + :show-comparison-state="showComparisonState" + :show-no-time-tracking-state="showNoTimeTrackingState" + :show-help-state="showHelpState" + :show-spent-only-state="showSpentOnlyState" + :show-estimate-only-state="showEstimateOnlyState" + :time-spent-human-readable="timeSpentHumanReadable" + :time-estimate-human-readable="timeEstimateHumanReadable" + /> + <div class="title hide-collapsed"> + {{ __('Time tracking') }} + <div + class="help-button pull-right" + v-if="!showHelpState" + @click="toggleHelpState(true)" + > + <i + class="fa fa-question-circle" + aria-hidden="true" + > + </i> + </div> + <div + class="close-help-button pull-right" + v-if="showHelpState" + @click="toggleHelpState(false)" + > + <i + class="fa fa-close" + aria-hidden="true" + > + </i> + </div> + </div> + <div class="time-tracking-content hide-collapsed"> + <time-tracking-estimate-only-pane + v-if="showEstimateOnlyState" + :time-estimate-human-readable="timeEstimateHumanReadable" + /> + <time-tracking-spent-only-pane + v-if="showSpentOnlyState" + :time-spent-human-readable="timeSpentHumanReadable" + /> + <time-tracking-no-tracking-pane + v-if="showNoTimeTrackingState" + /> + <time-tracking-comparison-pane + v-if="showComparisonState" + :time-estimate="timeEstimate" + :time-spent="timeSpent" :time-spent-human-readable="timeSpentHumanReadable" :time-estimate-human-readable="timeEstimateHumanReadable" /> - <div class="title hide-collapsed"> - Time tracking - <div - class="help-button pull-right" - v-if="!showHelpState" - @click="toggleHelpState(true)" - > - <i - class="fa fa-question-circle" - aria-hidden="true" - /> - </div> - <div - class="close-help-button pull-right" + <transition name="help-state-toggle"> + <time-tracking-help-state v-if="showHelpState" - @click="toggleHelpState(false)" - > - <i - class="fa fa-close" - aria-hidden="true" - /> - </div> - </div> - <div class="time-tracking-content hide-collapsed"> - <time-tracking-estimate-only-pane - v-if="showEstimateOnlyState" - :time-estimate-human-readable="timeEstimateHumanReadable" + :root-path="rootPath" /> - <time-tracking-spent-only-pane - v-if="showSpentOnlyState" - :time-spent-human-readable="timeSpentHumanReadable" - /> - <time-tracking-no-tracking-pane - v-if="showNoTimeTrackingState" - /> - <time-tracking-comparison-pane - v-if="showComparisonState" - :time-estimate="timeEstimate" - :time-spent="timeSpent" - :time-spent-human-readable="timeSpentHumanReadable" - :time-estimate-human-readable="timeEstimateHumanReadable" - /> - <transition name="help-state-toggle"> - <time-tracking-help-state - v-if="showHelpState" - :rootPath="rootPath" - /> - </transition> - </div> + </transition> </div> - `, -}; + </div> +</template> diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js index 977dd83a7ea..b10e2cc60ef 100644 --- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js +++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js @@ -50,7 +50,7 @@ class SidebarMoveIssue { const selectedProjectId = options.isMarking ? project.id : 0; this.mediator.setMoveToProjectId(selectedProjectId); - this.$confirmButton.attr('disabled', !isValidProjectId(selectedProjectId)); + this.$confirmButton.prop('disabled', !isValidProjectId(selectedProjectId)); }, }); } diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js new file mode 100644 index 00000000000..b15ad0e5586 --- /dev/null +++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js @@ -0,0 +1,27 @@ +import Vue from 'vue'; +import timeTracker from './components/time_tracking/time_tracker.vue'; + +export default class SidebarMilestone { + constructor() { + const el = document.getElementById('issuable-time-tracker'); + + if (!el) return; + + // eslint-disable-next-line no-new + new Vue({ + el, + components: { + timeTracker, + }, + render: createElement => createElement('timeTracker', { + props: { + time_estimate: parseInt(el.dataset.timeEstimate, 10), + time_spent: parseInt(el.dataset.timeSpent, 10), + human_time_estimate: el.dataset.humanTimeEstimate, + human_time_spent: el.dataset.humanTimeSpent, + rootPath: '/', + }, + }), + }); + } +} diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js index 04c39d7b6b5..377846db70e 100644 --- a/app/assets/javascripts/sidebar/sidebar_bundle.js +++ b/app/assets/javascripts/sidebar/sidebar_bundle.js @@ -1,13 +1,9 @@ import Mediator from './sidebar_mediator'; import { mountSidebar, getSidebarOptions } from './mount_sidebar'; -function domContentLoaded() { +export default () => { const mediator = new Mediator(getSidebarOptions()); mediator.fetch(); mountSidebar(mediator); -} - -document.addEventListener('DOMContentLoaded', domContentLoaded); - -export default domContentLoaded; +}; diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 48dd91bdf06..6142ce6c6a3 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -18,7 +18,7 @@ export default class SingleFileDiff { this.toggleDiff = this.toggleDiff.bind(this); this.content = $('.diff-content', this.file); this.$toggleIcon = $('.diff-toggle-caret', this.file); - this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path'); + this.diffForPath = this.content.find('[data-diff-for-path]').data('diffForPath'); this.isOpen = !this.diffForPath; if (this.diffForPath) { this.collapsedContent = this.content; @@ -88,6 +88,8 @@ export default class SingleFileDiff { if (cb) cb(); }) - .catch(createFlash(__('An error occurred while retrieving diff'))); + .catch(() => { + createFlash(__('An error occurred while retrieving diff')); + }); } } diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index a98403f4cf2..ce0fd3f6ff8 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,12 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */ /* global ace */ -(function() { - $(function() { - var editor = ace.edit("editor"); +export default () => { + const editor = ace.edit('editor'); - $(".snippet-form-holder form").on('submit', function() { - $(".snippet-file-content").val(editor.getValue()); - }); + $('.snippet-form-holder form').on('submit', () => { + $('.snippet-file-content').val(editor.getValue()); }); -}).call(window); +}; diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index d5606e153f6..3deb629d5f2 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -1,17 +1,20 @@ import Flash from './flash'; import { __, s__ } from './locale'; import { spriteIcon } from './lib/utils/common_utils'; +import axios from './lib/utils/axios_utils'; export default class Star { constructor() { - $('.project-home-panel .toggle-star') - .on('ajax:success', function handleSuccess(e, data) { - const $this = $(this); - const $starSpan = $this.find('span'); - const $startIcon = $this.find('svg'); + $('.project-home-panel .toggle-star').on('click', function toggleStarClickCallback() { + const $this = $(this); + const $starSpan = $this.find('span'); + const $startIcon = $this.find('svg'); - function toggleStar(isStarred) { + axios.post($this.data('endpoint')) + .then(({ data }) => { + const isStarred = $starSpan.hasClass('starred'); $this.parent().find('.star-count').text(data.star_count); + if (isStarred) { $starSpan.removeClass('starred').text(s__('StarProject|Star')); $startIcon.remove(); @@ -21,12 +24,8 @@ export default class Star { $startIcon.remove(); $this.prepend(spriteIcon('star')); } - } - - toggleStar($starSpan.hasClass('starred')); - }) - .on('ajax:error', () => { - Flash('Star toggle failed. Try again later.', 'alert'); - }); + }) + .catch(() => Flash('Star toggle failed. Try again later.')); + }); } } diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js index 1ab4c2229ca..3ed064f87a9 100644 --- a/app/assets/javascripts/subscription_select.js +++ b/app/assets/javascripts/subscription_select.js @@ -1,6 +1,6 @@ export default function subscriptionSelect() { $('.js-subscription-event').each((i, element) => { - const fieldName = $(element).data('field-name'); + const fieldName = $(element).data('fieldName'); return $(element).glDropdown({ selectable: true, diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js index 129a551cbcd..8fa78b636f8 100644 --- a/app/assets/javascripts/task_list.js +++ b/app/assets/javascripts/task_list.js @@ -40,7 +40,7 @@ export default class TaskList { [this.fieldName]: $target.val(), }; - return axios.patch($target.data('update-url') || $('form.js-issuable-update').attr('action'), patchData) + return axios.patch($target.data('updateUrl') || $('form.js-issuable-update').attr('action'), patchData) .then(({ data }) => this.onSuccess(data)) .catch(err => this.onError(err)); } diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js index 4cc1c96b870..b5b64f44a11 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js +++ b/app/assets/javascripts/templates/issuable_template_selector.js @@ -6,9 +6,9 @@ import TemplateSelector from '../blob/template_selector'; export default class IssuableTemplateSelector extends TemplateSelector { constructor(...args) { super(...args); - this.projectPath = this.dropdown.data('project-path'); - this.namespacePath = this.dropdown.data('namespace-path'); - this.issuableType = this.$dropdownContainer.data('issuable-type'); + this.projectPath = this.dropdown.data('projectPath'); + this.namespacePath = this.dropdown.data('namespacePath'); + this.issuableType = this.$dropdownContainer.data('issuableType'); this.titleInput = $(`#${this.issuableType}_title`); const initialQuery = { diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js index a3cc04e35fe..fd42f9c3baa 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js @@ -1,7 +1,5 @@ -/* eslint-disable func-names, wrap-iife */ -/* global u2f */ import _ from 'underscore'; -import isU2FSupported from './util'; +import importU2FLibrary from './util'; import U2FError from './error'; // Authenticate U2F (universal 2nd factor) devices for users to authenticate with. @@ -10,6 +8,7 @@ import U2FError from './error'; // State Flow #2: setup -> in_progress -> error -> setup export default class U2FAuthenticate { constructor(container, form, u2fParams, fallbackButton, fallbackUI) { + this.u2fUtils = null; this.container = container; this.renderNotSupported = this.renderNotSupported.bind(this); this.renderAuthenticated = this.renderAuthenticated.bind(this); @@ -50,22 +49,23 @@ export default class U2FAuthenticate { } start() { - if (isU2FSupported()) { - return this.renderInProgress(); - } - return this.renderNotSupported(); + return importU2FLibrary() + .then((utils) => { + this.u2fUtils = utils; + this.renderInProgress(); + }) + .catch(() => this.renderNotSupported()); } authenticate() { - return u2f.sign(this.appId, this.challenge, this.signRequests, (function (_this) { - return function (response) { + return this.u2fUtils.sign(this.appId, this.challenge, this.signRequests, + (response) => { if (response.errorCode) { const error = new U2FError(response.errorCode, 'authenticate'); - return _this.renderError(error); + return this.renderError(error); } - return _this.renderAuthenticated(JSON.stringify(response)); - }; - })(this), 10); + return this.renderAuthenticated(JSON.stringify(response)); + }, 10); } renderTemplate(name, params) { diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index cc3f02e75f6..869fac658e8 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -1,8 +1,5 @@ -/* eslint-disable func-names, wrap-iife */ -/* global u2f */ - import _ from 'underscore'; -import isU2FSupported from './util'; +import importU2FLibrary from './util'; import U2FError from './error'; // Register U2F (universal 2nd factor) devices for users to authenticate with. @@ -11,6 +8,7 @@ import U2FError from './error'; // State Flow #2: setup -> in_progress -> error -> setup export default class U2FRegister { constructor(container, u2fParams) { + this.u2fUtils = null; this.container = container; this.renderNotSupported = this.renderNotSupported.bind(this); this.renderRegistered = this.renderRegistered.bind(this); @@ -34,22 +32,23 @@ export default class U2FRegister { } start() { - if (isU2FSupported()) { - return this.renderSetup(); - } - return this.renderNotSupported(); + return importU2FLibrary() + .then((utils) => { + this.u2fUtils = utils; + this.renderSetup(); + }) + .catch(() => this.renderNotSupported()); } register() { - return u2f.register(this.appId, this.registerRequests, this.signRequests, (function (_this) { - return function (response) { + return this.u2fUtils.register(this.appId, this.registerRequests, this.signRequests, + (response) => { if (response.errorCode) { const error = new U2FError(response.errorCode, 'register'); - return _this.renderError(error); + return this.renderError(error); } - return _this.renderRegistered(JSON.stringify(response)); - }; - })(this), 10); + return this.renderRegistered(JSON.stringify(response)); + }, 10); } renderTemplate(name, params) { diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js index 9771ff935c2..5778f00332d 100644 --- a/app/assets/javascripts/u2f/util.js +++ b/app/assets/javascripts/u2f/util.js @@ -1,3 +1,41 @@ -export default function isU2FSupported() { - return window.u2f; +function isOpera(userAgent) { + return userAgent.indexOf('Opera') >= 0 || userAgent.indexOf('OPR') >= 0; +} + +function getOperaVersion(userAgent) { + const match = userAgent.match(/OPR[^0-9]*([0-9]+)[^0-9]+/); + return match ? parseInt(match[1], 10) : false; +} + +function isChrome(userAgent) { + return userAgent.indexOf('Chrom') >= 0 && !isOpera(userAgent); +} + +function getChromeVersion(userAgent) { + const match = userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./); + return match ? parseInt(match[1], 10) : false; +} + +export function canInjectU2fApi(userAgent) { + const isSupportedChrome = isChrome(userAgent) && getChromeVersion(userAgent) >= 41; + const isSupportedOpera = isOpera(userAgent) && getOperaVersion(userAgent) >= 40; + const isMobile = ( + userAgent.indexOf('droid') >= 0 || + userAgent.indexOf('CriOS') >= 0 || + /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) + ); + return (isSupportedChrome || isSupportedOpera) && !isMobile; +} + +export default function importU2FLibrary() { + if (window.u2f) { + return Promise.resolve(window.u2f); + } + + const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : ''; + if (canInjectU2fApi(userAgent) || (gon && gon.test_env)) { + return import(/* webpackMode: "eager" */ 'vendor/u2f').then(() => window.u2f); + } + + return Promise.reject(); } diff --git a/app/assets/javascripts/ui_development_kit.js b/app/assets/javascripts/ui_development_kit.js index f503076715c..78dda172ee6 100644 --- a/app/assets/javascripts/ui_development_kit.js +++ b/app/assets/javascripts/ui_development_kit.js @@ -1,6 +1,6 @@ import Api from './api'; -document.addEventListener('DOMContentLoaded', () => { +export default () => { $('#js-project-dropdown').glDropdown({ data: (term, callback) => { Api.projects(term, { @@ -19,4 +19,4 @@ document.addEventListener('DOMContentLoaded', () => { id: data => data.id, isSelected: data => (data.id === 2), }); -}); +}; diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index a45b22f3084..a783122d500 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -22,7 +22,7 @@ export default class UserCallout { const $currentTarget = $(e.currentTarget); if (this.options.setCalloutPerProject) { - Cookies.set(this.cookieName, 'true', { expires: 365, path: this.userCalloutBody.data('project-path') }); + Cookies.set(this.cookieName, 'true', { expires: 365, path: this.userCalloutBody.data('projectPath') }); } else { Cookies.set(this.cookieName, 'true', { expires: 365 }); } diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index eaed81cf79e..3385aba0279 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -34,23 +34,22 @@ function UsersSelect(currentUser, els, options = {}) { var options = {}; var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, defaultNullUser, firstUser, issueURL, selectedId, selectedIdDefault, showAnyUser, showNullUser, showMenuAbove; $dropdown = $(dropdown); - options.projectId = $dropdown.data('project-id'); - options.groupId = $dropdown.data('group-id'); - options.showCurrentUser = $dropdown.data('current-user'); - options.todoFilter = $dropdown.data('todo-filter'); - options.todoStateFilter = $dropdown.data('todo-state-filter'); - options.perPage = $dropdown.data('per-page'); - showNullUser = $dropdown.data('null-user'); - defaultNullUser = $dropdown.data('null-user-default'); + options.projectId = $dropdown.data('projectId'); + options.groupId = $dropdown.data('groupId'); + options.showCurrentUser = $dropdown.data('currentUser'); + options.todoFilter = $dropdown.data('todoFilter'); + options.todoStateFilter = $dropdown.data('todoStateFilter'); + showNullUser = $dropdown.data('nullUser'); + defaultNullUser = $dropdown.data('nullUserDefault'); showMenuAbove = $dropdown.data('showMenuAbove'); - showAnyUser = $dropdown.data('any-user'); - firstUser = $dropdown.data('first-user'); - options.authorId = $dropdown.data('author-id'); - defaultLabel = $dropdown.data('default-label'); + showAnyUser = $dropdown.data('anyUser'); + firstUser = $dropdown.data('firstUser'); + options.authorId = $dropdown.data('authorId'); + defaultLabel = $dropdown.data('defaultLabel'); issueURL = $dropdown.data('issueUpdate'); $selectbox = $dropdown.closest('.selectbox'); $block = $selectbox.closest('.block'); - abilityName = $dropdown.data('ability-name'); + abilityName = $dropdown.data('abilityName'); $value = $block.find('.value'); $collapsedSidebar = $block.find('.sidebar-collapsed-user'); $loading = $block.find('.block-loading').fadeOut(); @@ -63,7 +62,7 @@ function UsersSelect(currentUser, els, options = {}) { const assignYourself = function () { const unassignedSelected = $dropdown.closest('.selectbox') - .find(`input[name='${$dropdown.data('field-name')}'][value=0]`); + .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`); if (unassignedSelected) { unassignedSelected.remove(); @@ -72,7 +71,7 @@ function UsersSelect(currentUser, els, options = {}) { // Save current selected user to the DOM const input = document.createElement('input'); input.type = 'hidden'; - input.name = $dropdown.data('field-name'); + input.name = $dropdown.data('fieldName'); const currentUserInfo = $dropdown.data('currentUserInfo'); @@ -96,7 +95,7 @@ function UsersSelect(currentUser, els, options = {}) { const getSelectedUserInputs = function() { return $selectbox - .find(`input[name="${$dropdown.data('field-name')}"]`); + .find(`input[name="${$dropdown.data('fieldName')}"]`); }; const getSelected = function() { @@ -106,14 +105,14 @@ function UsersSelect(currentUser, els, options = {}) { }; const checkMaxSelect = function() { - const maxSelect = $dropdown.data('max-select'); + const maxSelect = $dropdown.data('maxSelect'); if (maxSelect) { const selected = getSelected(); if (selected.length > maxSelect) { const firstSelectedId = selected[0]; const firstSelected = $dropdown.closest('.selectbox') - .find(`input[name='${$dropdown.data('field-name')}'][value=${firstSelectedId}]`); + .find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`); firstSelected.remove(); emitSidebarEvent('sidebar.removeAssignee', { @@ -158,7 +157,7 @@ function UsersSelect(currentUser, els, options = {}) { const currentUserInfo = $dropdown.data('currentUserInfo'); $dropdown.find('.dropdown-toggle-text').text(getMultiSelectDropdownTitle(currentUserInfo)).removeClass('is-default'); } else { - const $input = $(`input[name="${$dropdown.data('field-name')}"]`); + const $input = $(`input[name="${$dropdown.data('fieldName')}"]`); $input.val(gon.current_user_id); selectedId = $input.val(); $dropdown.find('.dropdown-toggle-text').text(gon.current_user_fullname).removeClass('is-default'); @@ -293,10 +292,10 @@ function UsersSelect(currentUser, els, options = {}) { const selected = getSelected().filter(i => i !== 0); if (selected.length > 0) { - if ($dropdown.data('dropdown-header')) { + if ($dropdown.data('dropdownHeader')) { showDivider += 1; users.splice(showDivider, 0, { - header: $dropdown.data('dropdown-header'), + header: $dropdown.data('dropdownHeader'), }); } @@ -327,7 +326,7 @@ function UsersSelect(currentUser, els, options = {}) { fields: ['name', 'username'] }, selectable: true, - fieldName: $dropdown.data('field-name'), + fieldName: $dropdown.data('fieldName'), toggleLabel: function(selected, el, glDropdown) { const inputValue = glDropdown.filterInput.val(); @@ -362,7 +361,7 @@ function UsersSelect(currentUser, els, options = {}) { emitSidebarEvent('sidebar.saveAssignees'); } - if (!$dropdown.data('always-show-selectbox')) { + if (!$dropdown.data('alwaysShowSelectbox')) { $selectbox.hide(); // Recalculate where .value is because vue might have changed it @@ -373,7 +372,7 @@ function UsersSelect(currentUser, els, options = {}) { } }, multiSelect: $dropdown.hasClass('js-multiselect'), - inputMeta: $dropdown.data('input-meta'), + inputMeta: $dropdown.data('inputMeta'), clicked: function(options) { const { $el, e, isMarking } = options; const user = options.selectedObj; @@ -381,7 +380,7 @@ function UsersSelect(currentUser, els, options = {}) { if ($dropdown.hasClass('js-multiselect')) { const isActive = $el.hasClass('is-active'); const previouslySelected = $dropdown.closest('.selectbox') - .find("input[name='" + ($dropdown.data('field-name')) + "'][value!=0]"); + .find("input[name='" + ($dropdown.data('fieldName')) + "'][value!=0]"); // Enables support for limiting the number of users selected // Automatically removes the first on the list if more users are selected @@ -400,7 +399,7 @@ function UsersSelect(currentUser, els, options = {}) { // Remove unassigned selection (if it was previously selected) const unassignedSelected = $dropdown.closest('.selectbox') - .find("input[name='" + ($dropdown.data('field-name')) + "'][value=0]"); + .find("input[name='" + ($dropdown.data('fieldName')) + "'][value=0]"); if (unassignedSelected) { unassignedSelected.remove(); @@ -408,7 +407,7 @@ function UsersSelect(currentUser, els, options = {}) { } else { if (previouslySelected.length === 0) { // Select unassigned because there is no more selected users - this.addInput($dropdown.data('field-name'), 0, {}); + this.addInput($dropdown.data('fieldName'), 0, {}); } // User unselected @@ -440,7 +439,7 @@ function UsersSelect(currentUser, els, options = {}) { return; } if ($el.closest('.add-issues-modal').length) { - gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; + gl.issueBoards.ModalStore.store.filter[$dropdown.data('fieldName')] = user.id; } else if (handleClick) { e.preventDefault(); handleClick(user, isMarking); @@ -449,15 +448,15 @@ function UsersSelect(currentUser, els, options = {}) { } else if ($dropdown.hasClass('js-filter-submit')) { return $dropdown.closest('form').submit(); } else if (!$dropdown.hasClass('js-multiselect')) { - selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val(); + selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('fieldName')) + "']").val(); return assignTo(selected); } // Automatically close dropdown after assignee is selected // since CE has no multiple assignees // EE does not have a max-select - if ($dropdown.data('max-select') && - getSelected().length === $dropdown.data('max-select')) { + if ($dropdown.data('maxSelect') && + getSelected().length === $dropdown.data('maxSelect')) { // Close the dropdown $dropdown.dropdown('toggle'); } @@ -469,7 +468,7 @@ function UsersSelect(currentUser, els, options = {}) { const $el = $(e.currentTarget); const selected = getSelected(); if ($dropdown.hasClass('js-issue-board-sidebar') && selected.length === 0) { - this.addInput($dropdown.data('field-name'), 0, {}); + this.addInput($dropdown.data('fieldName'), 0, {}); } $el.find('.is-active').removeClass('is-active'); @@ -485,7 +484,7 @@ function UsersSelect(currentUser, els, options = {}) { highlightSelected(selectedId); } }, - updateLabel: $dropdown.data('dropdown-title'), + updateLabel: $dropdown.data('dropdownTitle'), renderRow: function(user) { var avatar, img, listClosingTags, listWithName, listWithUserName, username; username = user.username ? "@" + user.username : ""; @@ -533,15 +532,15 @@ function UsersSelect(currentUser, els, options = {}) { var firstUser, showAnyUser, showEmailUser, showNullUser; var options = {}; options.skipLdap = $(select).hasClass('skip_ldap'); - options.projectId = $(select).data('project-id'); - options.groupId = $(select).data('group-id'); - options.showCurrentUser = $(select).data('current-user'); - options.authorId = $(select).data('author-id'); - options.skipUsers = $(select).data('skip-users'); - showNullUser = $(select).data('null-user'); - showAnyUser = $(select).data('any-user'); - showEmailUser = $(select).data('email-user'); - firstUser = $(select).data('first-user'); + options.projectId = $(select).data('projectId'); + options.groupId = $(select).data('groupId'); + options.showCurrentUser = $(select).data('currentUser'); + options.authorId = $(select).data('authorId'); + options.skipUsers = $(select).data('skipUsers'); + showNullUser = $(select).data('nullUser'); + showAnyUser = $(select).data('anyUser'); + showEmailUser = $(select).data('emailUser'); + firstUser = $(select).data('firstUser'); return $(select).select2({ placeholder: "Search for a user", multiple: $(select).hasClass('multiselect'), @@ -669,7 +668,6 @@ UsersSelect.prototype.users = function(query, options, callback) { const url = this.buildUrl(this.usersPath); const params = { search: query, - per_page: options.perPage || 20, active: true, project_id: options.projectId || null, group_id: options.groupId || null, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index 109a302a172..54a98abf860 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -1,8 +1,8 @@ <script> /* eslint-disable vue/require-default-prop */ - import pipelineStage from '../../pipelines/components/stage.vue'; - import ciIcon from '../../vue_shared/components/ci_icon.vue'; - import icon from '../../vue_shared/components/icon.vue'; + import pipelineStage from '~/pipelines/components/stage.vue'; + import ciIcon from '~/vue_shared/components/ci_icon.vue'; + import icon from '~/vue_shared/components/icon.vue'; export default { name: 'MRWidgetPipeline', diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue index 40c3cb500bb..ebaf2b972eb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue @@ -44,7 +44,10 @@ type="button" class="btn btn-xs btn-default" > - <loading-icon v-if="isRefreshing" /> + <loading-icon + v-if="isRefreshing" + :inline="true" + /> {{ s__("mrWidget|Refresh") }} </button> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js deleted file mode 100644 index 7733fb74afe..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js +++ /dev/null @@ -1,43 +0,0 @@ -import statusIcon from '../mr_widget_status_icon.vue'; -import tooltip from '../../../vue_shared/directives/tooltip'; -import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue'; - -export default { - name: 'MRWidgetMissingBranch', - props: { - mr: { type: Object, required: true }, - }, - directives: { - tooltip, - }, - components: { - 'mr-widget-merge-help': mrWidgetMergeHelp, - statusIcon, - }, - computed: { - missingBranchName() { - return this.mr.sourceBranchRemoved ? 'source' : 'target'; - }, - message() { - return `If the ${this.missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line`; - }, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="warning" :show-disabled-button="true" /> - <div class="media-body space-children"> - <span class="bold js-branch-text"> - <span class="capitalize"> - {{missingBranchName}} - </span> branch does not exist. - Please restore it or use a different {{missingBranchName}} branch - <i - v-tooltip - class="fa fa-question-circle" - :title="message" - :aria-label="message"></i> - </span> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue new file mode 100644 index 00000000000..718c0e4b3c6 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue @@ -0,0 +1,62 @@ +<script> + import { sprintf, s__ } from '~/locale'; + import tooltip from '~/vue_shared/directives/tooltip'; + import statusIcon from '../mr_widget_status_icon.vue'; + import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue'; + + export default { + name: 'MRWidgetMissingBranch', + directives: { + tooltip, + }, + components: { + mrWidgetMergeHelp, + statusIcon, + }, + props: { + mr: { + type: Object, + required: true, + }, + }, + computed: { + missingBranchName() { + return this.mr.sourceBranchRemoved ? 'source' : 'target'; + }, + missingBranchNameMessage() { + return sprintf(s__('mrWidget| Please restore it or use a different %{missingBranchName} branch'), { + missingBranchName: this.missingBranchName, + }); + }, + message() { + return sprintf(s__('mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line'), { + missingBranchName: this.missingBranchName, + }); + }, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon + status="warning" + :show-disabled-button="true" + /> + + <div class="media-body space-children"> + <span class="bold js-branch-text"> + <span class="capitalize"> + {{ missingBranchName }} + </span> {{ s__("mrWidget|branch does not exist.") }} + {{ missingBranchNameMessage }} + <i + v-tooltip + class="fa fa-question-circle" + :title="message" + :aria-label="message" + > + </i> + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js deleted file mode 100644 index cea3d97fa88..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js +++ /dev/null @@ -1,19 +0,0 @@ -import statusIcon from '../mr_widget_status_icon.vue'; - -export default { - name: 'MRWidgetNotAllowed', - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="success" :show-disabled-button="true" /> - <div class="media-body space-children"> - <span class="bold"> - Ready to be merged automatically. - Ask someone with write access to this repository to merge this request - </span> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue new file mode 100644 index 00000000000..e4af50b09f8 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue @@ -0,0 +1,25 @@ +<script> + import StatusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetNotAllowed', + components: { + StatusIcon, + }, + }; +</script> + +<template> + <div class="mr-widget-body media"> + <status-icon + status="success" + :show-disabled-button="true" + /> + <div class="media-body space-children"> + <span class="bold"> + {{ s__(`mrWidget|Ready to be merged automatically. +Ask someone with write access to this repository to merge this request`) }} + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js deleted file mode 100644 index e66ce071ab4..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js +++ /dev/null @@ -1,18 +0,0 @@ -import statusIcon from '../mr_widget_status_icon.vue'; - -export default { - name: 'MRWidgetPipelineBlocked', - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="warning" :show-disabled-button="true" /> - <div class="media-body space-children"> - <span class="bold"> - Pipeline blocked. The pipeline for this merge request requires a manual action to proceed - </span> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue new file mode 100644 index 00000000000..6d7cc03f7ad --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue @@ -0,0 +1,24 @@ +<script> + import StatusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetPipelineBlocked', + components: { + StatusIcon, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon + status="warning" + :show-disabled-button="true" + /> + <div class="media-body space-children"> + <span class="bold"> + {{ s__(`mrWidget|Pipeline blocked. +The pipeline for this merge request requires a manual action to proceed`) }} + </span> + </div> + </div> +</template> 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 7ba6c29006a..162f048aac7 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 @@ -227,7 +227,8 @@ export default { @click="handleMergeButtonClick()" :disabled="isMergeButtonDisabled" :class="mergeButtonClass" - type="button"> + type="button" + class="qa-merge-button"> <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 2968af0d5cb..143fd328d88 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -107,10 +107,11 @@ <template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest"> <div class="accept-merge-holder clearfix -js-toggle-container accept-action media space-children"> +js-toggle-container accept-action media space-children" + > <button type="button" - class="btn btn-sm btn-reopen btn-success" + class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button" :disabled="isMakingRequest" @click="rebase" > diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js index 7ca15537719..edb3baa39e4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js @@ -24,12 +24,12 @@ export { default as WipState } from './components/states/mr_widget_wip'; export { default as ArchivedState } from './components/states/mr_widget_archived.vue'; export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue'; export { default as NothingToMergeState } from './components/states/mr_widget_nothing_to_merge'; -export { default as MissingBranchState } from './components/states/mr_widget_missing_branch'; -export { default as NotAllowedState } from './components/states/mr_widget_not_allowed'; +export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue'; +export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue'; export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge'; export { default as SHAMismatchState } from './components/states/mr_widget_sha_mismatch'; export { default as UnresolvedDiscussionsState } from './components/states/mr_widget_unresolved_discussions'; -export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked'; +export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue'; export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed'; export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue'; export { default as RebaseState } from './components/states/mr_widget_rebase.vue'; diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index 6b9918b65b0..69a9132a2da 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -6,7 +6,7 @@ import Translate from '../vue_shared/translate'; Vue.use(Translate); -document.addEventListener('DOMContentLoaded', () => { +export default () => { gl.mrWidgetData.gitlabLogo = gon.gitlab_logo; const vm = new Vue(mrWidgetOptions); @@ -14,4 +14,4 @@ document.addEventListener('DOMContentLoaded', () => { window.gl.mrWidget = { checkStatus: vm.checkStatus, }; -}); +}; 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 edf67fcd0a7..797f0f6ec0f 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 @@ -96,9 +96,7 @@ export default { cb.call(null, data); } }) - .catch(() => { - new Flash('Something went wrong. Please try again.'); // eslint-disable-line - }); + .catch(() => new Flash('Something went wrong. Please try again.')); }, initPolling() { this.pollingInterval = new SmartInterval({ @@ -146,12 +144,11 @@ export default { Project.initRefSwitcher(); } }) - .catch(() => { - new Flash('Something went wrong. Please try again.'); // eslint-disable-line - }); + .catch(() => new Flash('Something went wrong. Please try again.')); }, handleNotification(data) { if (data.ci_status === this.mr.ciStatus) return; + if (!data.pipeline) return; const label = data.pipeline.details.status.label; const title = `Pipeline ${label}`; 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 ed004b3bb08..9a750ce42bd 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 @@ -4,7 +4,6 @@ import { stateKey } from './state_maps'; import { formatDate } from '../../lib/utils/datetime_utility'; export default class MergeRequestStore { - constructor(data) { this.sha = data.diff_head_sha; this.gitlabLogo = data.gitlabLogo; @@ -169,5 +168,4 @@ export default class MergeRequestStore { return timeagoInstance.format(date); } - } diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue index 31d9b9d9c48..3b6c2da1664 100644 --- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -1,8 +1,8 @@ <script> - import tooltip from '../directives/tooltip'; /** * Falls back to the code used in `copy_to_clipboard.js` */ + import tooltip from '../directives/tooltip'; export default { name: 'ClipboardButton', @@ -28,6 +28,11 @@ required: false, default: false, }, + cssClass: { + type: String, + required: false, + default: 'btn btn-default btn-transparent btn-clipboard', + }, }, }; </script> @@ -35,7 +40,7 @@ <template> <button type="button" - class="btn btn-transparent btn-clipboard" + :class="cssClass" :title="title" :data-clipboard-text="text" v-tooltip diff --git a/app/assets/javascripts/vue_shared/components/confirmation_input.vue b/app/assets/javascripts/vue_shared/components/confirmation_input.vue deleted file mode 100644 index 1aa03ea6317..00000000000 --- a/app/assets/javascripts/vue_shared/components/confirmation_input.vue +++ /dev/null @@ -1,62 +0,0 @@ -<script> - import _ from 'underscore'; - import { __, sprintf } from '~/locale'; - - export default { - props: { - inputId: { - type: String, - required: true, - }, - confirmationKey: { - type: String, - required: true, - }, - confirmationValue: { - type: String, - required: true, - }, - shouldEscapeConfirmationValue: { - type: Boolean, - required: false, - default: true, - }, - }, - computed: { - inputLabel() { - let value = this.confirmationValue; - if (this.shouldEscapeConfirmationValue) { - value = _.escape(value); - } - - return sprintf( - __('Type %{value} to confirm:'), - { value: `<code>${value}</code>` }, - false, - ); - }, - }, - methods: { - hasCorrectValue() { - return this.$refs.enteredValue.value === this.confirmationValue; - }, - }, - }; -</script> - -<template> - <div> - <label - v-html="inputLabel" - :for="inputId" - > - </label> - <input - :id="inputId" - :name="confirmationKey" - type="text" - ref="enteredValue" - class="form-control" - /> - </div> -</template> diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue index 3595a9389e9..c943c8d98a4 100644 --- a/app/assets/javascripts/vue_shared/components/expand_button.vue +++ b/app/assets/javascripts/vue_shared/components/expand_button.vue @@ -39,7 +39,7 @@ @click="onClick"> ... </button> - <span v-show="!isCollapsed"> + <span v-if="!isCollapsed"> <slot name="expanded"></slot> </span> </span> diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue new file mode 100644 index 00000000000..67c9181c7b1 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue @@ -0,0 +1,106 @@ +<script> + const buttonVariants = [ + 'danger', + 'primary', + 'success', + 'warning', + ]; + + export default { + name: 'GlModal', + + props: { + id: { + type: String, + required: false, + default: null, + }, + headerTitleText: { + type: String, + required: false, + default: '', + }, + footerPrimaryButtonVariant: { + type: String, + required: false, + default: 'primary', + validator: value => buttonVariants.indexOf(value) !== -1, + }, + footerPrimaryButtonText: { + type: String, + required: false, + default: '', + }, + }, + + methods: { + emitCancel(event) { + this.$emit('cancel', event); + }, + emitSubmit(event) { + this.$emit('submit', event); + }, + }, + }; +</script> + +<template> + <div + :id="id" + class="modal fade" + tabindex="-1" + role="dialog" + > + <div + class="modal-dialog" + role="document" + > + <div class="modal-content"> + <div class="modal-header"> + <slot name="header"> + <button + type="button" + class="close" + data-dismiss="modal" + :aria-label="s__('Modal|Close')" + @click="emitCancel($event)" + > + <span aria-hidden="true">×</span> + </button> + <h4 class="modal-title"> + <slot name="title"> + {{ headerTitleText }} + </slot> + </h4> + </slot> + </div> + + <div class="modal-body"> + <slot></slot> + </div> + + <div class="modal-footer"> + <slot name="footer"> + <button + type="button" + class="btn" + data-dismiss="modal" + @click="emitCancel($event)" + > + {{ s__('Modal|Cancel') }} + </button> + <button + type="button" + class="btn" + :class="`btn-${footerPrimaryButtonVariant}`" + data-dismiss="modal" + @click="emitSubmit($event)" + > + {{ footerPrimaryButtonText }} + </button> + </slot> + </div> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 1f72dea1b33..a0cd0cbd200 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -6,12 +6,12 @@ import userAvatarImage from './user_avatar/user_avatar_image.vue'; /** - * Renders header component for job and pipeline page based on UI mockups - * - * Used in: - * - job show page - * - pipeline show page - */ + * Renders header component for job and pipeline page based on UI mockups + * + * Used in: + * - job show page + * - pipeline show page + */ export default { components: { ciIconBadge, @@ -118,7 +118,8 @@ <section class="header-action-buttons" - v-if="actions.length"> + v-if="actions.length" + > <template v-for="(action, i) in actions" > diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue index b48828ae81f..3d39b3ab173 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue @@ -11,14 +11,12 @@ default: false, required: false, }, - isConfidential: { type: Boolean, default: false, required: false, }, }, - computed: { warningIcon() { if (this.isConfidential) return 'eye-slash'; @@ -26,7 +24,6 @@ return ''; }, - isLockedAndConfidential() { return this.isConfidential && this.isLocked; }, diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index ff8c0f7c1d2..e832d94d32f 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -1,6 +1,5 @@ <script> /* eslint-disable vue/require-default-prop */ - /* This is a re-usable vue component for rendering a button that will probably be sending off ajax requests and need to show the loading status by setting the `loading` option. @@ -40,7 +39,7 @@ required: false, }, containerClass: { - type: String, + type: [String, Array, Object], required: false, default: 'btn btn-align-content', }, diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 1371dca0c35..d2e968a8419 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -64,7 +64,7 @@ return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete); }, beforeDestroy() { - const glForm = $(this.$refs['gl-form']).data('gl-form'); + const glForm = $(this.$refs['gl-form']).data('glForm'); if (glForm) { glForm.destroy(); } diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index f65eab11a27..177d2cfc8da 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -65,7 +65,8 @@ </li> <li class="md-header-tab" - :class="{ active: previewMarkdown }"> + :class="{ active: previewMarkdown }" + > <a class="js-preview-link" href="#md-preview-holder" diff --git a/app/assets/javascripts/vue_shared/components/modal.vue b/app/assets/javascripts/vue_shared/components/modal.vue index 8227428d8ba..5f1364421aa 100644 --- a/app/assets/javascripts/vue_shared/components/modal.vue +++ b/app/assets/javascripts/vue_shared/components/modal.vue @@ -46,6 +46,11 @@ required: false, default: '', }, + secondaryButtonLabel: { + type: String, + required: false, + default: '', + }, submitDisabled: { type: Boolean, required: false, @@ -129,6 +134,21 @@ > {{ closeButtonLabel }} </button> + + <slot + v-if="secondaryButtonLabel" + name="secondary-button" + > + <button + v-if="secondaryButtonLabel" + type="button" + class="btn" + data-dismiss="modal" + > + {{ secondaryButtonLabel }} + </button> + </slot> + <button v-if="primaryButtonLabel" type="button" diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue new file mode 100644 index 00000000000..80e3db52cb0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue @@ -0,0 +1,24 @@ +<template> + <li class="timeline-entry note"> + <div class="timeline-entry-inner"> + <div class="timeline-icon"> + </div> + <div class="timeline-content"> + <div class="note-header"></div> + <div class="note-body"> + <skeleton-loading-container /> + </div> + </div> + </div> + </li> +</template> + +<script> + import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; + + export default { + components: { + skeletonLoadingContainer, + }, + }; +</script> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue index 1413dd69f24..3fcacd156c5 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -14,6 +14,11 @@ collapsedCalendarIcon, }, props: { + blockClass: { + type: String, + required: false, + default: '', + }, collapsed: { type: Boolean, required: false, @@ -91,7 +96,10 @@ </script> <template> - <div class="block"> + <div + class="block" + :class="blockClass" + > <div class="issuable-sidebar-header"> <toggle-sidebar :collapsed="collapsed" diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue index c44c606a8b2..22fc5757447 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.vue +++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue @@ -13,6 +13,12 @@ props: { /** This function will take the information given by the pagination component + + Here is an example `change` method: + + change(pagenum) { + gl.utils.visitUrl(`?page=${pagenum}`); + }, */ change: { type: Function, diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 887879ab715..2fccfa4011c 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -21,7 +21,7 @@ @import "framework/flash"; @import "framework/forms"; @import "framework/gfm"; -@import "framework/gitlab-theme"; +@import "framework/gitlab_theme"; @import "framework/header"; @import "framework/highlight"; @import "framework/issue_box"; @@ -35,10 +35,10 @@ @import "framework/pagination"; @import "framework/panels"; @import "framework/popup"; -@import "framework/secondary-navigation-elements"; +@import "framework/secondary_navigation_elements"; @import "framework/selects"; @import "framework/sidebar"; -@import "framework/contextual-sidebar"; +@import "framework/contextual_sidebar"; @import "framework/tables"; @import "framework/notes"; @import "framework/tabs"; @@ -49,16 +49,16 @@ @import "framework/zen"; @import "framework/blank"; @import "framework/wells"; -@import "framework/page-header"; +@import "framework/page_header"; @import "framework/awards"; @import "framework/images"; -@import "framework/broadcast-messages"; +@import "framework/broadcast_messages"; @import "framework/emojis"; -@import "framework/emoji-sprites"; +@import "framework/emoji_sprites"; @import "framework/icons"; @import "framework/snippets"; @import "framework/memory_graph"; @import "framework/responsive_tables"; -@import "framework/stacked-progress-bar"; +@import "framework/stacked_progress_bar"; @import "framework/ci_variable_list"; @import "framework/feature_highlight"; diff --git a/app/assets/stylesheets/framework/broadcast-messages.scss b/app/assets/stylesheets/framework/broadcast_messages.scss index 9b54fb94cdc..9b54fb94cdc 100644 --- a/app/assets/stylesheets/framework/broadcast-messages.scss +++ b/app/assets/stylesheets/framework/broadcast_messages.scss diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index c4b046a6d68..6b89387ab5f 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -444,6 +444,19 @@ } } +.btn-missing { + color: $notes-light-color; + border: 1px dashed $border-gray-normal-dashed; + border-radius: $border-radius-default; + + &:hover, + &:active, + &:focus { + color: $notes-light-color; + background-color: $white-normal; + } +} + .btn-svg svg { @include btn-svg; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 73524d5cf60..ae517c41cb2 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -449,9 +449,11 @@ img.emoji { .prepend-top-10 { margin-top: 10px; } .prepend-top-15 { margin-top: 15px; } .prepend-top-default { margin-top: $gl-padding !important; } +.prepend-top-16 { margin-top: 16px; } .prepend-top-20 { margin-top: 20px; } .prepend-left-4 { margin-left: 4px; } .prepend-left-5 { margin-left: 5px; } +.prepend-left-8 { margin-left: 8px; } .prepend-left-10 { margin-left: 10px; } .prepend-left-default { margin-left: $gl-padding; } .prepend-left-20 { margin-left: 20px; } diff --git a/app/assets/stylesheets/framework/contextual-sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index 1acde98c3ae..1acde98c3ae 100644 --- a/app/assets/stylesheets/framework/contextual-sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 691df098c70..1d7b0b602cc 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -736,10 +736,6 @@ } } -.droplab-item-ignore { - pointer-events: none; -} - .pika-single.animate-picker.is-bound, .pika-single.animate-picker.is-bound.is-hidden { /* diff --git a/app/assets/stylesheets/framework/emoji-sprites.scss b/app/assets/stylesheets/framework/emoji_sprites.scss index 0174e17b660..0174e17b660 100644 --- a/app/assets/stylesheets/framework/emoji-sprites.scss +++ b/app/assets/stylesheets/framework/emoji_sprites.scss diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index be96c8ee964..2c30311b1c1 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -167,7 +167,7 @@ label { .input-group { .select2-container { display: table-cell; - width: 200px !important; + max-width: 180px; } .input-group-addon { @@ -182,6 +182,7 @@ label { .help-block { margin-bottom: 0; + margin-top: #{$grid-size / 2}; } .gl-field-error { diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss index db36e27fa74..db36e27fa74 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab_theme.scss diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index dcd98cb522f..7e829826eba 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -255,8 +255,6 @@ ul.controls { } .author_link { - display: inline-block; - .avatar-inline { margin-left: 0; margin-right: 0; diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index a12f28efce6..8604e753c18 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -63,10 +63,6 @@ } } - .project-stats { - display: none; - } - .group-buttons { display: none; } diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page_header.scss index 0c879f40930..0c879f40930 100644 --- a/app/assets/stylesheets/framework/page-header.scss +++ b/app/assets/stylesheets/framework/page_header.scss diff --git a/app/assets/stylesheets/framework/secondary-navigation-elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss index 17c31d6b184..17c31d6b184 100644 --- a/app/assets/stylesheets/framework/secondary-navigation-elements.scss +++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index dbee7073975..b40dcf93969 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -8,7 +8,7 @@ .select2-choice { background: $white-light; border-color: $input-border; - height: 35px; + height: 34px; padding: $gl-vert-padding $gl-input-padding; font-size: $gl-font-size; line-height: 1.42857143; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index d61809cb0a4..d1d98270ad9 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -3,7 +3,6 @@ transition: padding $sidebar-transition-duration; .container-fluid { - background: $white-light; padding: 0 $gl-padding; &.container-blank { diff --git a/app/assets/stylesheets/framework/stacked-progress-bar.scss b/app/assets/stylesheets/framework/stacked_progress_bar.scss index 4869cda73e5..528ba53a48b 100644 --- a/app/assets/stylesheets/framework/stacked-progress-bar.scss +++ b/app/assets/stylesheets/framework/stacked_progress_bar.scss @@ -10,7 +10,7 @@ .status-neutral, .status-red, { height: 100%; - min-width: 25px; + min-width: 30px; padding: 0 5px; font-size: $tooltip-font-size; font-weight: normal; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index d0999e60e65..294c59f037f 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -296,7 +296,7 @@ body { line-height: 1.3; font-size: 1.25em; font-weight: $gl-font-weight-bold; - margin: 12px 7px; + margin: 12px 0; } h1, @@ -333,6 +333,10 @@ a > code { font-family: $monospace_font; } +.weight-normal { + font-weight: $gl-font-weight-normal; +} + .commit-sha, .ref-name { @extend .monospace; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 0d21a9f5f77..a5a8f6d2206 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -214,8 +214,9 @@ $tooltip-font-size: 12px; * Padding */ $gl-padding: 16px; +$gl-padding-8: 8px; +$gl-padding-4: 4px; $gl-col-padding: 15px; -$gl-btn-padding: 10px; $gl-input-padding: 10px; $gl-vert-padding: 6px; $gl-padding-top: 10px; @@ -376,6 +377,10 @@ $inactive-badge-background: rgba(0, 0, 0, .08); $btn-active-gray: #ececec; $btn-active-gray-light: e4e7ed; $btn-white-active: #848484; +$gl-btn-padding: 10px; +$gl-btn-line-height: 16px; +$gl-btn-vert-padding: 8px; +$gl-btn-horz-padding: 12px; /* * Badges @@ -557,6 +562,7 @@ $jq-ui-default-color: #777; /* * Label */ +$label-font-size: 12px; $label-padding: 7px; $label-padding-modal: 10px; $label-gray-bg: #f8fafc; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 17801ed5910..8b680c2dc52 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -196,17 +196,9 @@ @media (min-width: $screen-sm-min) { font-size: 0; - div { - display: inline; - } - .fa-spinner { font-size: 12px; } - - span { - font-size: 6px; - } } .ci-status-link { diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 794bc668562..884665d35c7 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -121,6 +121,10 @@ width: 100%; text-align: left; } + + .environment-child-row { + padding-left: 20px; + } } } @@ -205,7 +209,7 @@ } .prometheus-state { - max-width: 430px; + max-width: 460px; margin: 10px auto; text-align: center; @@ -213,6 +217,10 @@ max-width: 80vw; margin: 0 auto; } + + .state-button { + padding: $gl-padding / 2; + } } .environments-actions { diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index da096354b5a..8871a069d5d 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -72,7 +72,6 @@ .label { color: $gl-text-color; - font-size: inherit; } iframe.twitter-share-button { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 759719a72da..4c9732c26d9 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -102,12 +102,12 @@ .issuable-show-labels { a { - margin-right: 5px; margin-bottom: 5px; + margin-right: 5px; display: inline-block; .color-label { - padding: 6px 10px; + padding: 4px $grid-size; border-radius: $label-border-radius; } @@ -117,6 +117,12 @@ } &.has-labels { + // this font size is a fix to + // prevent unintended spacing between labels + // which shows up when rendering markup has white-space + // characters present. + // see: https://css-tricks.com/fighting-the-space-between-inline-block-elements/#article-header-id-3 + font-size: 0; margin-bottom: -5px; } } @@ -197,11 +203,18 @@ margin-left: 0; } + a.edit-link:not([href]):hover { + color: rgba($avatar-border, .2); + } + + .lock-edit, // uses same style, different js behaviour .edit-link { + @extend .btn-blank; color: $gl-text-color; - &:not([href]):hover { - color: rgba($avatar-border, .2); + &:hover { + text-decoration: underline; + color: $md-link-color; } } } diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index c48e58af691..b9390450477 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -13,10 +13,20 @@ display: inline-block; } + .issuable-meta { + .author_link { + display: inline-block; + } + + .issuable-comments { + height: 18px; + } + } + .icon-merge-request-unmerged { height: 13px; margin-bottom: 3px; - } + } } } @@ -181,11 +191,6 @@ ul.related-merge-requests > li { } .create-mr-dropdown-wrap { - .branch-message, - .ref-message { - display: none; - } - .ref::selection { color: $placeholder-text-color; } @@ -216,6 +221,17 @@ ul.related-merge-requests > li { transform: translateY(0); display: none; margin-top: 4px; + + // override dropdown item styles + .btn.btn-success { + @include btn-default; + @include btn-green; + + border-style: solid; + border-width: 1px; + line-height: $line-height-base; + width: auto; + } } .create-merge-request-dropdown-toggle { @@ -225,66 +241,6 @@ ul.related-merge-requests > li { margin-left: 0; } } - - .droplab-item-ignore { - pointer-events: auto; - } - - .create-item { - cursor: pointer; - margin: 0 1px; - - &:hover, - &:focus { - background-color: $dropdown-item-hover-bg; - color: $gl-text-color; - } - } - - li.divider { - margin: 8px 10px; - } - - li:not(.divider) { - padding: 8px 9px; - - &:last-child { - padding-bottom: 8px; - } - - &.droplab-item-selected { - .icon-container { - i { - visibility: visible; - } - } - - .description { - display: block; - } - } - - &.droplab-item-ignore { - padding-top: 8px; - } - - .icon-container { - float: left; - - i { - visibility: hidden; - } - } - - .description { - padding-left: 22px; - } - - input, - span { - margin: 4px 0 0; - } - } } .discussion-reply-holder .note-edit-form { diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index a72e654824e..0f49d15203b 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -105,13 +105,16 @@ } .label { - padding: 8px 12px; - font-size: 14px; + padding: 4px $grid-size; + font-size: $label-font-size; + position: relative; + top: ($grid-size / 2); } } .color-label { - padding: 3px $label-padding; + padding: 0 $grid-size; + line-height: 16px; border-radius: $label-border-radius; } @@ -302,10 +305,11 @@ } .label-link { - display: inline-block; + display: inline-flex; vertical-align: top; .label { vertical-align: inherit; + font-size: $label-font-size; } } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index ae8fa45a2d7..e5afa8fffcb 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -115,6 +115,10 @@ display: block; margin-top: 7px; + .issue-link { + display: inline-block; + } + .issuable-number { color: $gl-text-color-secondary; margin-right: 5px; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 26e6e8688b6..3c565837383 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -723,7 +723,7 @@ ul.notes { .line-resolve-all { vertical-align: middle; display: inline-block; - padding: 5px 10px 6px; + padding: 6px 10px; background-color: $gray-light; border: 1px solid $border-color; border-radius: $border-radius-default; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index f10908c3630..42772f13155 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -320,14 +320,17 @@ } } -// Pipeline graph -.pipeline-graph { +.pipeline-tab-content { width: 100%; background-color: $gray-light; padding: $gl-padding; + overflow: auto; +} + +// Pipeline graph +.pipeline-graph { white-space: nowrap; transition: max-height 0.3s, padding 0.3s; - overflow: auto; .stage-column-list, .builds-container > ul { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index bf41005b6d5..85de0d8e70f 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -678,6 +678,9 @@ a.deploy-project-label { } } +.project-empty-note-panel { + border-bottom: 1px solid $border-color; +} .project-stats { font-size: 0; @@ -686,11 +689,13 @@ a.deploy-project-label { border-bottom: 1px solid $border-color; .nav { - padding-top: 12px; - padding-bottom: 12px; + margin-top: $gl-padding-8; + margin-bottom: $gl-padding-8; > li { display: inline-block; + margin-top: $gl-padding-4; + margin-bottom: $gl-padding-4; &:not(:last-child) { margin-right: $gl-padding; @@ -704,36 +709,32 @@ a.deploy-project-label { float: right; } } + } - > a { - padding: 0; - background-color: transparent; - font-size: 14px; - line-height: 29px; - color: $notes-light-color; + .stat-text, + .stat-link { + padding: $gl-btn-vert-padding 0; + background-color: transparent; + font-size: $gl-font-size; + line-height: $gl-btn-line-height; + color: $notes-light-color; + } - &:hover, - &:focus { - color: $gl-text-color; - text-decoration: underline; - } + .stat-link { + &:hover, + &:focus { + color: $gl-text-color; + text-decoration: underline; } } - } - li.missing { - border: 1px dashed $border-gray-normal-dashed; - border-radius: $border-radius-default; - - a { - padding-left: 10px; - padding-right: 10px; - color: $notes-light-color; - display: block; + .btn { + padding: $gl-btn-vert-padding $gl-btn-horz-padding; + line-height: $gl-btn-line-height; } - &:hover { - background-color: $gray-normal; + .btn-missing { + @extend .btn-missing; } } } @@ -743,7 +744,7 @@ pre.light-well { } .git-empty { - margin: 0 7px 7px; + margin-bottom: 7px; h5 { color: $gl-text-color; @@ -895,6 +896,12 @@ pre.light-well { } } +.project-tip-command { + > .input-group-btn:first-child { + width: auto; + } +} + .protected-branches-list, .protected-tags-list { margin-bottom: 30px; diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 6353482ede7..47672783d5a 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -135,6 +135,17 @@ padding-top: 0; } +.integration-settings-form { + .well { + padding: $gl-padding / 2; + box-shadow: none; + } + + .svg-container { + max-width: 150px; + } +} + .token-token-container { #impersonation-token-token { width: 80%; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index a79772ea37b..4b9824fab0c 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -128,7 +128,6 @@ .label { color: $gl-text-color; - font-size: inherit; } p { |