diff options
Diffstat (limited to 'app/assets/javascripts')
505 files changed, 12730 insertions, 8921 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 21d8c790e90..1f34c6b50c2 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -1,9 +1,9 @@ -import $ from 'jquery'; +import _ from 'underscore'; import axios from './lib/utils/axios_utils'; const Api = { groupsPath: '/api/:version/groups.json', - groupPath: '/api/:version/groups/:id.json', + groupPath: '/api/:version/groups/:id', namespacesPath: '/api/:version/namespaces.json', groupProjectsPath: '/api/:version/groups/:id/projects.json', projectsPath: '/api/:version/projects.json', @@ -23,42 +23,44 @@ const Api = { group(groupId, callback) { const url = Api.buildUrl(Api.groupPath) .replace(':id', groupId); - return $.ajax({ - url, - dataType: 'json', - }) - .done(group => callback(group)); + return axios.get(url) + .then(({ data }) => { + callback(data); + + return data; + }); }, // Return groups list. Filtered by query groups(query, options, callback) { const url = Api.buildUrl(Api.groupsPath); - return $.ajax({ - url, - data: Object.assign({ + return axios.get(url, { + params: Object.assign({ search: query, per_page: 20, }, options), - dataType: 'json', }) - .done(groups => callback(groups)); + .then(({ data }) => { + callback(data); + + return data; + }); }, // Return namespaces list. Filtered by query namespaces(query, callback) { const url = Api.buildUrl(Api.namespacesPath); - return $.ajax({ - url, - data: { + return axios.get(url, { + params: { search: query, per_page: 20, }, - dataType: 'json', - }).done(namespaces => callback(namespaces)); + }) + .then(({ data }) => callback(data)); }, // Return projects list. Filtered by query - projects(query, options, callback) { + projects(query, options, callback = _.noop) { const url = Api.buildUrl(Api.projectsPath); const defaults = { search: query, @@ -70,12 +72,14 @@ const Api = { defaults.membership = true; } - return $.ajax({ - url, - data: Object.assign(defaults, options), - dataType: 'json', + return axios.get(url, { + params: Object.assign(defaults, options), }) - .done(projects => callback(projects)); + .then(({ data }) => { + callback(data); + + return data; + }); }, // Return single project @@ -97,41 +101,34 @@ const Api = { url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath); } - return $.ajax({ - url, - type: 'POST', - data: { label: data }, - dataType: 'json', + return axios.post(url, { + label: data, }) - .done(label => callback(label)) - .fail(message => callback(message.responseJSON)); + .then(res => callback(res.data)) + .catch(e => callback(e.response.data)); }, // Return group projects list. Filtered by query groupProjects(groupId, query, callback) { const url = Api.buildUrl(Api.groupProjectsPath) .replace(':id', groupId); - return $.ajax({ - url, - data: { + return axios.get(url, { + params: { search: query, per_page: 20, }, - dataType: 'json', }) - .done(projects => callback(projects)); + .then(({ data }) => callback(data)); }, commitMultiple(id, data) { // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions const url = Api.buildUrl(Api.commitPath) .replace(':id', encodeURIComponent(id)); - return this.wrapAjaxCall({ - url, - type: 'POST', - contentType: 'application/json; charset=utf-8', - data: JSON.stringify(data), - dataType: 'json', + return axios.post(url, JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, }); }, @@ -140,65 +137,57 @@ const Api = { .replace(':id', encodeURIComponent(id)) .replace(':branch', branch); - return this.wrapAjaxCall({ - url, - type: 'GET', - contentType: 'application/json; charset=utf-8', - dataType: 'json', - }); + return axios.get(url); }, // Return text for a specific license licenseText(key, data, callback) { const url = Api.buildUrl(Api.licensePath) .replace(':key', key); - return $.ajax({ - url, - data, + return axios.get(url, { + params: data, }) - .done(license => callback(license)); + .then(res => callback(res.data)); }, gitignoreText(key, callback) { const url = Api.buildUrl(Api.gitignorePath) .replace(':key', key); - return $.get(url, gitignore => callback(gitignore)); + return axios.get(url) + .then(({ data }) => callback(data)); }, gitlabCiYml(key, callback) { const url = Api.buildUrl(Api.gitlabCiYmlPath) .replace(':key', key); - return $.get(url, file => callback(file)); + return axios.get(url) + .then(({ data }) => callback(data)); }, dockerfileYml(key, callback) { const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); - $.get(url, callback); + return axios.get(url) + .then(({ data }) => callback(data)); }, issueTemplate(namespacePath, projectPath, key, type, callback) { const url = Api.buildUrl(Api.issuableTemplatePath) - .replace(':key', key) + .replace(':key', encodeURIComponent(key)) .replace(':type', type) .replace(':project_path', projectPath) .replace(':namespace_path', namespacePath); - $.ajax({ - url, - dataType: 'json', - }) - .done(file => callback(null, file)) - .fail(callback); + return axios.get(url) + .then(({ data }) => callback(null, data)) + .catch(callback); }, users(query, options) { const url = Api.buildUrl(this.usersPath); - return Api.wrapAjaxCall({ - url, - data: Object.assign({ + return axios.get(url, { + params: Object.assign({ search: query, per_page: 20, }, options), - dataType: 'json', }); }, @@ -209,20 +198,6 @@ const Api = { } return urlRoot + url.replace(':version', gon.api_version); }, - - wrapAjaxCall(options) { - return new Promise((resolve, reject) => { - // jQuery 2 is not Promises/A+ compatible (missing catch) - $.ajax(options) // eslint-disable-line promise/catch-or-return - .then(data => resolve(data), - (jqXHR, textStatus, errorThrown) => { - const error = new Error(`${options.url}: ${errorThrown}`); - error.textStatus = textStatus; - reject(error); - }, - ); - }); - }, }; export default Api; diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 622764107ad..87109a802e5 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,8 +1,10 @@ /* eslint-disable class-methods-use-this */ import _ from 'underscore'; import Cookies from 'js-cookie'; +import { __ } from './locale'; import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils'; -import Flash from './flash'; +import flash from './flash'; +import axios from './lib/utils/axios_utils'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; @@ -441,13 +443,15 @@ class AwardsHandler { if (this.isUserAuthored($emojiButton)) { this.userAuthored($emojiButton); } else { - $.post(awardUrl, { + axios.post(awardUrl, { name: emoji, - }, (data) => { + }) + .then(({ data }) => { if (data.ok) { callback(); } - }).fail(() => new Flash('Something went wrong on our end.')); + }) + .catch(() => flash(__('Something went wrong on our end.'))); } } diff --git a/app/assets/javascripts/behaviors/copy_as_gfm.js b/app/assets/javascripts/behaviors/copy_as_gfm.js index e7dc4ef8304..ffe90595b5d 100644 --- a/app/assets/javascripts/behaviors/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/copy_as_gfm.js @@ -74,6 +74,18 @@ const gfmRules = { return `![${el.dataset.title}](${el.getAttribute('src')})`; }, }, + MermaidFilter: { + 'svg.mermaid'(el, text) { + const sourceEl = el.querySelector('text.source'); + if (!sourceEl) return false; + + return `\`\`\`mermaid\n${CopyAsGFM.nodeToGFM(sourceEl)}\n\`\`\``; + }, + 'svg.mermaid style, svg.mermaid g'(el, text) { + // We don't want to include the content of these elements in the copied text. + return ''; + }, + }, MathFilter: { 'pre.code.math[data-math-style=display]'(el, text) { return `\`\`\`math\n${text.trim()}\n\`\`\``; @@ -287,6 +299,13 @@ const gfmRules = { export class CopyAsGFM { constructor() { + // iOS currently does not support clipboardData.setData(). This bug should + // be fixed in iOS 12, but for now we'll disable this for all iOS browsers + // ref: https://trac.webkit.org/changeset/222228/webkit + const userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || ''; + const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent); + if (isIOS) return; + $(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); }); $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); $(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM); diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js index 34e905222b4..8d021de7998 100644 --- a/app/assets/javascripts/behaviors/index.js +++ b/app/assets/javascripts/behaviors/index.js @@ -7,6 +7,7 @@ import installGlEmojiElement from './gl_emoji'; import './quick_submit'; import './requires_input'; import './toggler_behavior'; +import '../preview_markdown'; installGlEmojiElement(); initCopyAsGFM(); diff --git a/app/assets/javascripts/behaviors/secret_values.js b/app/assets/javascripts/behaviors/secret_values.js index 1cf0b960eb0..0d6e0dbefcc 100644 --- a/app/assets/javascripts/behaviors/secret_values.js +++ b/app/assets/javascripts/behaviors/secret_values.js @@ -2,22 +2,25 @@ import { n__ } from '../locale'; import { convertPermissionToBoolean } from '../lib/utils/common_utils'; export default class SecretValues { - constructor(container) { + constructor({ + container, + valueSelector = '.js-secret-value', + placeholderSelector = '.js-secret-value-placeholder', + }) { this.container = container; + this.valueSelector = valueSelector; + this.placeholderSelector = placeholderSelector; } init() { - this.values = this.container.querySelectorAll('.js-secret-value'); - this.placeholders = this.container.querySelectorAll('.js-secret-value-placeholder'); this.revealButton = this.container.querySelector('.js-secret-value-reveal-button'); - this.revealText = n__('Reveal value', 'Reveal values', this.values.length); - this.hideText = n__('Hide value', 'Hide values', this.values.length); + if (this.revealButton) { + const isRevealed = convertPermissionToBoolean(this.revealButton.dataset.secretRevealStatus); + this.updateDom(isRevealed); - const isRevealed = convertPermissionToBoolean(this.revealButton.dataset.secretRevealStatus); - this.updateDom(isRevealed); - - this.revealButton.addEventListener('click', this.onRevealButtonClicked.bind(this)); + this.revealButton.addEventListener('click', this.onRevealButtonClicked.bind(this)); + } } onRevealButtonClicked() { @@ -28,15 +31,17 @@ export default class SecretValues { } updateDom(isRevealed) { - this.values.forEach((value) => { + const values = this.container.querySelectorAll(this.valueSelector); + values.forEach((value) => { value.classList.toggle('hide', !isRevealed); }); - this.placeholders.forEach((placeholder) => { + const placeholders = this.container.querySelectorAll(this.placeholderSelector); + placeholders.forEach((placeholder) => { placeholder.classList.toggle('hide', isRevealed); }); - this.revealButton.textContent = isRevealed ? this.hideText : this.revealText; + this.revealButton.textContent = isRevealed ? n__('Hide value', 'Hide values', values.length) : n__('Reveal value', 'Reveal values', values.length); this.revealButton.dataset.secretRevealStatus = isRevealed; } } diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index f7ae6f1cd12..83cac896f86 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -4,6 +4,8 @@ import { visitUrl } from '../lib/utils/url_utility'; import { HIDDEN_CLASS } from '../lib/utils/constants'; import csrf from '../lib/utils/csrf'; +Dropzone.autoDiscover = false; + function toggleLoading($el, $icon, loading) { if (loading) { $el.disable(); diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js index 583e5faa506..37074301b51 100644 --- a/app/assets/javascripts/blob/file_template_mediator.js +++ b/app/assets/javascripts/blob/file_template_mediator.js @@ -235,7 +235,7 @@ export default class FileTemplateMediator { } setFilename(name) { - this.$filenameInput.val(name); + this.$filenameInput.val(name).trigger('change'); } getSelected() { diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js index c858a6bb7b4..6f1350e80fc 100644 --- a/app/assets/javascripts/blob/notebook/index.js +++ b/app/assets/javascripts/blob/notebook/index.js @@ -1,15 +1,16 @@ /* eslint-disable no-new */ import Vue from 'vue'; -import VueResource from 'vue-resource'; +import axios from '../../lib/utils/axios_utils'; import notebookLab from '../../notebook/index.vue'; -Vue.use(VueResource); - export default () => { const el = document.getElementById('js-notebook-viewer'); new Vue({ el, + components: { + notebookLab, + }, data() { return { error: false, @@ -18,8 +19,41 @@ export default () => { json: {}, }; }, - components: { - notebookLab, + mounted() { + if (gon.katex_css_url) { + const katexStyles = document.createElement('link'); + katexStyles.setAttribute('rel', 'stylesheet'); + katexStyles.setAttribute('href', gon.katex_css_url); + document.head.appendChild(katexStyles); + } + + if (gon.katex_js_url) { + const katexScript = document.createElement('script'); + katexScript.addEventListener('load', () => { + this.loadFile(); + }); + katexScript.setAttribute('src', gon.katex_js_url); + document.head.appendChild(katexScript); + } else { + this.loadFile(); + } + }, + methods: { + loadFile() { + axios.get(el.dataset.endpoint) + .then(res => res.data) + .then((data) => { + this.json = data; + this.loading = false; + }) + .catch((e) => { + if (e.status !== 200) { + this.loadError = true; + } + + this.error = true; + }); + }, }, template: ` <div class="container-fluid md prepend-top-default append-bottom-default"> @@ -48,41 +82,5 @@ export default () => { </p> </div> `, - methods: { - loadFile() { - this.$http.get(el.dataset.endpoint) - .then(response => response.json()) - .then((res) => { - this.json = res; - this.loading = false; - }) - .catch((e) => { - if (e.status) { - this.loadError = true; - } - - this.error = true; - }); - }, - }, - mounted() { - if (gon.katex_css_url) { - const katexStyles = document.createElement('link'); - katexStyles.setAttribute('rel', 'stylesheet'); - katexStyles.setAttribute('href', gon.katex_css_url); - document.head.appendChild(katexStyles); - } - - if (gon.katex_js_url) { - const katexScript = document.createElement('script'); - katexScript.addEventListener('load', () => { - this.loadFile(); - }); - katexScript.setAttribute('src', gon.katex_js_url); - document.head.appendChild(katexScript); - } else { - this.loadFile(); - } - }, }); }; diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js index 7109f356540..70136cc4087 100644 --- a/app/assets/javascripts/blob/pdf/index.js +++ b/app/assets/javascripts/blob/pdf/index.js @@ -7,6 +7,9 @@ export default () => { return new Vue({ el, + components: { + pdfLab, + }, data() { return { error: false, @@ -15,9 +18,6 @@ export default () => { pdf: el.dataset.endpoint, }; }, - components: { - pdfLab, - }, methods: { onLoad() { this.loading = false; diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 54132e8537b..612f604e725 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -1,5 +1,6 @@ import Flash from '../../flash'; import { handleLocationHash } from '../../lib/utils/common_utils'; +import axios from '../../lib/utils/axios_utils'; export default class BlobViewer { constructor() { @@ -127,25 +128,18 @@ export default class BlobViewer { const viewer = viewerParam; const url = viewer.getAttribute('data-url'); - return new Promise((resolve, reject) => { - if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) { - resolve(viewer); - return; - } + if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) { + return Promise.resolve(viewer); + } - viewer.setAttribute('data-loading', 'true'); + viewer.setAttribute('data-loading', 'true'); - $.ajax({ - url, - dataType: 'JSON', - }) - .fail(reject) - .done((data) => { + return axios.get(url) + .then(({ data }) => { viewer.innerHTML = data.html; viewer.setAttribute('data-loaded', 'true'); - resolve(viewer); + return viewer; }); - }); } } diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index b37988a674d..a25f7fb3dcd 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -1,5 +1,8 @@ /* global ace */ +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import { __ } from '~/locale'; import TemplateSelectorMediator from '../blob/file_template_mediator'; export default class EditBlob { @@ -56,12 +59,14 @@ export default class EditBlob { if (paneId === '#preview') { this.$toggleButton.hide(); - return $.post(currentLink.data('preview-url'), { + axios.post(currentLink.data('preview-url'), { content: this.editor.getValue(), - }, (response) => { - currentPane.empty().append(response); - return currentPane.renderGFM(); - }); + }) + .then(({ data }) => { + currentPane.empty().append(data); + currentPane.renderGFM(); + }) + .catch(() => createFlash(__('An error occurred previewing the blob'))); } this.$toggleButton.show(); diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 20d23162940..90166b3d3d1 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -2,7 +2,6 @@ import _ from 'underscore'; import Vue from 'vue'; -import VueResource from 'vue-resource'; import Flash from '../flash'; import { __ } from '../locale'; import FilteredSearchBoards from './filtered_search_boards'; @@ -25,8 +24,6 @@ import './components/new_list_dropdown'; import './components/modal/index'; import '../vue_shared/vue_resource_interceptor'; -Vue.use(VueResource); - $(() => { const $boardApp = document.getElementById('board-app'); const Store = gl.issueBoards.BoardsStore; @@ -95,14 +92,13 @@ $(() => { Store.disabled = this.disabled; gl.boardService.all() - .then(response => response.json()) - .then((resp) => { - resp.forEach((board) => { + .then(res => res.data) + .then((data) => { + data.forEach((board) => { const list = Store.addList(board, this.defaultAvatar); if (list.type === 'closed') { list.position = Infinity; - list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' }; } else if (list.type === 'backlog') { list.position = -1; } @@ -113,7 +109,9 @@ $(() => { Store.addBlankState(); this.loading = false; }) - .catch(() => new Flash('An error occurred. Please try again.')); + .catch(() => { + Flash('An error occurred while fetching the board lists. Please try again.'); + }); }, methods: { updateTokens() { @@ -124,7 +122,7 @@ $(() => { if (sidebarInfoEndpoint && newIssue.subscribed === undefined) { newIssue.setFetchingState('subscriptions', true); BoardService.getIssueInfo(sidebarInfoEndpoint) - .then(res => res.json()) + .then(res => res.data) .then((data) => { newIssue.setFetchingState('subscriptions', false); newIssue.updateData({ @@ -173,19 +171,14 @@ $(() => { }); gl.IssueBoardsModalAddBtn = new Vue({ - mixins: [gl.issueBoards.ModalMixins], el: document.getElementById('js-add-issues-btn'), + mixins: [gl.issueBoards.ModalMixins], data() { return { modal: ModalStore.store, store: Store.state, }; }, - watch: { - disabled() { - this.updateTooltip(); - }, - }, computed: { disabled() { if (!this.store) { @@ -201,6 +194,14 @@ $(() => { return ''; }, }, + watch: { + disabled() { + this.updateTooltip(); + }, + }, + mounted() { + this.updateTooltip(); + }, methods: { updateTooltip() { const $tooltip = $(this.$refs.addIssuesButton); @@ -219,9 +220,6 @@ $(() => { } }, }, - mounted() { - this.updateTooltip(); - }, template: ` <div class="board-extra-actions"> <button diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index adb7360327c..a8dafd31f12 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -1,5 +1,5 @@ /* eslint-disable comma-dangle, space-before-function-paren, one-var */ -/* global Sortable */ +import Sortable from 'vendor/Sortable'; import Vue from 'vue'; import AccessorUtilities from '../../lib/utils/accessor'; import boardList from './board_list'; diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.js index edfe7c326db..72db626d3c7 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js +++ b/app/assets/javascripts/boards/components/board_blank_state.js @@ -65,7 +65,7 @@ export default { // Save the labels gl.boardService.generateDefaultLists() - .then(resp => resp.json()) + .then(res => res.data) .then((data) => { data.forEach((listObj) => { const list = Store.findList('title', listObj.title); diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 0b220a56e0b..23fec503586 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -10,12 +10,30 @@ export default { 'issue-card-inner': gl.issueBoards.IssueCardInner, }, props: { - list: Object, - issue: Object, - issueLinkBase: String, - disabled: Boolean, - index: Number, - rootPath: String, + list: { + type: Object, + default: () => ({}), + }, + issue: { + type: Object, + default: () => ({}), + }, + issueLinkBase: { + type: String, + default: '', + }, + disabled: { + type: Boolean, + default: false, + }, + index: { + type: Number, + default: 0, + }, + rootPath: { + type: String, + default: '', + }, }, data() { return { @@ -54,8 +72,13 @@ export default { </script> <template> - <li class="card" - :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }" + <li + class="card" + :class="{ + 'user-can-drag': !disabled && issue.id, + 'is-disabled': disabled || !issue.id, + 'is-active': issueDetailVisible + }" :index="index" :data-issue-id="issue.id" @mousedown="mouseDown" @@ -66,6 +89,7 @@ export default { :issue="issue" :issue-link-base="issueLinkBase" :root-path="rootPath" - :update-filters="true" /> + :update-filters="true" + /> </li> </template> diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js index 29aeb8e84aa..591f1dc8313 100644 --- a/app/assets/javascripts/boards/components/board_list.js +++ b/app/assets/javascripts/boards/components/board_list.js @@ -1,4 +1,4 @@ -/* global Sortable */ +import Sortable from 'vendor/Sortable'; import boardNewIssue from './board_new_issue'; import boardCard from './board_card.vue'; import eventHub from '../eventhub'; @@ -115,7 +115,7 @@ export default { }, mounted() { const options = gl.issueBoards.getBoardSortableDefaultOptions({ - scroll: document.querySelectorAll('.boards-list')[0], + scroll: true, group: 'issues', disabled: this.disabled, filter: '.board-list-count, .is-disabled', @@ -187,7 +187,7 @@ export default { <li class="board-list-count text-center" v-if="showCount" - data-id="-1"> + data-issue-id="-1"> <loading-icon v-show="list.loadingMore" diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 616de2347e1..983429550f0 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -1,5 +1,4 @@ /* eslint-disable comma-dangle, space-before-function-paren, no-new */ -/* global MilestoneSelect */ import Vue from 'vue'; import Flash from '../../flash'; @@ -12,6 +11,7 @@ import './sidebar/remove_issue'; import IssuableContext from '../../issuable_context'; import LabelsSelect from '../../labels_select'; import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue'; +import MilestoneSelect from '../../milestone_select'; const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js index d2044f20ebe..d825ff38587 100644 --- a/app/assets/javascripts/boards/components/modal/index.js +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -89,7 +89,7 @@ gl.issueBoards.IssuesModal = Vue.extend({ page: this.page, per: this.perPage, })) - .then(resp => resp.json()) + .then(res => res.data) .then((data) => { if (clearIssues) { this.issues = []; diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index df2809e1805..e210d69895e 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -40,7 +40,7 @@ class List { save () { return gl.boardService.createList(this.label.id) - .then(resp => resp.json()) + .then(res => res.data) .then((data) => { this.id = data.id; this.type = data.list_type; @@ -90,7 +90,7 @@ class List { } return gl.boardService.getIssuesForList(this.id, data) - .then(resp => resp.json()) + .then(res => res.data) .then((data) => { this.loading = false; this.issuesSize = data.size; @@ -108,7 +108,7 @@ class List { this.issuesSize += 1; return gl.boardService.newIssue(this.id, issue) - .then(resp => resp.json()) + .then(res => res.data) .then((data) => { issue.id = data.id; issue.iid = data.iid; diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index fa7ddd25e1f..d78d4701974 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -1,82 +1,79 @@ -/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */ - -import Vue from 'vue'; +import axios from '../../lib/utils/axios_utils'; +import { mergeUrlParams } from '../../lib/utils/url_utility'; export default class BoardService { - constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) { - this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, { - issues: { - method: 'GET', - url: `${gon.relative_url_root}/-/boards/${boardId}/issues.json`, - } - }); - this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, { - generate: { - method: 'POST', - url: `${listsEndpoint}/generate.json` - } - }); - this.issue = Vue.resource(`${gon.relative_url_root}/-/boards/${boardId}/issues{/id}`, {}); - this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, { - bulkUpdate: { - method: 'POST', - url: bulkUpdatePath, - }, - }); + constructor({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) { + this.boardsEndpoint = boardsEndpoint; + this.boardId = boardId; + this.listsEndpoint = listsEndpoint; + this.listsEndpointGenerate = `${listsEndpoint}/generate.json`; + this.bulkUpdatePath = bulkUpdatePath; + } + + generateBoardsPath(id) { + return `${this.boardsEndpoint}${id ? `/${id}` : ''}.json`; } - all () { - return this.lists.get(); + generateIssuesPath(id) { + return `${this.listsEndpoint}${id ? `/${id}` : ''}/issues`; } - generateDefaultLists () { - return this.lists.generate({}); + static generateIssuePath(boardId, id) { + return `${gon.relative_url_root}/-/boards/${boardId ? `/${boardId}` : ''}/issues${id ? `/${id}` : ''}`; } - createList (label_id) { - return this.lists.save({}, { + all() { + return axios.get(this.listsEndpoint); + } + + generateDefaultLists() { + return axios.post(this.listsEndpointGenerate, {}); + } + + createList(labelId) { + return axios.post(this.listsEndpoint, { list: { - label_id - } + label_id: labelId, + }, }); } - updateList (id, position) { - return this.lists.update({ id }, { + updateList(id, position) { + return axios.put(`${this.listsEndpoint}/${id}`, { list: { - position - } + position, + }, }); } - destroyList (id) { - return this.lists.delete({ id }); + destroyList(id) { + return axios.delete(`${this.listsEndpoint}/${id}`); } - getIssuesForList (id, filter = {}) { + getIssuesForList(id, filter = {}) { const data = { id }; Object.keys(filter).forEach((key) => { data[key] = filter[key]; }); - return this.issues.get(data); + return axios.get(mergeUrlParams(data, this.generateIssuesPath(id))); } - moveIssue (id, from_list_id = null, to_list_id = null, move_before_id = null, move_after_id = null) { - return this.issue.update({ id }, { - from_list_id, - to_list_id, - move_before_id, - move_after_id, + moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) { + return axios.put(BoardService.generateIssuePath(this.boardId, id), { + from_list_id: fromListId, + to_list_id: toListId, + move_before_id: moveBeforeId, + move_after_id: moveAfterId, }); } - newIssue (id, issue) { - return this.issues.save({ id }, { - issue + newIssue(id, issue) { + return axios.post(this.generateIssuesPath(id), { + issue, }); } getBacklog(data) { - return this.boards.issues(data); + return axios.get(mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`)); } bulkUpdate(issueIds, extraData = {}) { @@ -86,15 +83,15 @@ export default class BoardService { }), }; - return this.issues.bulkUpdate(data); + return axios.post(this.bulkUpdatePath, data); } static getIssueInfo(endpoint) { - return Vue.http.get(endpoint); + return axios.get(endpoint); } static toggleIssueSubscription(endpoint) { - return Vue.http.post(endpoint); + return axios.post(endpoint); } } diff --git a/app/assets/javascripts/boards/utils/query_data.js b/app/assets/javascripts/boards/utils/query_data.js index 2cd3c146f11..65315979df7 100644 --- a/app/assets/javascripts/boards/utils/query_data.js +++ b/app/assets/javascripts/boards/utils/query_data.js @@ -5,7 +5,7 @@ export default (path, extraData) => path.split('&').reduce((dataParam, filterPar const paramSplit = filterParam.split('='); const paramKeyNormalized = paramSplit[0].replace('[]', ''); const isArray = paramSplit[0].indexOf('[]'); - const value = decodeURIComponent(paramSplit[1]).replace(/\+/g, ' '); + const value = decodeURIComponent(paramSplit[1].replace(/\+/g, ' ')); if (isArray !== -1) { if (!data[paramKeyNormalized]) { diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js new file mode 100644 index 00000000000..e46478ddb98 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js @@ -0,0 +1,205 @@ +import $ from 'jquery'; +import { convertPermissionToBoolean } from '../lib/utils/common_utils'; +import { s__ } from '../locale'; +import setupToggleButtons from '../toggle_buttons'; +import CreateItemDropdown from '../create_item_dropdown'; +import SecretValues from '../behaviors/secret_values'; + +const ALL_ENVIRONMENTS_STRING = s__('CiVariable|All environments'); + +function createEnvironmentItem(value) { + return { + title: value === '*' ? ALL_ENVIRONMENTS_STRING : value, + id: value, + text: value, + }; +} + +export default class VariableList { + constructor({ + container, + formField, + }) { + this.$container = $(container); + this.formField = formField; + this.environmentDropdownMap = new WeakMap(); + + this.inputMap = { + id: { + selector: '.js-ci-variable-input-id', + default: '', + }, + key: { + selector: '.js-ci-variable-input-key', + default: '', + }, + value: { + selector: '.js-ci-variable-input-value', + default: '', + }, + protected: { + selector: '.js-ci-variable-input-protected', + default: 'true', + }, + environment: { + // We can't use a `.js-` class here because + // gl_dropdown replaces the <input> and doesn't copy over the class + // See https://gitlab.com/gitlab-org/gitlab-ce/issues/42458 + selector: `input[name="${this.formField}[variables_attributes][][environment]"]`, + default: '*', + }, + _destroy: { + selector: '.js-ci-variable-input-destroy', + default: '', + }, + }; + + this.secretValues = new SecretValues({ + container: this.$container[0], + valueSelector: '.js-row:not(:last-child) .js-secret-value', + placeholderSelector: '.js-row:not(:last-child) .js-secret-value-placeholder', + }); + } + + init() { + this.bindEvents(); + this.secretValues.init(); + } + + bindEvents() { + this.$container.find('.js-row').each((index, rowEl) => { + this.initRow(rowEl); + }); + + this.$container.on('click', '.js-row-remove-button', (e) => { + e.preventDefault(); + this.removeRow($(e.currentTarget).closest('.js-row')); + }); + + const inputSelector = Object.keys(this.inputMap) + .map(name => this.inputMap[name].selector) + .join(','); + + // Remove any empty rows except the last row + this.$container.on('blur', inputSelector, (e) => { + const $row = $(e.currentTarget).closest('.js-row'); + + if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) { + this.removeRow($row); + } + }); + + // Always make sure there is an empty last row + this.$container.on('input trigger-change', inputSelector, () => { + const $lastRow = this.$container.find('.js-row').last(); + + if (this.checkIfRowTouched($lastRow)) { + this.insertRow($lastRow); + } + }); + } + + initRow(rowEl) { + const $row = $(rowEl); + + setupToggleButtons($row[0]); + + const $environmentSelect = $row.find('.js-variable-environment-toggle'); + if ($environmentSelect.length) { + const createItemDropdown = new CreateItemDropdown({ + $dropdown: $environmentSelect, + defaultToggleLabel: ALL_ENVIRONMENTS_STRING, + fieldName: `${this.formField}[variables_attributes][][environment]`, + getData: (term, callback) => callback(this.getEnvironmentValues()), + createNewItemFromValue: createEnvironmentItem, + onSelect: () => { + // Refresh the other dropdowns in the variable list + // so they have the new value we just picked + this.refreshDropdownData(); + + $row.find(this.inputMap.environment.selector).trigger('trigger-change'); + }, + }); + + // Clear out any data that might have been left-over from the row clone + createItemDropdown.clearDropdown(); + + this.environmentDropdownMap.set($row[0], createItemDropdown); + } + } + + insertRow($row) { + const $rowClone = $row.clone(); + $rowClone.removeAttr('data-is-persisted'); + + // Reset the inputs to their defaults + Object.keys(this.inputMap).forEach((name) => { + const entry = this.inputMap[name]; + $rowClone.find(entry.selector).val(entry.default); + }); + + this.initRow($rowClone); + + $row.after($rowClone); + } + + removeRow($row) { + const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted')); + + if (isPersisted) { + $row.hide(); + $row + // eslint-disable-next-line no-underscore-dangle + .find(this.inputMap._destroy.selector) + .val(true); + } else { + $row.remove(); + } + } + + checkIfRowTouched($row) { + return Object.keys(this.inputMap).some((name) => { + const entry = this.inputMap[name]; + const $el = $row.find(entry.selector); + return $el.length && $el.val() !== entry.default; + }); + } + + getAllData() { + // Ignore the last empty row because we don't want to try persist + // a blank variable and run into validation problems. + const validRows = this.$container.find('.js-row').toArray().slice(0, -1); + + return validRows.map((rowEl) => { + const resultant = {}; + Object.keys(this.inputMap).forEach((name) => { + const entry = this.inputMap[name]; + const $input = $(rowEl).find(entry.selector); + if ($input.length) { + resultant[name] = $input.val(); + } + }); + + return resultant; + }); + } + + getEnvironmentValues() { + const valueMap = this.$container.find(this.inputMap.environment.selector).toArray() + .reduce((prevValueMap, envInput) => ({ + ...prevValueMap, + [envInput.value]: envInput.value, + }), {}); + + return Object.keys(valueMap).map(createEnvironmentItem); + } + + refreshDropdownData() { + this.$container.find('.js-row').each((index, rowEl) => { + const environmentDropdown = this.environmentDropdownMap.get(rowEl); + if (environmentDropdown) { + environmentDropdown.refreshData(); + } + }); + } +} diff --git a/app/assets/javascripts/ci_variable_list/native_form_variable_list.js b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js new file mode 100644 index 00000000000..d54ea7df1c3 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js @@ -0,0 +1,26 @@ +import VariableList from './ci_variable_list'; + +// Used for the variable list on scheduled pipeline edit page +export default function setupNativeFormVariableList({ + container, + formField = 'variables', +}) { + const $container = $(container); + + const variableList = new VariableList({ + container: $container, + formField, + }); + variableList.init(); + + // Clear out the names in the empty last row so it + // doesn't get submitted and throw validation errors + $container.closest('form').on('submit trigger-submit', () => { + const $lastRow = $container.find('.js-row').last(); + + const isTouched = variableList.checkIfRowTouched($lastRow); + if (!isTouched) { + $lastRow.find('input, textarea').attr('name', ''); + } + }); +} diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 2cfd6179a25..4dddb6eb0d6 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -14,6 +14,7 @@ import { import ClustersService from './services/clusters_service'; import ClustersStore from './stores/clusters_store'; import applications from './components/applications.vue'; +import setupToggleButtons from '../toggle_buttons'; /** * Cluster page has 2 separate parts: @@ -30,6 +31,7 @@ export default class Clusters { installHelmPath, installIngressPath, installRunnerPath, + installPrometheusPath, clusterStatus, clusterStatusReason, helpPath, @@ -44,14 +46,12 @@ export default class Clusters { installHelmEndpoint: installHelmPath, installIngressEndpoint: installIngressPath, installRunnerEndpoint: installRunnerPath, + installPrometheusEndpoint: installPrometheusPath, }); - this.toggle = this.toggle.bind(this); this.installApplication = this.installApplication.bind(this); this.showToken = this.showToken.bind(this); - this.toggleButton = document.querySelector('.js-toggle-cluster'); - this.toggleInput = document.querySelector('.js-toggle-input'); this.errorContainer = document.querySelector('.js-cluster-error'); this.successContainer = document.querySelector('.js-cluster-success'); this.creatingContainer = document.querySelector('.js-cluster-creating'); @@ -61,6 +61,7 @@ export default class Clusters { this.tokenField = document.querySelector('.js-cluster-token'); initSettingsPanels(); + setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area')); this.initApplications(); if (this.store.state.status !== 'created') { @@ -99,13 +100,11 @@ export default class Clusters { } addListeners() { - this.toggleButton.addEventListener('click', this.toggle); if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); eventHub.$on('installApplication', this.installApplication); } removeListeners() { - this.toggleButton.removeEventListener('click', this.toggle); if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken); eventHub.$off('installApplication', this.installApplication); } @@ -149,11 +148,6 @@ export default class Clusters { this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason); } - toggle() { - this.toggleButton.classList.toggle('is-checked'); - this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('is-checked').toString()); - } - showToken() { const type = this.tokenField.getAttribute('type'); diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js index 6844d1dbd83..2e3ad244375 100644 --- a/app/assets/javascripts/clusters/clusters_index.js +++ b/app/assets/javascripts/clusters/clusters_index.js @@ -1,58 +1,20 @@ import Flash from '../flash'; import { s__ } from '../locale'; +import setupToggleButtons from '../toggle_buttons'; import ClustersService from './services/clusters_service'; -/** - * Toggles loading and disabled classes. - * @param {HTMLElement} button - */ -const toggleLoadingButton = (button) => { - if (button.getAttribute('disabled')) { - button.removeAttribute('disabled'); - } else { - button.setAttribute('disabled', true); - } - - button.classList.toggle('is-loading'); -}; -/** - * Toggles checked class for the given button - * @param {HTMLElement} button - */ -const toggleValue = (button) => { - button.classList.toggle('is-checked'); +export default () => { + const clusterList = document.querySelector('.js-clusters-list'); + // The empty state won't have a clusterList + if (clusterList) { + setupToggleButtons( + document.querySelector('.js-clusters-list'), + (value, toggle) => + ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }) + .catch((err) => { + Flash(s__('ClusterIntegration|Something went wrong on our end.')); + throw err; + }), + ); + } }; - -/** - * Handles toggle buttons in the cluster's table. - * - * When the user clicks the toggle button for each cluster, it: - * - toggles the button - * - shows a loading and disables button - * - Makes a put request to the given endpoint - * Once we receive the response, either: - * 1) Show updated status in case of successfull response - * 2) Show initial status in case of failed response - */ -export default function setClusterTableToggles() { - document.querySelectorAll('.js-toggle-cluster-list') - .forEach(button => button.addEventListener('click', (e) => { - const toggleButton = e.currentTarget; - const endpoint = toggleButton.getAttribute('data-endpoint'); - - toggleValue(toggleButton); - toggleLoadingButton(toggleButton); - - const value = toggleButton.classList.contains('is-checked'); - - ClustersService.updateCluster(endpoint, { cluster: { enabled: value } }) - .then(() => { - toggleLoadingButton(toggleButton); - }) - .catch(() => { - toggleLoadingButton(toggleButton); - toggleValue(toggleButton); - Flash(s__('ClusterIntegration|Something went wrong on our end.')); - }); - })); -} diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 872abf03ef1..c13bbcee863 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -1,108 +1,112 @@ <script> -import { s__, sprintf } from '../../locale'; -import eventHub from '../event_hub'; -import loadingButton from '../../vue_shared/components/loading_button.vue'; -import { - APPLICATION_NOT_INSTALLABLE, - APPLICATION_SCHEDULED, - APPLICATION_INSTALLABLE, - APPLICATION_INSTALLING, - APPLICATION_INSTALLED, - APPLICATION_ERROR, - REQUEST_LOADING, - REQUEST_SUCCESS, - REQUEST_FAILURE, -} from '../constants'; + /* eslint-disable vue/require-default-prop */ + import { s__, sprintf } from '../../locale'; + import eventHub from '../event_hub'; + import loadingButton from '../../vue_shared/components/loading_button.vue'; + import { + APPLICATION_NOT_INSTALLABLE, + APPLICATION_SCHEDULED, + APPLICATION_INSTALLABLE, + APPLICATION_INSTALLING, + APPLICATION_INSTALLED, + APPLICATION_ERROR, + REQUEST_LOADING, + REQUEST_SUCCESS, + REQUEST_FAILURE, + } from '../constants'; -export default { - props: { - id: { - type: String, - required: true, + export default { + components: { + loadingButton, }, - title: { - type: String, - required: true, + props: { + id: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + titleLink: { + type: String, + required: false, + }, + description: { + type: String, + required: true, + }, + status: { + type: String, + required: false, + }, + statusReason: { + type: String, + required: false, + }, + requestStatus: { + type: String, + required: false, + }, + requestReason: { + type: String, + required: false, + }, }, - titleLink: { - type: String, - required: false, - }, - description: { - type: String, - required: true, - }, - status: { - type: String, - required: false, - }, - statusReason: { - type: String, - required: false, - }, - requestStatus: { - type: String, - required: false, - }, - requestReason: { - type: String, - required: false, - }, - }, - components: { - loadingButton, - }, - computed: { - rowJsClass() { - return `js-cluster-application-row-${this.id}`; - }, - installButtonLoading() { - return !this.status || - this.status === APPLICATION_SCHEDULED || - this.status === APPLICATION_INSTALLING || - this.requestStatus === REQUEST_LOADING; - }, - installButtonDisabled() { - // Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but - // we already made a request to install and are just waiting for the real-time - // to sync up. - return (this.status !== APPLICATION_INSTALLABLE && this.status !== APPLICATION_ERROR) || - this.requestStatus === REQUEST_LOADING || - this.requestStatus === REQUEST_SUCCESS; - }, - installButtonLabel() { - let label; - if ( - this.status === APPLICATION_NOT_INSTALLABLE || - this.status === APPLICATION_INSTALLABLE || - this.status === APPLICATION_ERROR - ) { - label = s__('ClusterIntegration|Install'); - } else if (this.status === APPLICATION_SCHEDULED || this.status === APPLICATION_INSTALLING) { - label = s__('ClusterIntegration|Installing'); - } else if (this.status === APPLICATION_INSTALLED) { - label = s__('ClusterIntegration|Installed'); - } + computed: { + rowJsClass() { + return `js-cluster-application-row-${this.id}`; + }, + installButtonLoading() { + return !this.status || + this.status === APPLICATION_SCHEDULED || + this.status === APPLICATION_INSTALLING || + this.requestStatus === REQUEST_LOADING; + }, + installButtonDisabled() { + // Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but + // we already made a request to install and are just waiting for the real-time + // to sync up. + return (this.status !== APPLICATION_INSTALLABLE + && this.status !== APPLICATION_ERROR) || + this.requestStatus === REQUEST_LOADING || + this.requestStatus === REQUEST_SUCCESS; + }, + installButtonLabel() { + let label; + if ( + this.status === APPLICATION_NOT_INSTALLABLE || + this.status === APPLICATION_INSTALLABLE || + this.status === APPLICATION_ERROR + ) { + label = s__('ClusterIntegration|Install'); + } else if (this.status === APPLICATION_SCHEDULED || + this.status === APPLICATION_INSTALLING) { + label = s__('ClusterIntegration|Installing'); + } else if (this.status === APPLICATION_INSTALLED) { + label = s__('ClusterIntegration|Installed'); + } - return label; - }, - hasError() { - return this.status === APPLICATION_ERROR || this.requestStatus === REQUEST_FAILURE; - }, - generalErrorDescription() { - return sprintf( - s__('ClusterIntegration|Something went wrong while installing %{title}'), { - title: this.title, - }, - ); + return label; + }, + hasError() { + return this.status === APPLICATION_ERROR || + this.requestStatus === REQUEST_FAILURE; + }, + generalErrorDescription() { + return sprintf( + s__('ClusterIntegration|Something went wrong while installing %{title}'), { + title: this.title, + }, + ); + }, }, - }, - methods: { - installClicked() { - eventHub.$emit('installApplication', this.id); + methods: { + installClicked() { + eventHub.$emit('installApplication', this.id); + }, }, - }, -}; + }; </script> <template> diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index e5ae439d26e..ff2e0768a87 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -1,74 +1,93 @@ <script> -import _ from 'underscore'; -import { s__, sprintf } from '../../locale'; -import applicationRow from './application_row.vue'; + import _ from 'underscore'; + import { s__, sprintf } from '../../locale'; + import applicationRow from './application_row.vue'; -export default { - props: { - applications: { - type: Object, - required: false, - default: () => ({}), + export default { + components: { + applicationRow, }, - helpPath: { - type: String, - required: false, + props: { + applications: { + type: Object, + required: false, + default: () => ({}), + }, + helpPath: { + type: String, + required: false, + default: '', + }, }, - }, - components: { - applicationRow, - }, - computed: { - generalApplicationDescription() { - return sprintf( - _.escape(s__('ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}')), { - helpLink: `<a href="${this.helpPath}"> - ${_.escape(s__('ClusterIntegration|installing applications'))} - </a>`, - }, - false, - ); - }, - helmTillerDescription() { - return _.escape(s__( - `ClusterIntegration|Helm streamlines installing and managing Kubernets applications. - Tiller runs inside of your Kubernetes Cluster, and manages - releases of your charts.`, - )); - }, - 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.`, - )); + computed: { + generalApplicationDescription() { + return sprintf( + _.escape(s__(`ClusterIntegration|Install applications on your cluster. + Read more about %{helpLink}`)), + { + helpLink: `<a href="${this.helpPath}"> + ${_.escape(s__('ClusterIntegration|installing applications'))} + </a>`, + }, + false, + ); + }, + helmTillerDescription() { + return _.escape(s__( + `ClusterIntegration|Helm streamlines installing and managing Kubernets applications. + Tiller runs inside of your Kubernetes Cluster, and manages + releases of your charts.`, + )); + }, + 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 like a load balancer, which incur additional costs. See %{pricingLink}')), { - boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`, - pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer"> - ${_.escape(s__('ClusterIntegration|GKE pricing'))} - </a>`, - }, - false, - ); + const extraCostParagraph = sprintf( + _.escape(s__( + `ClusterIntegration|%{boldNotice} This will add some extra resources + like a load balancer, which may incur additional costs depending on + the hosting provider Kubernetes is installed on. If you are using GKE, + you can %{pricingLink}.`, + )), { + boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`, + pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer"> + ${_.escape(s__('ClusterIntegration|check the pricing here'))}</a>`, + }, + false, + ); - return ` - <p> - ${descriptionParagraph} - </p> - <p class="append-bottom-0"> - ${extraCostParagraph} - </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.`, - )); + return ` + <p> + ${descriptionParagraph} + </p> + <p class="append-bottom-0"> + ${extraCostParagraph} + </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.`)), + { + gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html" +target="_blank" rel="noopener noreferrer"> + ${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`, + }, + false, + ); + }, }, - }, -}; + }; </script> <template> @@ -97,16 +116,29 @@ export default { :request-reason="applications.helm.requestReason" /> <application-row - id="ingress" - :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" - /> - <!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests --> + id="ingress" + :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" + /> + <application-row + id="prometheus" + :title="applications.prometheus.title" + title-link="https://prometheus.io/docs/introduction/overview/" + :description="prometheusDescription" + :status="applications.prometheus.status" + :status-reason="applications.prometheus.statusReason" + :request-status="applications.prometheus.requestStatus" + :request-reason="applications.prometheus.requestReason" + /> + <!-- + NOTE: Don't forget to update `clusters.scss` + min-height for this block and uncomment `application_spec` tests + --> <!-- Add GitLab Runner row, all other plumbing is complete --> </div> </div> diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js index 755c2981c2e..13468578f4f 100644 --- a/app/assets/javascripts/clusters/services/clusters_service.js +++ b/app/assets/javascripts/clusters/services/clusters_service.js @@ -7,6 +7,7 @@ export default class ClusterService { helm: this.options.installHelmEndpoint, ingress: this.options.installIngressEndpoint, runner: this.options.installRunnerEndpoint, + prometheus: this.options.installPrometheusEndpoint, }; } diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index e731cdc3042..bd4a1fb37f9 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -28,6 +28,13 @@ export default class ClusterStore { requestStatus: null, requestReason: null, }, + prometheus: { + title: s__('ClusterIntegration|Prometheus'), + status: null, + statusReason: null, + requestStatus: null, + requestReason: null, + }, }, }; } diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index b6a0ece7907..525fbf9dac9 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -94,7 +94,7 @@ export default class ImageFile { }); return [maxWidth, maxHeight]; } - + // eslint-disable-next-line views = { 'two-up': function() { return $('.two-up.view .wrap', this.file).each((function(_this) { diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index e9a0dbaa59d..da0e8063ccb 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -4,6 +4,10 @@ import pipelinesMixin from '../../pipelines/mixins/pipelines'; export default { + mixins: [ + pipelinesMixin, + ], + props: { endpoint: { type: String, @@ -31,9 +35,6 @@ default: 'child', }, }, - mixins: [ - pipelinesMixin, - ], data() { const store = new PipelineStore(); @@ -95,28 +96,29 @@ label="Loading pipelines" size="3" v-if="isLoading" - /> + /> <empty-state v-if="shouldRenderEmptyState" :help-page-path="helpPagePath" :empty-state-svg-path="emptyStateSvgPath" - /> + /> <error-state v-if="shouldRenderErrorState" :error-state-svg-path="errorStateSvgPath" - /> + /> <div class="table-holder" - v-if="shouldRenderTable"> + v-if="shouldRenderTable" + > <pipelines-table-component :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" - /> + /> </div> </div> </template> diff --git a/app/assets/javascripts/commit_merge_requests.js b/app/assets/javascripts/commit_merge_requests.js new file mode 100644 index 00000000000..f76c9b7e690 --- /dev/null +++ b/app/assets/javascripts/commit_merge_requests.js @@ -0,0 +1,73 @@ +/* global Flash */ + +import axios from './lib/utils/axios_utils'; +import { n__, s__ } from './locale'; + +export function getHeaderText(childElementCount, mergeRequestCount) { + if (childElementCount === 0) { + return `${mergeRequestCount} ${n__('merge request', 'merge requests', mergeRequestCount)}`; + } + return ','; +} + +export function createHeader(childElementCount, mergeRequestCount) { + const headerText = getHeaderText(childElementCount, mergeRequestCount); + + return $('<span />', { + class: 'append-right-5', + text: headerText, + }); +} + +export function createLink(mergeRequest) { + return $('<a />', { + class: 'append-right-5', + href: mergeRequest.path, + text: `!${mergeRequest.iid}`, + }); +} + +export function createTitle(mergeRequest) { + return $('<span />', { + text: mergeRequest.title, + }); +} + +export function createItem(mergeRequest) { + const $item = $('<span />'); + const $link = createLink(mergeRequest); + const $title = createTitle(mergeRequest); + $item.append($link); + $item.append($title); + + return $item; +} + +export function createContent(mergeRequests) { + const $content = $('<span />'); + + if (mergeRequests.length === 0) { + $content.text(s__('Commits|No related merge requests found')); + } else { + mergeRequests.forEach((mergeRequest) => { + const $header = createHeader($content.children().length, mergeRequests.length); + const $item = createItem(mergeRequest); + $content.append($header); + $content.append($item); + }); + } + + return $content; +} + +export function fetchCommitMergeRequests() { + const $container = $('.merge-requests'); + + axios.get($container.data('projectCommitPath')) + .then((response) => { + const $content = createContent(response.data); + + $container.html($content); + }) + .catch(() => Flash(s__('Commits|An error occurred while fetching merge requests data.'))); +} diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 3a03cbf6b90..4b2f75fffde 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -5,6 +5,7 @@ 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 = {}; @@ -43,29 +44,30 @@ export default (function () { CommitsList.filterResults = function () { const form = $('.commits-search-form'); const search = CommitsList.searchField.val(); - if (search === CommitsList.lastSearch) return; + if (search === CommitsList.lastSearch) return Promise.resolve(); const commitsUrl = form.attr('action') + '?' + form.serialize(); CommitsList.content.fadeTo('fast', 0.5); - return $.ajax({ - type: 'GET', - url: form.attr('action'), - data: form.serialize(), - complete: function () { - return CommitsList.content.fadeTo('fast', 1.0); - }, - success: function (data) { + const params = form.serializeArray().reduce((acc, obj) => Object.assign(acc, { + [obj.name]: obj.value, + }), {}); + + return axios.get(form.attr('action'), { + params, + }) + .then(({ data }) => { CommitsList.lastSearch = search; CommitsList.content.html(data.html); - return history.replaceState({ - page: commitsUrl, + CommitsList.content.fadeTo('fast', 1.0); + // Change url so if user reload a page - search results are saved + history.replaceState({ + page: commitsUrl, }, document.title, commitsUrl); - }, - error: function () { + }) + .catch(() => { + CommitsList.content.fadeTo('fast', 1.0); CommitsList.lastSearch = null; - }, - dataType: 'json', - }); + }); }; // Prepare loaded data. diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index ff9e4485916..46232726510 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -8,6 +8,8 @@ import 'core-js/fn/promise'; import 'core-js/fn/string/code-point-at'; import 'core-js/fn/string/from-code-point'; import 'core-js/fn/symbol'; +import 'core-js/es6/map'; +import 'core-js/es6/weak-map'; // Browser polyfills import 'classlist-polyfill'; diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js index 144caf1d278..e2a008e8904 100644 --- a/app/assets/javascripts/compare.js +++ b/app/assets/javascripts/compare.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */ import { localTimeAgo } from './lib/utils/datetime_utility'; +import axios from './lib/utils/axios_utils'; export default class Compare { constructor(opts) { @@ -41,17 +42,14 @@ export default class Compare { } getTargetProject() { - return $.ajax({ - url: this.opts.targetProjectUrl, - data: { - target_project_id: $("input[name='merge_request[target_project_id]']").val() - }, - beforeSend: function() { - return $('.mr_target_commit').empty(); + $('.mr_target_commit').empty(); + + return axios.get(this.opts.targetProjectUrl, { + params: { + target_project_id: $("input[name='merge_request[target_project_id]']").val(), }, - success: function(html) { - return $('.js-target-branch-dropdown .dropdown-content').html(html); - } + }).then(({ data }) => { + $('.js-target-branch-dropdown .dropdown-content').html(data); }); } @@ -68,22 +66,19 @@ export default class Compare { }); } - static sendAjax(url, loading, target, data) { - var $target; - $target = $(target); - return $.ajax({ - url: url, - data: data, - beforeSend: function() { - loading.show(); - return $target.empty(); - }, - success: function(html) { - loading.hide(); - $target.html(html); - var className = '.' + $target[0].className.replace(' ', '.'); - localTimeAgo($('.js-timeago', className)); - } + static sendAjax(url, loading, target, params) { + const $target = $(target); + + loading.show(); + $target.empty(); + + return axios.get(url, { + params, + }).then(({ data }) => { + loading.hide(); + $target.html(data); + const className = '.' + $target[0].className.replace(' ', '.'); + localTimeAgo($('.js-timeago', className)); }); } } diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js index e633ef8a29e..59899e97be1 100644 --- a/app/assets/javascripts/compare_autocomplete.js +++ b/app/assets/javascripts/compare_autocomplete.js @@ -1,4 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */ +import { __ } from './locale'; +import axios from './lib/utils/axios_utils'; +import flash from './flash'; export default function initCompareAutocomplete() { $('.js-compare-dropdown').each(function() { @@ -10,15 +13,14 @@ export default function initCompareAutocomplete() { const $filterInput = $('input[type="search"]', $dropdownContainer); $dropdown.glDropdown({ data: function(term, callback) { - return $.ajax({ - url: $dropdown.data('refs-url'), - data: { + axios.get($dropdown.data('refsUrl'), { + params: { ref: $dropdown.data('ref'), search: term, - } - }).done(function(refs) { - return callback(refs); - }); + }, + }).then(({ data }) => { + callback(data); + }).catch(() => flash(__('Error fetching refs'))); }, selectable: true, filterable: true, diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js new file mode 100644 index 00000000000..42e9e568170 --- /dev/null +++ b/app/assets/javascripts/create_item_dropdown.js @@ -0,0 +1,119 @@ +import _ from 'underscore'; + +export default class CreateItemDropdown { + /** + * @param {Object} options containing + * `$dropdown` target element + * `onSelect` event callback + * $dropdown must be an element created using `dropdown_tag()` rails helper + */ + constructor(options) { + this.defaultToggleLabel = options.defaultToggleLabel; + this.fieldName = options.fieldName; + this.onSelect = options.onSelect || (() => {}); + this.getDataOption = options.getData; + this.createNewItemFromValueOption = options.createNewItemFromValue; + this.$dropdown = options.$dropdown; + this.$dropdownContainer = this.$dropdown.parent(); + this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); + this.$createButton = this.$dropdownContainer.find('.js-dropdown-create-new-item'); + + this.buildDropdown(); + this.bindEvents(); + + // Hide footer + this.toggleFooter(true); + } + + buildDropdown() { + this.$dropdown.glDropdown({ + data: this.getData.bind(this), + filterable: true, + remote: false, + search: { + fields: ['text'], + }, + selectable: true, + toggleLabel(selected) { + return (selected && 'id' in selected) ? _.escape(selected.title) : this.defaultToggleLabel; + }, + fieldName: this.fieldName, + text(item) { + return _.escape(item.text); + }, + id(item) { + return _.escape(item.id); + }, + onFilter: this.toggleCreateNewButton.bind(this), + clicked: (options) => { + options.e.preventDefault(); + this.onSelect(); + }, + }); + } + + clearDropdown() { + this.$dropdownContainer.find('.dropdown-content').html(''); + this.$dropdownContainer.find('.dropdown-input-field').val(''); + } + + bindEvents() { + this.$createButton.on('click', this.onClickCreateWildcard.bind(this)); + } + + onClickCreateWildcard(e) { + e.preventDefault(); + + this.refreshData(); + this.$dropdown.data('glDropdown').selectRowAtIndex(); + } + + refreshData() { + // Refresh the dropdown's data, which ends up calling `getData` + this.$dropdown.data('glDropdown').remote.execute(); + } + + getData(term, callback) { + this.getDataOption(term, (data = []) => { + // Ensure the selected item isn't already in the data to avoid duplicates + const alreadyHasSelectedItem = this.selectedItem && data.some(item => + item.id === this.selectedItem.id, + ); + + let uniqueData = data; + if (!alreadyHasSelectedItem) { + uniqueData = data.concat(this.selectedItem || []); + } + + callback(uniqueData); + }); + } + + createNewItemFromValue(newValue) { + if (this.createNewItemFromValueOption) { + return this.createNewItemFromValueOption(newValue); + } + + return { + title: newValue, + id: newValue, + text: newValue, + }; + } + + toggleCreateNewButton(newValue) { + if (newValue) { + this.selectedItem = this.createNewItemFromValue(newValue); + + this.$dropdownContainer + .find('.js-dropdown-create-new-item code') + .text(newValue); + } + + this.toggleFooter(!newValue); + } + + toggleFooter(toggleState) { + this.$dropdownFooter.toggleClass('hidden', toggleState); + } +} diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index 23425672b16..482d83621e2 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -1,4 +1,6 @@ /* eslint-disable no-new */ +import _ from 'underscore'; +import axios from './lib/utils/axios_utils'; import Flash from './flash'; import DropLab from './droplab/drop_lab'; import ISetter from './droplab/plugins/input_setter'; @@ -73,60 +75,52 @@ export default class CreateMergeRequestDropdown { } checkAbilityToCreateBranch() { - return $.ajax({ - type: 'GET', - dataType: 'json', - url: this.canCreatePath, - beforeSend: () => this.setUnavailableButtonState(), - }) - .done((data) => { - this.setUnavailableButtonState(false); - - if (data.can_create_branch) { - this.available(); - this.enable(); - - if (!this.droplabInitialized) { - this.droplabInitialized = true; - this.initDroplab(); - this.bindEvents(); + this.setUnavailableButtonState(); + + axios.get(this.canCreatePath) + .then(({ data }) => { + this.setUnavailableButtonState(false); + + if (data.can_create_branch) { + this.available(); + this.enable(); + + if (!this.droplabInitialized) { + this.droplabInitialized = true; + this.initDroplab(); + this.bindEvents(); + } + } else if (data.has_related_branch) { + this.hide(); } - } else if (data.has_related_branch) { - this.hide(); - } - }).fail(() => { - this.unavailable(); - this.disable(); - new Flash('Failed to check if a new branch can be created.'); - }); + }) + .catch(() => { + this.unavailable(); + this.disable(); + Flash('Failed to check if a new branch can be created.'); + }); } createBranch() { - return $.ajax({ - method: 'POST', - dataType: 'json', - url: this.createBranchPath, - beforeSend: () => (this.isCreatingBranch = true), - }) - .done((data) => { - this.branchCreated = true; - window.location.href = data.url; - }) - .fail(() => new Flash('Failed to create a branch for this issue. Please try again.')); + this.isCreatingBranch = true; + + return axios.post(this.createBranchPath) + .then(({ data }) => { + this.branchCreated = true; + window.location.href = data.url; + }) + .catch(() => Flash('Failed to create a branch for this issue. Please try again.')); } createMergeRequest() { - return $.ajax({ - method: 'POST', - dataType: 'json', - url: this.createMrPath, - beforeSend: () => (this.isCreatingMergeRequest = true), - }) - .done((data) => { - this.mergeRequestCreated = true; - window.location.href = data.url; - }) - .fail(() => new Flash('Failed to create Merge Request. Please try again.')); + this.isCreatingMergeRequest = true; + + return axios.post(this.createMrPath) + .then(({ data }) => { + this.mergeRequestCreated = true; + window.location.href = data.url; + }) + .catch(() => Flash('Failed to create Merge Request. Please try again.')); } disable() { @@ -199,39 +193,33 @@ export default class CreateMergeRequestDropdown { getRef(ref, target = 'all') { if (!ref) return false; - return $.ajax({ - method: 'GET', - dataType: 'json', - url: this.refsPath + ref, - beforeSend: () => { - this.isGettingRef = true; - }, - }) - .always(() => { - this.isGettingRef = false; - }) - .done((data) => { - const branches = data[Object.keys(data)[0]]; - const tags = data[Object.keys(data)[1]]; - let result; + return axios.get(this.refsPath + ref) + .then(({ data }) => { + const branches = data[Object.keys(data)[0]]; + const tags = data[Object.keys(data)[1]]; + let result; + + if (target === 'branch') { + result = CreateMergeRequestDropdown.findByValue(branches, ref); + } else { + result = CreateMergeRequestDropdown.findByValue(branches, ref, true) || + CreateMergeRequestDropdown.findByValue(tags, ref, true); + this.suggestedRef = result; + } - if (target === 'branch') { - result = CreateMergeRequestDropdown.findByValue(branches, ref); - } else { - result = CreateMergeRequestDropdown.findByValue(branches, ref, true) || - CreateMergeRequestDropdown.findByValue(tags, ref, true); - this.suggestedRef = result; - } + this.isGettingRef = false; - return this.updateInputState(target, ref, result); - }) - .fail(() => { - this.unavailable(); - this.disable(); - new Flash('Failed to get ref.'); + return this.updateInputState(target, ref, result); + }) + .catch(() => { + this.unavailable(); + this.disable(); + new Flash('Failed to get ref.'); - return false; - }); + this.isGettingRef = false; + + return false; + }); } getTargetData(target) { @@ -276,13 +264,13 @@ export default class CreateMergeRequestDropdown { let target; let value; - if (event.srcElement === this.branchInput) { + if (event.target === this.branchInput) { target = 'branch'; value = this.branchInput.value; - } else if (event.srcElement === this.refInput) { + } else if (event.target === this.refInput) { target = 'ref'; - value = event.srcElement.value.slice(0, event.srcElement.selectionStart) + - event.srcElement.value.slice(event.srcElement.selectionEnd); + value = event.target.value.slice(0, event.target.selectionStart) + + event.target.value.slice(event.target.selectionEnd); } else { return false; } @@ -331,12 +319,12 @@ export default class CreateMergeRequestDropdown { xhr = this.createBranch(); } - xhr.fail(() => { + xhr.catch(() => { this.isCreatingMergeRequest = false; this.isCreatingBranch = false; - }); - xhr.always(() => this.enable()); + this.enable(); + }); this.disable(); } diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue index 732697c134e..3204b8dd8e7 100644 --- a/app/assets/javascripts/cycle_analytics/components/banner.vue +++ b/app/assets/javascripts/cycle_analytics/components/banner.vue @@ -26,28 +26,34 @@ class="js-ca-dismiss-button dismiss-button" type="button" :aria-label="__('Dismiss Cycle Analytics introduction box')" - @click="dismissOverviewDialog"> + @click="dismissOverviewDialog" + > <i class="fa fa-times" aria-hidden="true"> </i> </button> - <div class="svg-container" v-html="iconCycleAnalyticsSplash"> + <div + class="svg-container" + v-html="iconCycleAnalyticsSplash" + > </div> <div class="inner-content"> <h4> - {{__('Introducing Cycle Analytics')}} + {{ __('Introducing Cycle Analytics') }} </h4> <p> - {{ __('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.') }} + {{ __(`Cycle Analytics gives an overview +of how much time it takes to go from idea to production in your project.`) }} </p> <p> <a :href="documentationLink" target="_blank" rel="nofollow" - class="btn"> - {{__('Read more')}} + class="btn" + > + {{ __('Read more') }} </a> </p> </div> diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue index 6e94ba929b2..32ae0cc1476 100644 --- a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue @@ -2,25 +2,34 @@ import tooltip from '../../vue_shared/directives/tooltip'; export default { + directives: { + tooltip, + }, props: { count: { type: Number, required: true, }, }, - directives: { - tooltip, - }, }; </script> <template> - <span v-if="count === 50" class="events-info pull-right"> + <span + v-if="count === 50" + class="events-info pull-right" + > <i class="fa fa-warning" v-tooltip aria-hidden="true" - :title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)" - data-placement="top"></i> + :title="n__( + 'Limited to showing %d event at most', + 'Limited to showing %d events at most', + 50 + )" + data-placement="top" + > + </i> {{ n__('Showing %d event', 'Showing %d events', 50) }} </span> </template> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue index 45930145b0a..a71dcf78103 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue @@ -4,15 +4,21 @@ import totalTime from './total_time_component.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { userAvatarImage, limitWarning, totalTime, }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, + }, }; </script> <template> @@ -22,28 +28,44 @@ <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> - <li v-for="mergeRequest in items" class="stage-event-item"> + <li + v-for="(mergeRequest, i) in items" + :key="i" + class="stage-event-item" + > <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 }} </a> </h5> - <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> + <a + :href="mergeRequest.url" + class="issue-link"> + !{{ mergeRequest.iid }} + </a> · <span> {{ s__('OpenedNDaysAgo|Opened') }} - <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> + <a + :href="mergeRequest.url" + class="issue-date"> + {{ mergeRequest.createdAt }} + </a> </span> <span> {{ s__('ByAuthor|by') }} - <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> + <a + :href="mergeRequest.author.webUrl" + class="issue-author-link"> + {{ mergeRequest.author.name }} + </a> </span> </div> <div class="item-time"> - <total-time :time="mergeRequest.totalTime"></total-time> + <total-time :time="mergeRequest.totalTime" /> </div> </li> </ul> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_component.vue index 8c98bd249a1..907638d798a 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_component.vue @@ -4,15 +4,21 @@ import totalTime from './total_time_component.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { userAvatarImage, limitWarning, totalTime, }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, + }, }; </script> <template> @@ -25,30 +31,43 @@ <li v-for="(issue, i) in items" :key="i" - class="stage-event-item"> + class="stage-event-item" + > <div class="item-details"> <!-- FIXME: Pass an alt attribute here for accessibility --> <user-avatar-image :img-src="issue.author.avatarUrl"/> <h5 class="item-title issue-title"> - <a class="issue-title" :href="issue.url"> + <a + class="issue-title" + :href="issue.url" + > {{ issue.title }} </a> </h5> - <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> + <a + :href="issue.url" + class="issue-link" + >#{{ issue.iid }}</a> · <span> {{ s__('OpenedNDaysAgo|Opened') }} - <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> + <a + :href="issue.url" + class="issue-date" + >{{ issue.createdAt }}</a> </span> <span> {{ s__('ByAuthor|by') }} - <a :href="issue.author.webUrl" class="issue-author-link"> + <a + :href="issue.author.webUrl" + class="issue-author-link" + > {{ issue.author.name }} </a> </span> </div> <div class="item-time"> - <total-time :time="issue.totalTime"/> + <total-time :time="issue.totalTime" /> </div> </li> </ul> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue index 75d2f1fd70c..cee294b4ac2 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue @@ -5,15 +5,21 @@ import totalTime from './total_time_component.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { userAvatarImage, totalTime, limitWarning, }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, + }, computed: { iconCommit() { return iconCommit; @@ -31,10 +37,11 @@ <li v-for="(commit, i) in items" :key="i" - class="stage-event-item"> + class="stage-event-item" + > <div class="item-details item-conmmit-component"> <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image :img-src="commit.author.avatarUrl"/> + <user-avatar-image :img-src="commit.author.avatarUrl" /> <h5 class="item-title commit-title"> <a :href="commit.commitUrl"> {{ commit.title }} @@ -42,10 +49,20 @@ </h5> <span> {{ s__('FirstPushedBy|First') }} - <span class="commit-icon" v-html="iconCommit"></span> - <a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a> + <span + class="commit-icon" + v-html="iconCommit" + > + </span> + <a + :href="commit.commitUrl" + class="commit-hash-link commit-sha" + >{{ commit.shortSha }}</a> {{ s__('FirstPushedBy|pushed by') }} - <a :href="commit.author.webUrl" class="commit-author-link"> + <a + :href="commit.author.webUrl" + class="commit-author-link" + > {{ commit.author.name }} </a> </span> 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 f54ea7df522..39b699a6395 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue @@ -2,16 +2,24 @@ import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; import limitWarning from './limit_warning_component.vue'; import totalTime from './total_time_component.vue'; + import icon from '../../vue_shared/components/icon.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { userAvatarImage, totalTime, limitWarning, + icon, + }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, }, }; </script> @@ -25,7 +33,8 @@ <li v-for="(mergeRequest, i) in items" :key="i" - class="stage-event-item"> + class="stage-event-item" + > <div class="item-details"> <!-- FIXME: Pass an alt attribute here for accessibility --> <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> @@ -34,31 +43,52 @@ {{ mergeRequest.title }} </a> </h5> - <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> + <a + :href="mergeRequest.url" + class="issue-link" + >!{{ mergeRequest.iid }}</a> · <span> {{ s__('OpenedNDaysAgo|Opened') }} - <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> + <a + :href="mergeRequest.url" + class="issue-date" + >{{ mergeRequest.createdAt }}</a> </span> <span> {{ s__('ByAuthor|by') }} - <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> + <a + :href="mergeRequest.author.webUrl" + class="issue-author-link" + >{{ mergeRequest.author.name }}</a> </span> <template v-if="mergeRequest.state === 'closed'"> <span class="merge-request-state"> - <i class="fa fa-ban"></i> + <i + class="fa fa-ban" + aria-hidden="true" + > + </i> {{ mergeRequest.state.toUpperCase() }} </span> </template> <template v-else> - <span class="merge-request-branch" v-if="mergeRequest.branch"> - <i class= "fa fa-code-fork"></i> - <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a> + <span + class="merge-request-branch" + v-if="mergeRequest.branch" + > + <icon + name="fork" + :size="16" + /> + <a :href="mergeRequest.branch.url"> + {{ mergeRequest.branch.name }} + </a> </span> </template> </div> <div class="item-time"> - <total-time :time="mergeRequest.totalTime"/> + <total-time :time="mergeRequest.totalTime" /> </div> </li> </ul> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue index 5d95ddcd90e..92f2a95a66a 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue @@ -3,16 +3,24 @@ import iconBranch from '../svg/icon_branch.svg'; import limitWarning from './limit_warning_component.vue'; import totalTime from './total_time_component.vue'; + import icon from '../../vue_shared/components/icon.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { userAvatarImage, totalTime, limitWarning, + icon, + }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, }, computed: { iconBranch() { @@ -31,27 +39,58 @@ <li v-for="(build, i) in items" class="stage-event-item item-build-component" - :key="i"> + :key="i" + > <div class="item-details"> <!-- FIXME: Pass an alt attribute here for accessibility --> <user-avatar-image :img-src="build.author.avatarUrl"/> <h5 class="item-title"> - <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> - <i class="fa fa-code-fork"></i> - <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a> - <span class="icon-branch" v-html="iconBranch"></span> - <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a> + <a + :href="build.url" + class="pipeline-id" + > + #{{ build.id }} + </a> + <icon + name="fork" + :size="16" + /> + <a + :href="build.branch.url" + class="ref-name" + > + {{ build.branch.name }} + </a> + <span + class="icon-branch" + v-html="iconBranch" + > + </span> + <a + :href="build.commitUrl" + class="commit-sha" + > + {{ build.shortSha }} + </a> </h5> <span> - <a :href="build.url" class="build-date">{{ build.date }}</a> + <a + :href="build.url" + class="build-date" + > + {{ build.date }} + </a> {{ s__('ByAuthor|by') }} - <a :href="build.author.webUrl" class="issue-author-link"> + <a + :href="build.author.webUrl" + class="issue-author-link" + > {{ build.author.name }} </a> </span> </div> <div class="item-time"> - <total-time :time="build.totalTime"/> + <total-time :time="build.totalTime" /> </div> </li> </ul> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue index 04d5440b77b..b84bb6ed792 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue @@ -3,15 +3,23 @@ import iconBranch from '../svg/icon_branch.svg'; import limitWarning from './limit_warning_component.vue'; import totalTime from './total_time_component.vue'; + import icon from '../../vue_shared/components/icon.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { totalTime, limitWarning, + icon, + }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, }, computed: { iconBuildStatus() { @@ -33,26 +41,59 @@ <li v-for="(build, i) in items" :key="i" - class="stage-event-item item-build-component"> + class="stage-event-item item-build-component" + > <div class="item-details"> <h5 class="item-title"> - <span class="icon-build-status" v-html="iconBuildStatus"></span> - <a :href="build.url" class="item-build-name">{{ build.name }}</a> + <span + class="icon-build-status" + v-html="iconBuildStatus" + > + </span> + <a + :href="build.url" + class="item-build-name" + > + {{ build.name }} + </a> · - <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> - <i class="fa fa-code-fork"></i> - <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a> - <span class="icon-branch" v-html="iconBranch"></span> - <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a> + <a + :href="build.url" + class="pipeline-id" + > + #{{ build.id }} + </a> + <icon + name="fork" + :size="16" + /> + <a + :href="build.branch.url" + class="ref-name" + > + {{ build.branch.name }} + </a> + <span + class="icon-branch" + v-html="iconBranch" + > + </span> + <a + :href="build.commitUrl" + class="commit-sha"> + {{ build.shortSha }} + </a> </h5> <span> - <a :href="build.url" class="issue-date"> + <a + :href="build.url" + class="issue-date"> {{ build.date }} </a> </span> </div> <div class="item-time"> - <total-time :time="build.totalTime"/> + <total-time :time="build.totalTime" /> </div> </li> </ul> diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue index 62efd4f9c28..7758bf0cb3f 100644 --- a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue @@ -17,13 +17,33 @@ <template> <span class="total-time"> <template v-if="hasData"> - <template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template> - <template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template> - <template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template> - <template v-if="time.seconds && hasData === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template> + <template v-if="time.days"> + {{ time.days }} + <span> + {{ n__('day', 'days', time.days) }} + </span> + </template> + <template v-if="time.hours"> + {{ time.hours }} + <span> + {{ n__('Time|hr', 'Time|hrs', time.hours) }} + </span> + </template> + <template v-if="time.mins && !time.days"> + {{ time.mins }} + <span> + {{ n__('Time|min', 'Time|mins', time.mins) }} + </span> + </template> + <template v-if="time.seconds && hasData === 1 || time.seconds === 0"> + {{ time.seconds }} + <span> + {{ s__('Time|s') }} + </span> + </template> </template> <template v-else> -- </template> - </span> + </span> </template> diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 49bb6c52180..034f2923b3b 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -20,6 +20,16 @@ $(() => { gl.cycleAnalyticsApp = new Vue({ el: '#cycle-analytics', name: 'CycleAnalytics', + components: { + banner, + 'stage-issue-component': stageComponent, + 'stage-plan-component': stagePlanComponent, + 'stage-code-component': stageCodeComponent, + 'stage-test-component': stageTestComponent, + 'stage-review-component': stageReviewComponent, + 'stage-staging-component': stageStagingComponent, + 'stage-production-component': stageComponent, + }, data() { const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); const cycleAnalyticsService = new CycleAnalyticsService({ @@ -43,16 +53,6 @@ $(() => { return this.store.currentActiveStage(); }, }, - components: { - banner, - 'stage-issue-component': stageComponent, - 'stage-plan-component': stagePlanComponent, - 'stage-code-component': stageCodeComponent, - 'stage-test-component': stageTestComponent, - 'stage-review-component': stageReviewComponent, - 'stage-staging-component': stageStagingComponent, - 'stage-production-component': stageComponent, - }, created() { this.fetchCycleAnalyticsData(); }, diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue index f9f2f9bf693..b839b9f286f 100644 --- a/app/assets/javascripts/deploy_keys/components/action_btn.vue +++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue @@ -3,10 +3,8 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { - data() { - return { - isLoading: false, - }; + components: { + loadingIcon, }, props: { deployKey: { @@ -23,11 +21,16 @@ default: 'btn-default', }, }, - - components: { - loadingIcon, + data() { + return { + isLoading: false, + }; + }, + computed: { + text() { + return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`; + }, }, - methods: { doAction() { this.isLoading = true; @@ -37,11 +40,6 @@ }); }, }, - computed: { - text() { - return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`; - }, - }, }; </script> diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index fe046449054..5a782237b7d 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -7,11 +7,9 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { - data() { - return { - isLoading: false, - store: new DeployKeysStore(), - }; + components: { + keysPanel, + loadingIcon, }, props: { endpoint: { @@ -19,6 +17,12 @@ required: true, }, }, + data() { + return { + isLoading: false, + store: new DeployKeysStore(), + }; + }, computed: { hasKeys() { return Object.keys(this.keys).length; @@ -27,9 +31,20 @@ return this.store.keys; }, }, - components: { - keysPanel, - loadingIcon, + created() { + this.service = new DeployKeysService(this.endpoint); + + eventHub.$on('enable.key', this.enableKey); + eventHub.$on('remove.key', this.disableKey); + eventHub.$on('disable.key', this.disableKey); + }, + mounted() { + this.fetchKeys(); + }, + beforeDestroy() { + eventHub.$off('enable.key', this.enableKey); + eventHub.$off('remove.key', this.disableKey); + eventHub.$off('disable.key', this.disableKey); }, methods: { fetchKeys() { @@ -59,21 +74,6 @@ } }, }, - created() { - this.service = new DeployKeysService(this.endpoint); - - eventHub.$on('enable.key', this.enableKey); - eventHub.$on('remove.key', this.disableKey); - eventHub.$on('disable.key', this.disableKey); - }, - mounted() { - this.fetchKeys(); - }, - beforeDestroy() { - eventHub.$off('enable.key', this.enableKey); - eventHub.$off('remove.key', this.disableKey); - eventHub.$off('disable.key', this.disableKey); - }, }; </script> @@ -87,6 +87,7 @@ <div v-else-if="hasKeys"> <keys-panel title="Enabled deploy keys for this project" + class="qa-project-deploy-keys" :keys="keys.enabled_keys" :store="store" :endpoint="endpoint" diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index 2a05c6f001e..c6091efd62f 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -1,8 +1,15 @@ <script> import actionBtn from './action_btn.vue'; import { getTimeago } from '../../lib/utils/datetime_utility'; + import tooltip from '../../vue_shared/directives/tooltip'; export default { + components: { + actionBtn, + }, + directives: { + tooltip, + }, props: { deployKey: { type: Object, @@ -17,9 +24,6 @@ required: true, }, }, - components: { - actionBtn, - }, computed: { timeagoDate() { return getTimeago().format(this.deployKey.created_at); @@ -32,6 +36,9 @@ isEnabled(id) { return this.store.findEnabledKey(id) !== undefined; }, + tooltipTitle(project) { + return project.can_push ? 'Write access allowed' : 'Read access only'; + }, }, }; </script> @@ -46,26 +53,29 @@ </i> </div> <div class="deploy-key-content key-list-item-info"> - <strong class="title"> + <strong class="title qa-key-title"> {{ deployKey.title }} </strong> - <div class="description"> + <div class="description qa-key-fingerprint"> {{ deployKey.fingerprint }} </div> - <div - v-if="deployKey.can_push" - class="write-access-allowed" - > - Write access allowed - </div> </div> <div class="deploy-key-content prepend-left-default deploy-key-projects"> <a - v-for="project in deployKey.projects" + v-for="(deployKeysProject, i) in deployKey.deploy_keys_projects" + :key="i" class="label deploy-project-label" - :href="project.full_path" + :href="deployKeysProject.project.full_path" + :title="tooltipTitle(deployKeysProject)" + v-tooltip > - {{ project.full_name }} + {{ deployKeysProject.project.full_name }} + <i + v-if="!deployKeysProject.can_push" + aria-hidden="true" + class="fa fa-lock" + > + </i> </a> </div> <div class="deploy-key-content"> diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue index 9e6fb244af6..822b0323156 100644 --- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue +++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue @@ -2,6 +2,9 @@ import key from './key.vue'; export default { + components: { + key, + }, props: { title: { type: String, @@ -25,9 +28,6 @@ required: true, }, }, - components: { - key, - }, }; </script> @@ -37,12 +37,14 @@ {{ title }} ({{ keys.length }}) </h5> - <ul class="well-list" + <ul + class="well-list" v-if="keys.length" > <li v-for="deployKey in keys" - :key="deployKey.id"> + :key="deployKey.id" + > <key :deploy-key="deployKey" :store="store" diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js index a5f232f950a..ca8798facc9 100644 --- a/app/assets/javascripts/deploy_keys/index.js +++ b/app/assets/javascripts/deploy_keys/index.js @@ -3,14 +3,14 @@ import deployKeysApp from './components/app.vue'; document.addEventListener('DOMContentLoaded', () => new Vue({ el: document.getElementById('js-deploy-keys'), + components: { + deployKeysApp, + }, data() { return { endpoint: this.$options.el.dataset.endpoint, }; }, - components: { - deployKeysApp, - }, render(createElement) { return createElement('deploy-keys-app', { props: { diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 118437b82a3..ab28b7d8d44 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,101 +1,20 @@ /* 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 { s__ } from './locale'; -import projectSelect from './project_select'; -import IssuableIndex from './issuable_index'; -import Milestone from './milestone'; -import IssuableForm from './issuable_form'; -import LabelsSelect from './labels_select'; -/* global MilestoneSelect */ -import NewBranchForm from './new_branch_form'; -import NotificationsForm from './notifications_form'; -import notificationsDropdown from './notifications_dropdown'; -import groupAvatar from './group_avatar'; -import GroupLabelSubscription from './group_label_subscription'; -import LineHighlighter from './line_highlighter'; -import BuildArtifacts from './build_artifacts'; -import CILintEditor from './ci_lint_editor'; -import groupsSelect from './groups_select'; -import Search from './search'; -import initAdmin from './admin'; -import NamespaceSelect from './namespace_select'; -import NewCommitForm from './new_commit_form'; -import Project from './project'; -import projectAvatar from './project_avatar'; import MergeRequest from './merge_request'; -import Compare from './compare'; -import initCompareAutocomplete from './compare_autocomplete'; -import ProjectFindFile from './project_find_file'; -import ProjectNew from './project_new'; -import projectImport from './project_import'; -import Labels from './labels'; -import LabelManager from './label_manager'; -import Sidebar from './right_sidebar'; -import IssuableTemplateSelectors from './templates/issuable_template_selectors'; import Flash from './flash'; -import CommitsList from './commits'; -import Issue from './issue'; -import BindInOut from './behaviors/bind_in_out'; -import SecretValues from './behaviors/secret_values'; -import DeleteModal from './branches/branches_delete_modal'; -import Group from './group'; -import GroupsList from './groups_list'; -import ProjectsList from './projects_list'; -import setupProjectEdit from './project_edit'; -import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; -import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; -import Landing from './landing'; -import BlobForkSuggestion from './blob/blob_fork_suggestion'; -import UserCallout from './user_callout'; -import ShortcutsWiki from './shortcuts_wiki'; -import Pipelines from './pipelines'; -import BlobViewer from './blob/viewer/index'; -import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; -import UsersSelect from './users_select'; -import RefSelectDropdown from './ref_select_dropdown'; import GfmAutoComplete from './gfm_auto_complete'; -import ShortcutsBlob from './shortcuts_blob'; -import SigninTabsMemoizer from './signin_tabs_memoizer'; -import Star from './star'; -import Todos from './todos'; -import TreeView from './tree'; -import UsagePing from './usage_ping'; -import UsernameValidator from './username_validator'; -import VersionCheckImage from './version_check_image'; -import Wikis from './wikis'; import ZenMode from './zen_mode'; -import initSettingsPanels from './settings_panels'; -import initExperimentalFlags from './experimental_flags'; -import OAuthRememberMe from './oauth_remember_me'; -import PerformanceBar from './performance_bar'; -import initBroadcastMessagesForm from './broadcast_message'; import initNotes from './init_notes'; -import initLegacyFilters from './init_legacy_filters'; import initIssuableSidebar from './init_issuable_sidebar'; -import initProjectVisibilitySelector from './project_visibility'; -import GpgBadges from './gpg_badges'; -import initChangesDropdown from './init_changes_dropdown'; -import NewGroupChild from './groups/new_group_child'; -import AbuseReports from './abuse_reports'; -import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; -import AjaxLoadingSpinner from './ajax_loading_spinner'; +import { convertPermissionToBoolean } from './lib/utils/common_utils'; import GlFieldErrors from './gl_field_errors'; -import GLForm from './gl_form'; import Shortcuts from './shortcuts'; -import ShortcutsNavigation from './shortcuts_navigation'; -import ShortcutsFindFile from './shortcuts_find_file'; import ShortcutsIssuable from './shortcuts_issuable'; -import U2FAuthenticate from './u2f/authenticate'; -import Members from './members'; -import memberExpirationDate from './member_expiration_date'; -import DueDateSelectors from './due_date_select'; import Diff from './diff'; -import ProjectLabelSubscription from './project_label_subscription'; import SearchAutocomplete from './search_autocomplete'; -import Activities from './activities'; -(function() { - var Dispatcher; +var Dispatcher; +(function() { Dispatcher = (function() { function Dispatcher() { this.initSearch(); @@ -104,13 +23,14 @@ import Activities from './activities'; } Dispatcher.prototype.initPageScripts = function() { - var path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl; + 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; @@ -128,198 +48,203 @@ import Activities from './activities'; }); }); - function initBlob() { - new LineHighlighter(); - - new BlobLinePermalinkUpdater( - document.querySelector('#blob-content-holder'), - '.diff-line-num[data-line-number]', - document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'), - ); - - shortcut_handler = new ShortcutsNavigation(); - fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); - fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); - new ShortcutsBlob({ - skipResetBindings: true, - fileBlobPermalinkUrl, - }); - - new BlobForkSuggestion({ - openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'), - forkButtons: document.querySelectorAll('.js-fork-suggestion-button'), - cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'), - suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'), - actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'), - }) - .init(); - } - - const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); - switch (page) { - case 'profiles:preferences:show': - initExperimentalFlags(); - break; - case 'sessions:new': - new UsernameValidator(); - new SigninTabsMemoizer(); - new OAuthRememberMe({ container: $(".omniauth-container") }).bindEvents(); - break; - case 'projects:boards:show': - case 'projects:boards:index': - shortcut_handler = new ShortcutsNavigation(); - new UsersSelect(); + case 'projects:environments:metrics': + import('./pages/projects/environments/metrics') + .then(callDefault) + .catch(fail); break; case 'projects:merge_requests:index': case 'projects:issues:index': - if (filteredSearchEnabled) { - const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests'); - filteredSearchManager.setup(); - } - const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_'; - new IssuableIndex(pagePrefix); - - shortcut_handler = new ShortcutsNavigation(); - new UsersSelect(); - break; case 'projects:issues:show': - new Issue(); - shortcut_handler = new ShortcutsIssuable(); - new ZenMode(); - initIssuableSidebar(); + shortcut_handler = true; break; - case 'dashboard:milestones:index': - projectSelect(); + 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': - new Milestone(); - new Sidebar(); + 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': - projectSelect(); - initLegacyFilters(); + 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': - if (filteredSearchEnabled) { - const filteredSearchManager = new gl.FilteredSearchManager(page === 'groups:issues' ? 'issues' : 'merge_requests'); - filteredSearchManager.setup(); - } - projectSelect(); + import('./pages/groups/merge_requests') + .then(callDefault) + .catch(fail); break; case 'dashboard:todos:index': - new Todos(); + 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': - case 'admin:projects:index': - new ProjectsList(); + import('./pages/explore/projects') + .then(callDefault) + .catch(fail); break; case 'explore:groups:index': - new GroupsList(); - const landingElement = document.querySelector('.js-explore-groups-landing'); - if (!landingElement) break; - const exploreGroupsLanding = new Landing( - landingElement, - landingElement.querySelector('.dismiss-button'), - 'explore_groups_landing_dismissed', - ); - exploreGroupsLanding.toggle(); + 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': - new ZenMode(); - new DueDateSelectors(); - new GLForm($('.milestone-form'), true); + 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': - new ZenMode(); - new DueDateSelectors(); - new GLForm($('.milestone-form'), false); + import('./pages/groups/milestones/edit') + .then(callDefault) + .catch(fail); break; case 'projects:compare:show': - new Diff(); - const paddingTop = 16; - initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop); + 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': - new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)); + import('./pages/projects/branches/new') + .then(callDefault) + .catch(fail); break; case 'projects:branches:index': - AjaxLoadingSpinner.init(); - new DeleteModal(); + 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': - shortcut_handler = new ShortcutsNavigation(); - new GLForm($('.issue-form'), true); - new IssuableForm($('.issue-form')); - new LabelsSelect(); - new MilestoneSelect(); - new IssuableTemplateSelectors(); + import('./pages/projects/issues/edit') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:merge_requests:creations:new': - const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); - if (mrNewCompareNode) { - new Compare({ - targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl, - sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl, - targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl, - }); - } else { - const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit'); - new MergeRequest({ - action: mrNewSubmitNode.dataset.mrSubmitAction, - }); - } + 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': - new Diff(); - shortcut_handler = new ShortcutsNavigation(); - new GLForm($('.merge-request-form'), true); - new IssuableForm($('.merge-request-form')); - new LabelsSelect(); - new MilestoneSelect(); - new IssuableTemplateSelectors(); - new AutoWidthDropdownSelect($('.js-target-branch-select')).init(); + import('./pages/projects/merge_requests/edit') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:tags:new': - new ZenMode(); - new GLForm($('.tag-form'), true); - new RefSelectDropdown($('.js-branch-select')); + import('./pages/projects/tags/new') + .then(callDefault) + .catch(fail); break; case 'projects:snippets:show': - initNotes(); - new ZenMode(); + import('./pages/projects/snippets/show') + .then(callDefault) + .catch(fail); break; case 'projects:snippets:new': - case 'projects:snippets:edit': case 'projects:snippets:create': + import('./pages/projects/snippets/new') + .then(callDefault) + .catch(fail); + break; + case 'projects:snippets:edit': case 'projects:snippets:update': - new GLForm($('.snippet-form'), true); - new ZenMode(); + 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': - new GLForm($('.snippet-form'), false); - new ZenMode(); + import('./pages/snippets/edit') + .then(callDefault) + .catch(fail); break; case 'projects:releases:edit': - new ZenMode(); - new GLForm($('.release-form'), true); + import('./pages/projects/releases/edit') + .then(callDefault) + .catch(fail); break; case 'projects:merge_requests:show': new Diff(); @@ -332,166 +257,155 @@ import Activities from './activities'; window.mergeRequest = new MergeRequest({ action: mrShowNode.dataset.mrAction, }); - shortcut_handler = new ShortcutsIssuable(true); break; case 'dashboard:activity': - new Activities(); + import('./pages/dashboard/activity') + .then(callDefault) + .catch(fail); break; case 'projects:commit:show': - new Diff(); - new ZenMode(); - shortcut_handler = new ShortcutsNavigation(); - new MiniPipelineGraph({ - container: '.js-commit-pipeline-graph', - }).bindEvents(); - initNotes(); - const stickyBarPaddingTop = 16; - initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop); - $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); + import('./pages/projects/commit/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:commit:pipelines': - new MiniPipelineGraph({ - container: '.js-commit-pipeline-graph', - }).bindEvents(); - $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); + import('./pages/projects/commit/pipelines') + .then(callDefault) + .catch(fail); break; case 'projects:activity': - new Activities(); - shortcut_handler = new ShortcutsNavigation(); + import('./pages/projects/activity') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:commits:show': - CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit); - shortcut_handler = new ShortcutsNavigation(); - GpgBadges.fetch(); + import('./pages/projects/commits/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:show': - shortcut_handler = new ShortcutsNavigation(); - new NotificationsForm(); - new UserCallout({ - setCalloutPerProject: true, - className: 'js-autodevops-banner', - }); - - if ($('#tree-slider').length) new TreeView(); - if ($('.blob-viewer').length) new BlobViewer(); - if ($('.project-show-activity').length) new Activities(); - $('#tree-slider').waitForImages(function() { - ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); - }); + shortcut_handler = true; break; case 'projects:edit': - setupProjectEdit(); - // Initialize expandable settings panels - initSettingsPanels(); + import('./pages/projects/edit') + .then(callDefault) + .catch(fail); break; case 'projects:imports:show': - projectImport(); + import('./pages/projects/imports/show') + .then(callDefault) + .catch(fail); break; case 'projects:pipelines:new': case 'projects:pipelines:create': - new NewBranchForm($('.js-new-pipeline-form')); + import('./pages/projects/pipelines/new') + .then(callDefault) + .catch(fail); break; case 'projects:pipelines:builds': case 'projects:pipelines:failures': case 'projects:pipelines:show': - const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; - const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`; - - new Pipelines({ - initTabs: true, - pipelineStatusUrl, - tabsOptions: { - action: controllerAction, - defaultAction: 'pipelines', - parentEl: '.pipelines-tabs', - }, - }); + import('./pages/projects/pipelines/builds') + .then(callDefault) + .catch(fail); break; case 'groups:activity': - new Activities(); + import('./pages/groups/activity') + .then(callDefault) + .catch(fail); break; case 'groups:show': - const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup'); - shortcut_handler = new ShortcutsNavigation(); - new NotificationsForm(); - notificationsDropdown(); - new ProjectsList(); - - if (newGroupChildWrapper) { - new NewGroupChild(newGroupChildWrapper); - } + shortcut_handler = true; break; case 'groups:group_members:index': - memberExpirationDate(); - new Members(); - new UsersSelect(); + import('./pages/groups/group_members/index') + .then(callDefault) + .catch(fail); break; case 'projects:project_members:index': - memberExpirationDate('.js-access-expiration-date-groups'); - groupsSelect(); - memberExpirationDate(); - new Members(); - new UsersSelect(); + import('./pages/projects/project_members') + .then(callDefault) + .catch(fail); break; - case 'groups:new': - case 'admin:groups:new': case 'groups:create': - case 'admin:groups:create': - BindInOut.initAll(); - new Group(); - groupAvatar(); + 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': - groupAvatar(); + import('./pages/admin/groups/edit') + .then(callDefault) + .catch(fail); break; case 'projects:tree:show': - shortcut_handler = new ShortcutsNavigation(); - new TreeView(); - new BlobViewer(); - new NewCommitForm($('.js-create-dir-form')); - $('#tree-slider').waitForImages(function() { - ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); - }); + import('./pages/projects/tree/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:find_file:show': - const findElement = document.querySelector('.js-file-finder'); - const projectFindFile = new ProjectFindFile($(".file-finder-holder"), { - url: findElement.dataset.fileFindUrl, - treeUrl: findElement.dataset.findTreeUrl, - blobUrlTemplate: findElement.dataset.blobUrlTemplate, - }); - new ShortcutsFindFile(projectFindFile); + import('./pages/projects/find_file/show') + .then(callDefault) + .catch(fail); shortcut_handler = true; break; case 'projects:blob:show': - new BlobViewer(); - initBlob(); + import('./pages/projects/blob/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:blame:show': - initBlob(); + 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': - new Labels(); + 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': - if ($('.prioritized-labels').length) { - new LabelManager(); - } - $('.label-subscription').each((i, el) => { - const $el = $(el); - - if ($el.find('.dropdown-group-label').length) { - new GroupLabelSubscription($el); - } else { - new ProjectLabelSubscription($el); - } - }); + 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 @@ -499,183 +413,189 @@ import Activities from './activities'; shortcut_handler = true; break; case 'projects:forks:new': - import(/* webpackChunkName: 'project_fork' */ './project_fork') - .then(fork => fork.default()) - .catch(() => {}); + import('./pages/projects/forks/new') + .then(callDefault) + .catch(fail); break; case 'projects:artifacts:browse': - new ShortcutsNavigation(); - new BuildArtifacts(); + import('./pages/projects/artifacts/browse') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:artifacts:file': - new ShortcutsNavigation(); - new BlobViewer(); + import('./pages/projects/artifacts/file') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'help:index': - VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); + import('./pages/help') + .then(callDefault) + .catch(fail); break; case 'search:show': - new Search(); + import('./pages/search/show') + .then(callDefault) + .catch(fail); break; case 'projects:settings:repository:show': - // Initialize expandable settings panels - initSettingsPanels(); + import('./pages/projects/settings/repository/show') + .then(callDefault) + .catch(fail); break; case 'projects:settings:ci_cd:show': - // Initialize expandable settings panels - initSettingsPanels(); - - const runnerToken = document.querySelector('.js-secret-runner-token'); - if (runnerToken) { - const runnerTokenSecretValue = new SecretValues(runnerToken); - runnerTokenSecretValue.init(); - } + import('./pages/projects/settings/ci_cd/show') + .then(callDefault) + .catch(fail); + break; case 'groups:settings:ci_cd:show': - const secretVariableTable = document.querySelector('.js-secret-variable-table'); - if (secretVariableTable) { - const secretVariableTableValues = new SecretValues(secretVariableTable); - secretVariableTableValues.init(); - } + import('./pages/groups/settings/ci_cd/show') + .then(callDefault) + .catch(fail); break; case 'ci:lints:create': case 'ci:lints:show': - new CILintEditor(); + import('./pages/ci/lints') + .then(callDefault) + .catch(fail); break; case 'users:show': - import('./pages/users/show').then(m => m.default()).catch(fail); + import('./pages/users/show') + .then(callDefault) + .catch(fail); break; case 'admin:conversational_development_index:show': - new UserCallout(); + import('./pages/admin/conversational_development_index/show') + .then(callDefault) + .catch(fail); break; case 'snippets:show': - new LineHighlighter(); - new BlobViewer(); - initNotes(); - new ZenMode(); + import('./pages/snippets/show') + .then(callDefault) + .catch(fail); break; case 'import:fogbugz:new_user_map': - new UsersSelect(); + 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': - new DueDateSelectors(); + import('./pages/admin/impersonation_tokens') + .then(callDefault) + .catch(fail); break; case 'projects:clusters:show': - import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle') - .then(cluster => new cluster.default()) // eslint-disable-line new-cap - .catch((err) => { - Flash(s__('ClusterIntegration|Problem setting up the cluster')); - throw err; - }); + case 'projects:clusters:update': + case 'projects:clusters:destroy': + import('./pages/projects/clusters/show') + .then(callDefault) + .catch(fail); break; case 'projects:clusters:index': - import(/* webpackChunkName: "clusters_index" */ './clusters/clusters_index') - .then(clusterIndex => clusterIndex.default()) - .catch((err) => { - Flash(s__('ClusterIntegration|Problem setting up the clusters list')); - throw err; - }); + 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': - if (!gon.u2f) break; - const u2fAuthenticate = new U2FAuthenticate( - $('#js-authenticate-u2f'), - '#js-login-u2f-form', - gon.u2f, - document.querySelector('#js-login-2fa-device'), - document.querySelector('.js-2fa-form'), - ); - u2fAuthenticate.start(); - // needed in rspec - gl.u2fAuthenticate = u2fAuthenticate; + import('./pages/omniauth_callbacks') + .then(callDefault) + .catch(fail); + break; case 'admin': - initAdmin(); + import('./pages/admin') + .then(callDefault) + .catch(fail); switch (path[1]) { case 'broadcast_messages': - initBroadcastMessagesForm(); + import('./pages/admin/broadcast_messages') + .then(callDefault) + .catch(fail); break; case 'cohorts': - new UsagePing(); + import('./pages/admin/cohorts') + .then(callDefault) + .catch(fail); break; case 'groups': - new UsersSelect(); + switch (path[2]) { + case 'show': + import('./pages/admin/groups/show') + .then(callDefault) + .catch(fail); + break; + } break; case 'projects': - document.querySelectorAll('.js-namespace-select') - .forEach(dropdown => new NamespaceSelect({ dropdown })); + 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': - new Labels(); + import('./pages/admin/labels/edit') + .then(callDefault) + .catch(fail); + break; } case 'abuse_reports': - new AbuseReports(); + import('./pages/admin/abuse_reports') + .then(callDefault) + .catch(fail); break; } break; - case 'dashboard': - case 'root': - new UserCallout(); - break; case 'profiles': - new NotificationsForm(); - notificationsDropdown(); + import('./pages/profiles/index') + .then(callDefault) + .catch(fail); break; case 'projects': - new Project(); - projectAvatar(); + import('./pages/projects') + .then(callDefault) + .catch(fail); + shortcut_handler = true; switch (path[1]) { case 'compare': - initCompareAutocomplete(); - break; - case 'edit': - shortcut_handler = new ShortcutsNavigation(); - new ProjectNew(); - import(/* webpackChunkName: 'project_permissions' */ './projects/permissions') - .then(permissions => permissions.default()) - .catch(() => {}); + import('./pages/projects/compare') + .then(callDefault) + .catch(fail); break; + case 'create': case 'new': - new ProjectNew(); - initProjectVisibilitySelector(); - break; - case 'show': - new Star(); - new ProjectNew(); - notificationsDropdown(); + import('./pages/projects/new') + .then(callDefault) + .catch(fail); break; case 'wikis': - new Wikis(); - shortcut_handler = new ShortcutsWiki(); - new ZenMode(); - new GLForm($('.wiki-form'), true); - break; - case 'snippets': - shortcut_handler = new ShortcutsNavigation(); - if (path[2] === 'show') { - new ZenMode(); - new LineHighlighter(); - new BlobViewer(); - } + import('./pages/projects/wikis') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; - case 'labels': - case 'graphs': - case 'compare': - case 'pipelines': - case 'forks': - case 'milestones': - case 'project_members': - case 'deploy_keys': - case 'builds': - case 'hooks': - case 'services': - case 'protected_branches': - shortcut_handler = new ShortcutsNavigation(); } break; } @@ -685,7 +605,9 @@ import Activities from './activities'; } if (document.querySelector('#peek')) { - new PerformanceBar({ container: '#peek' }); + import('./performance_bar') + .then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap + .catch(fail); } }; @@ -704,8 +626,8 @@ import Activities from './activities'; return Dispatcher; })(); +})(); - $(window).on('load', function() { - new Dispatcher(); - }); -}).call(window); +export default function initDispatcher() { + return new Dispatcher(); +} diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index c84be42649a..ba89e5726fa 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -2,6 +2,9 @@ import Dropzone from 'dropzone'; import _ from 'underscore'; import './preview_markdown'; import csrf from './lib/utils/csrf'; +import axios from './lib/utils/axios_utils'; + +Dropzone.autoDiscover = false; export default function dropzoneInput(form) { const divHover = '<div class="div-dropzone-hover"></div>'; @@ -233,25 +236,21 @@ export default function dropzoneInput(form) { uploadFile = (item, filename) => { const formData = new FormData(); formData.append('file', item, filename); - return $.ajax({ - url: uploadsPath, - type: 'POST', - data: formData, - dataType: 'json', - processData: false, - contentType: false, - headers: csrf.headers, - beforeSend: () => { - showSpinner(); - return closeAlertMessage(); - }, - success: (e, text, response) => { - const md = response.responseJSON.link.markdown; + + showSpinner(); + closeAlertMessage(); + + axios.post(uploadsPath, formData) + .then(({ data }) => { + const md = data.link.markdown; + insertToTextArea(filename, md); - }, - error: response => showError(response.responseJSON.message), - complete: () => closeSpinner(), - }); + closeSpinner(); + }) + .catch((e) => { + showError(e.response.data.message); + closeSpinner(); + }); }; updateAttachingMessage = (files, messageContainer) => { diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index ada985913bb..bd4c58b7cb1 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -1,6 +1,7 @@ /* global dateFormat */ import Pikaday from 'pikaday'; +import axios from './lib/utils/axios_utils'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; class DueDateSelect { @@ -125,37 +126,30 @@ class DueDateSelect { } submitSelectedDate(isDropdown) { - return $.ajax({ - type: 'PUT', - url: this.issueUpdateURL, - data: this.datePayload, - dataType: 'json', - beforeSend: () => { - const selectedDateValue = this.datePayload[this.abilityName].due_date; - const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; + const selectedDateValue = this.datePayload[this.abilityName].due_date; + const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; - this.$loading.removeClass('hidden').fadeIn(); + this.$loading.removeClass('hidden').fadeIn(); - if (isDropdown) { - this.$dropdown.trigger('loading.gl.dropdown'); - this.$selectbox.hide(); - } + if (isDropdown) { + this.$dropdown.trigger('loading.gl.dropdown'); + this.$selectbox.hide(); + } - this.$value.css('display', ''); - this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`); - this.$sidebarValue.html(this.displayedDate); + this.$value.css('display', ''); + this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`); + this.$sidebarValue.html(this.displayedDate); - return selectedDateValue.length ? - $('.js-remove-due-date-holder').removeClass('hidden') : - $('.js-remove-due-date-holder').addClass('hidden'); - }, - }).done(() => { - if (isDropdown) { - this.$dropdown.trigger('loaded.gl.dropdown'); - this.$dropdown.dropdown('toggle'); - } - return this.$loading.fadeOut(); - }); + $('.js-remove-due-date-holder').toggleClass('hidden', selectedDateValue.length); + + return axios.put(this.issueUpdateURL, this.datePayload) + .then(() => { + if (isDropdown) { + this.$dropdown.trigger('loaded.gl.dropdown'); + this.$dropdown.dropdown('toggle'); + } + return this.$loading.fadeOut(); + }); } } diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue index 3236077c3cf..dbee81fa320 100644 --- a/app/assets/javascripts/environments/components/container.vue +++ b/app/assets/javascripts/environments/components/container.vue @@ -4,6 +4,11 @@ import environmentTable from '../components/environments_table.vue'; export default { + components: { + environmentTable, + loadingIcon, + tablePagination, + }, props: { isLoading: { type: Boolean, @@ -26,12 +31,6 @@ required: true, }, }, - components: { - environmentTable, - loadingIcon, - tablePagination, - }, - methods: { onChangePage(page) { this.$emit('onChangePage', page); @@ -47,7 +46,7 @@ label="Loading environments" v-if="isLoading" size="3" - /> + /> <slot name="emptyState"></slot> @@ -59,13 +58,13 @@ :environments="environments" :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" - /> + /> <table-pagination v-if="pagination && pagination.totalPages > 1" :change="onChangePage" - :pageInfo="pagination" - /> + :page-info="pagination" + /> </div> </div> </template> diff --git a/app/assets/javascripts/environments/components/empty_state.vue b/app/assets/javascripts/environments/components/empty_state.vue index 2646f08c8e6..00e63c3467a 100644 --- a/app/assets/javascripts/environments/components/empty_state.vue +++ b/app/assets/javascripts/environments/components/empty_state.vue @@ -1,6 +1,6 @@ <script> export default { - name: 'environmentsEmptyState', + name: 'EnvironmentsEmptyState', props: { newPath: { type: String, @@ -21,21 +21,23 @@ <div class="blank-state-row"> <div class="blank-state-center"> <h2 class="blank-state-title js-blank-state-title"> - {{s__("Environments|You don't have any environments right now.")}} + {{ s__("Environments|You don't have any environments right now.") }} </h2> <p class="blank-state-text"> - {{s__("Environments|Environments are places where code gets deployed, such as staging or production.")}} + {{ s__(`Environments|Environments are places where +code gets deployed, such as staging or production.`) }} <br /> <a :href="helpPath"> - {{s__("Environments|Read more about environments")}} + {{ s__("Environments|Read more about environments") }} </a> </p> <a v-if="canCreateEnvironment" :href="newPath" - class="btn btn-create js-new-environment-button"> - {{s__("Environments|New environment")}} + class="btn btn-create js-new-environment-button" + > + {{ s__("Environments|New environment") }} </a> </div> </div> diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index e7495677e7c..16bd2f5feb3 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -1,55 +1,54 @@ <script> -import playIconSvg from 'icons/_icon_play.svg'; -import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; + import playIconSvg from 'icons/_icon_play.svg'; + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; -export default { - props: { - actions: { - type: Array, - required: false, - default: () => [], + export default { + directives: { + tooltip, }, - }, - directives: { - tooltip, - }, - - components: { - loadingIcon, - }, + components: { + loadingIcon, + }, + props: { + actions: { + type: Array, + required: false, + default: () => [], + }, + }, - data() { - return { - playIconSvg, - isLoading: false, - }; - }, + data() { + return { + playIconSvg, + isLoading: false, + }; + }, - computed: { - title() { - return 'Deploy to...'; + computed: { + title() { + return 'Deploy to...'; + }, }, - }, - methods: { - onClickAction(endpoint) { - this.isLoading = true; + methods: { + onClickAction(endpoint) { + this.isLoading = true; - eventHub.$emit('postAction', endpoint); - }, + eventHub.$emit('postAction', endpoint); + }, - isActionDisabled(action) { - if (action.playable === undefined) { - return false; - } + isActionDisabled(action) { + if (action.playable === undefined) { + return false; + } - return !action.playable; + return !action.playable; + }, }, - }, -}; + }; </script> <template> <div @@ -63,27 +62,33 @@ export default { data-toggle="dropdown" :title="title" :aria-label="title" - :disabled="isLoading"> + :disabled="isLoading" + > <span> <span v-html="playIconSvg"></span> <i class="fa fa-caret-down" - aria-hidden="true"/> + aria-hidden="true" + > + </i> <loading-icon v-if="isLoading" /> </span> </button> <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for="action in actions"> + <li + v-for="(action, i) in actions" + :key="i"> <button type="button" class="js-manual-action-link no-btn btn" @click="onClickAction(action.play_path)" :class="{ disabled: isActionDisabled(action) }" - :disabled="isActionDisabled(action)"> + :disabled="isActionDisabled(action)" + > <span v-html="playIconSvg"></span> <span> - {{action.name}} + {{ action.name }} </span> </button> </li> diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue index 520c3ac8ace..c9a68cface6 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.vue +++ b/app/assets/javascripts/environments/components/environment_external_url.vue @@ -1,28 +1,27 @@ <script> -import tooltip from '../../vue_shared/directives/tooltip'; -import { s__ } from '../../locale'; + import tooltip from '../../vue_shared/directives/tooltip'; + import { s__ } from '../../locale'; -/** - * Renders the external url link in environments table. - */ -export default { - props: { - externalUrl: { - type: String, - required: true, + /** + * Renders the external url link in environments table. + */ + export default { + directives: { + tooltip, + }, + props: { + externalUrl: { + type: String, + required: true, + }, }, - }, - - directives: { - tooltip, - }, - computed: { - title() { - return s__('Environments|Open'); + computed: { + title() { + return s__('Environments|Open'); + }, }, - }, -}; + }; </script> <template> <a @@ -33,9 +32,12 @@ export default { rel="noopener noreferrer nofollow" :title="title" :aria-label="title" - :href="externalUrl"> + :href="externalUrl" + > <i class="fa fa-external-link" - aria-hidden="true" /> + aria-hidden="true" + > + </i> </a> </template> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 2f0e397aa45..a9d554e549e 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -1,423 +1,424 @@ <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 ActionsComponent from './environment_actions.vue'; -import ExternalUrlComponent from './environment_external_url.vue'; -import StopComponent from './environment_stop.vue'; -import RollbackComponent from './environment_rollback.vue'; -import TerminalButtonComponent from './environment_terminal_button.vue'; -import MonitoringButtonComponent from './environment_monitoring.vue'; -import CommitComponent from '../../vue_shared/components/commit.vue'; -import eventHub from '../event_hub'; - -/** - * Envrionment Item Component - * - * Renders a table row for each environment. - */ -const timeagoInstance = new Timeago(); - -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, - }, - - props: { - model: { - type: Object, - required: true, - default: () => ({}), + 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 ActionsComponent from './environment_actions.vue'; + import ExternalUrlComponent from './environment_external_url.vue'; + import StopComponent from './environment_stop.vue'; + import RollbackComponent from './environment_rollback.vue'; + import TerminalButtonComponent from './environment_terminal_button.vue'; + import MonitoringButtonComponent from './environment_monitoring.vue'; + import CommitComponent from '../../vue_shared/components/commit.vue'; + import eventHub from '../event_hub'; + + /** + * Envrionment Item Component + * + * Renders a table row for each environment. + */ + const timeagoInstance = new Timeago(); + + 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, }, - canCreateDeployment: { - type: Boolean, - required: false, - default: false, + props: { + model: { + type: Object, + required: true, + default: () => ({}), + }, + + canCreateDeployment: { + type: Boolean, + required: false, + default: false, + }, + + canReadEnvironment: { + type: Boolean, + required: false, + default: false, + }, }, - canReadEnvironment: { - type: Boolean, - required: false, - default: false, - }, - }, - - computed: { - /** - * Verifies if `last_deployment` key exists in the current Envrionment. - * This key is required to render most of the html - this method works has - * an helper. - * - * @returns {Boolean} - */ - hasLastDeploymentKey() { - if (this.model && - this.model.last_deployment && - !_.isEmpty(this.model.last_deployment)) { - return true; - } - return false; - }, - - /** - * Verifies is the given environment has manual actions. - * Used to verify if we should render them or nor. - * - * @returns {Boolean|Undefined} - */ - hasManualActions() { - return this.model && - this.model.last_deployment && - this.model.last_deployment.manual_actions && - this.model.last_deployment.manual_actions.length > 0; - }, - - /** - * Returns the value of the `stop_action?` key provided in the response. - * - * @returns {Boolean} - */ - hasStopAction() { - return this.model && this.model['stop_action?']; - }, - - /** - * Verifies if the `deployable` key is present in `last_deployment` key. - * Used to verify whether we should or not render the rollback partial. - * - * @returns {Boolean|Undefined} - */ - canRetry() { - return this.model && - this.hasLastDeploymentKey && - this.model.last_deployment && - this.model.last_deployment.deployable; - }, - - /** - * Verifies if the date to be shown is present. - * - * @returns {Boolean|Undefined} - */ - canShowDate() { - return this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable !== undefined; - }, - - /** - * Human readable date. - * - * @returns {String} - */ - createdDate() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.created_at) { - return timeagoInstance.format(this.model.last_deployment.deployable.created_at); - } - return ''; - }, - - /** - * Returns the manual actions with the name parsed. - * - * @returns {Array.<Object>|Undefined} - */ - manualActions() { - if (this.hasManualActions) { - return this.model.last_deployment.manual_actions.map((action) => { - const parsedAction = { - name: humanize(action.name), - play_path: action.play_path, - playable: action.playable, - }; - return parsedAction; - }); - } - return []; - }, - - /** - * Builds the string used in the user image alt attribute. - * - * @returns {String} - */ - userImageAltDescription() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.user && - this.model.last_deployment.user.username) { - return `${this.model.last_deployment.user.username}'s avatar'`; - } - return ''; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {String|Undefined} - */ - commitTag() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.tag) { - return this.model.last_deployment.tag; - } - return undefined; + computed: { + /** + * Verifies if `last_deployment` key exists in the current Envrionment. + * This key is required to render most of the html - this method works has + * an helper. + * + * @returns {Boolean} + */ + hasLastDeploymentKey() { + if (this.model && + this.model.last_deployment && + !_.isEmpty(this.model.last_deployment)) { + return true; + } + return false; + }, + + /** + * Verifies is the given environment has manual actions. + * Used to verify if we should render them or nor. + * + * @returns {Boolean|Undefined} + */ + hasManualActions() { + return this.model && + this.model.last_deployment && + this.model.last_deployment.manual_actions && + this.model.last_deployment.manual_actions.length > 0; + }, + + /** + * Returns the value of the `stop_action?` key provided in the response. + * + * @returns {Boolean} + */ + hasStopAction() { + return this.model && this.model['stop_action?']; + }, + + /** + * Verifies if the `deployable` key is present in `last_deployment` key. + * Used to verify whether we should or not render the rollback partial. + * + * @returns {Boolean|Undefined} + */ + canRetry() { + return this.model && + this.hasLastDeploymentKey && + this.model.last_deployment && + this.model.last_deployment.deployable; + }, + + /** + * Verifies if the date to be shown is present. + * + * @returns {Boolean|Undefined} + */ + canShowDate() { + return this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable !== undefined; + }, + + /** + * Human readable date. + * + * @returns {String} + */ + createdDate() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.created_at) { + return timeagoInstance.format(this.model.last_deployment.deployable.created_at); + } + return ''; + }, + + /** + * Returns the manual actions with the name parsed. + * + * @returns {Array.<Object>|Undefined} + */ + manualActions() { + if (this.hasManualActions) { + return this.model.last_deployment.manual_actions.map((action) => { + const parsedAction = { + name: humanize(action.name), + play_path: action.play_path, + playable: action.playable, + }; + return parsedAction; + }); + } + return []; + }, + + /** + * Builds the string used in the user image alt attribute. + * + * @returns {String} + */ + userImageAltDescription() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.user && + this.model.last_deployment.user.username) { + return `${this.model.last_deployment.user.username}'s avatar'`; + } + return ''; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.tag) { + return this.model.last_deployment.tag; + } + return undefined; + }, + + /** + * If provided, returns the commit ref. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.ref) { + return this.model.last_deployment.ref; + } + return undefined; + }, + + /** + * If provided, returns the commit url. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.commit_path) { + return this.model.last_deployment.commit.commit_path; + } + return undefined; + }, + + /** + * If provided, returns the commit short sha. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.short_id) { + return this.model.last_deployment.commit.short_id; + } + return undefined; + }, + + /** + * If provided, returns the commit title. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.title) { + return this.model.last_deployment.commit.title; + } + return undefined; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {Object|Undefined} + */ + commitAuthor() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.author) { + return this.model.last_deployment.commit.author; + } + + return undefined; + }, + + /** + * Verifies if the `retry_path` key is present and returns its value. + * + * @returns {String|Undefined} + */ + retryUrl() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.retry_path) { + return this.model.last_deployment.deployable.retry_path; + } + return undefined; + }, + + /** + * Verifies if the `last?` key is present and returns its value. + * + * @returns {Boolean|Undefined} + */ + isLastDeployment() { + return this.model && this.model.last_deployment && + this.model.last_deployment['last?']; + }, + + /** + * Builds the name of the builds needed to display both the name and the id. + * + * @returns {String} + */ + buildName() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable) { + const deployable = this.model.last_deployment.deployable; + return `${deployable.name} #${deployable.id}`; + } + return ''; + }, + + /** + * Builds the needed string to show the internal id. + * + * @returns {String} + */ + deploymentInternalId() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.iid) { + return `#${this.model.last_deployment.iid}`; + } + return ''; + }, + + /** + * Verifies if the user object is present under last_deployment object. + * + * @returns {Boolean} + */ + deploymentHasUser() { + return this.model && + !_.isEmpty(this.model.last_deployment) && + !_.isEmpty(this.model.last_deployment.user); + }, + + /** + * Returns the user object nested with the last_deployment object. + * Used to render the template. + * + * @returns {Object} + */ + deploymentUser() { + if (this.model && + !_.isEmpty(this.model.last_deployment) && + !_.isEmpty(this.model.last_deployment.user)) { + return this.model.last_deployment.user; + } + return {}; + }, + + /** + * Verifies if the build name column should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderBuildName() { + return !this.model.isFolder && + !_.isEmpty(this.model.last_deployment) && + !_.isEmpty(this.model.last_deployment.deployable); + }, + + /** + * Verifies the presence of all the keys needed to render the buil_path. + * + * @return {String} + */ + buildPath() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.build_path) { + return this.model.last_deployment.deployable.build_path; + } + + return ''; + }, + + /** + * Verifies the presence of all the keys needed to render the external_url. + * + * @return {String} + */ + externalURL() { + if (this.model && this.model.external_url) { + return this.model.external_url; + } + + return ''; + }, + + /** + * Verifies if deplyment internal ID should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderDeploymentID() { + return !this.model.isFolder && + !_.isEmpty(this.model.last_deployment) && + this.model.last_deployment.iid !== undefined; + }, + + environmentPath() { + if (this.model && this.model.environment_path) { + return this.model.environment_path; + } + + return ''; + }, + + monitoringUrl() { + if (this.model && this.model.metrics_path) { + return this.model.metrics_path; + } + + return ''; + }, + + displayEnvironmentActions() { + return this.hasManualActions || + this.externalURL || + this.monitoringUrl || + this.hasStopAction || + this.canRetry; + }, }, - /** - * If provided, returns the commit ref. - * - * @returns {Object|Undefined} - */ - commitRef() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.ref) { - return this.model.last_deployment.ref; - } - return undefined; + methods: { + onClickFolder() { + eventHub.$emit('toggleFolder', this.model); + }, }, - - /** - * If provided, returns the commit url. - * - * @returns {String|Undefined} - */ - commitUrl() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.commit_path) { - return this.model.last_deployment.commit.commit_path; - } - return undefined; - }, - - /** - * If provided, returns the commit short sha. - * - * @returns {String|Undefined} - */ - commitShortSha() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.short_id) { - return this.model.last_deployment.commit.short_id; - } - return undefined; - }, - - /** - * If provided, returns the commit title. - * - * @returns {String|Undefined} - */ - commitTitle() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.title) { - return this.model.last_deployment.commit.title; - } - return undefined; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {Object|Undefined} - */ - commitAuthor() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.author) { - return this.model.last_deployment.commit.author; - } - - return undefined; - }, - - /** - * Verifies if the `retry_path` key is present and returns its value. - * - * @returns {String|Undefined} - */ - retryUrl() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.retry_path) { - return this.model.last_deployment.deployable.retry_path; - } - return undefined; - }, - - /** - * Verifies if the `last?` key is present and returns its value. - * - * @returns {Boolean|Undefined} - */ - isLastDeployment() { - return this.model && this.model.last_deployment && - this.model.last_deployment['last?']; - }, - - /** - * Builds the name of the builds needed to display both the name and the id. - * - * @returns {String} - */ - buildName() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable) { - return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`; - } - return ''; - }, - - /** - * Builds the needed string to show the internal id. - * - * @returns {String} - */ - deploymentInternalId() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.iid) { - return `#${this.model.last_deployment.iid}`; - } - return ''; - }, - - /** - * Verifies if the user object is present under last_deployment object. - * - * @returns {Boolean} - */ - deploymentHasUser() { - return this.model && - !_.isEmpty(this.model.last_deployment) && - !_.isEmpty(this.model.last_deployment.user); - }, - - /** - * Returns the user object nested with the last_deployment object. - * Used to render the template. - * - * @returns {Object} - */ - deploymentUser() { - if (this.model && - !_.isEmpty(this.model.last_deployment) && - !_.isEmpty(this.model.last_deployment.user)) { - return this.model.last_deployment.user; - } - return {}; - }, - - /** - * Verifies if the build name column should be rendered by verifing - * if all the information needed is present - * and if the environment is not a folder. - * - * @returns {Boolean} - */ - shouldRenderBuildName() { - return !this.model.isFolder && - !_.isEmpty(this.model.last_deployment) && - !_.isEmpty(this.model.last_deployment.deployable); - }, - - /** - * Verifies the presence of all the keys needed to render the buil_path. - * - * @return {String} - */ - buildPath() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.build_path) { - return this.model.last_deployment.deployable.build_path; - } - - return ''; - }, - - /** - * Verifies the presence of all the keys needed to render the external_url. - * - * @return {String} - */ - externalURL() { - if (this.model && this.model.external_url) { - return this.model.external_url; - } - - return ''; - }, - - /** - * Verifies if deplyment internal ID should be rendered by verifing - * if all the information needed is present - * and if the environment is not a folder. - * - * @returns {Boolean} - */ - shouldRenderDeploymentID() { - return !this.model.isFolder && - !_.isEmpty(this.model.last_deployment) && - this.model.last_deployment.iid !== undefined; - }, - - environmentPath() { - if (this.model && this.model.environment_path) { - return this.model.environment_path; - } - - return ''; - }, - - monitoringUrl() { - if (this.model && this.model.metrics_path) { - return this.model.metrics_path; - } - - return ''; - }, - - displayEnvironmentActions() { - return this.hasManualActions || - this.externalURL || - this.monitoringUrl || - this.hasStopAction || - this.canRetry; - }, - }, - - methods: { - onClickFolder() { - eventHub.$emit('toggleFolder', this.model); - }, - }, -}; + }; </script> <template> <div @@ -427,18 +428,22 @@ export default { 'folder-row': model.isFolder, }" role="row"> - <div class="table-section section-10" role="gridcell"> + <div + class="table-section section-10" + role="gridcell" + > <div v-if="!model.isFolder" class="table-mobile-header" - role="rowheader"> - {{s__("Environments|Environment")}} + role="rowheader" + > + {{ s__("Environments|Environment") }} </div> <a 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">{{ model.name }}</span> </a> <span v-else @@ -450,32 +455,40 @@ export default { <i v-show="model.isOpen" class="fa fa-caret-down" - aria-hidden="true" /> + aria-hidden="true" + > + </i> <i v-show="!model.isOpen" class="fa fa-caret-right" - aria-hidden="true"/> + aria-hidden="true" + > + </i> </span> <span class="folder-icon"> <i class="fa fa-folder" - aria-hidden="true" /> + aria-hidden="true"> + </i> </span> <span> - {{model.folderName}} + {{ model.folderName }} </span> <span class="badge"> - {{model.size}} + {{ model.size }} </span> </span> </div> - <div class="table-section section-10 deployment-column hidden-xs hidden-sm" role="gridcell"> + <div + class="table-section section-10 deployment-column hidden-xs hidden-sm" + role="gridcell" + > <span v-if="shouldRenderDeploymentID"> - {{deploymentInternalId}} + {{ deploymentInternalId }} </span> <span v-if="!model.isFolder && deploymentHasUser"> @@ -490,22 +503,29 @@ export default { </span> </div> - <div class="table-section section-15 hidden-xs hidden-sm" role="gridcell"> + <div + class="table-section section-15 hidden-xs hidden-sm" + role="gridcell" + > <a v-if="shouldRenderBuildName" class="build-link flex-truncate-parent" - :href="buildPath"> - <span class="flex-truncate-child">{{buildName}}</span> + :href="buildPath" + > + <span class="flex-truncate-child">{{ buildName }}</span> </a> </div> <div v-if="!model.isFolder" - class="table-section section-25" role="gridcell"> + class="table-section section-25" + role="gridcell" + > <div role="rowheader" - class="table-mobile-header"> - {{s__("Environments|Commit")}} + class="table-mobile-header" + > + {{ s__("Environments|Commit") }} </div> <div v-if="hasLastDeploymentKey" @@ -521,22 +541,24 @@ export default { <div v-if="!hasLastDeploymentKey" class="commit-title table-mobile-content"> - {{s__("Environments|No deployments yet")}} + {{ s__("Environments|No deployments yet") }} </div> </div> <div v-if="!model.isFolder" - class="table-section section-10" role="gridcell"> + class="table-section section-10" + role="gridcell" + > <div role="rowheader" class="table-mobile-header"> - {{s__("Environments|Updated")}} + {{ s__("Environments|Updated") }} </div> <span v-if="canShowDate" class="environment-created-date-timeago table-mobile-content"> - {{createdDate}} + {{ createdDate }} </span> </div> @@ -552,33 +574,33 @@ export default { <actions-component v-if="hasManualActions && canCreateDeployment" :actions="manualActions" - /> + /> <external-url-component v-if="externalURL && canReadEnvironment" :external-url="externalURL" - /> + /> <monitoring-button-component v-if="monitoringUrl && canReadEnvironment" :monitoring-url="monitoringUrl" - /> + /> <terminal-button-component v-if="model && model.terminal_path" :terminal-path="model.terminal_path" - /> + /> <stop-component v-if="hasStopAction && canCreateDeployment" :stop-url="model.stop_path" - /> + /> <rollback-component v-if="canRetry && canCreateDeployment" :is-last-deployment="isLastDeployment" :retry-url="retryUrl" - /> + /> </div> </div> </div> diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index b45af1a5ebc..081537cf218 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -1,27 +1,27 @@ <script> -/** - * Renders the Monitoring (Metrics) link in environments table. - */ -import tooltip from '../../vue_shared/directives/tooltip'; + /** + * Renders the Monitoring (Metrics) link in environments table. + */ + import tooltip from '../../vue_shared/directives/tooltip'; -export default { - props: { - monitoringUrl: { - type: String, - required: true, + export default { + directives: { + tooltip, }, - }, - directives: { - tooltip, - }, + props: { + monitoringUrl: { + type: String, + required: true, + }, + }, - computed: { - title() { - return 'Monitoring'; + computed: { + title() { + return 'Monitoring'; + }, }, - }, -}; + }; </script> <template> <a @@ -31,10 +31,12 @@ export default { rel="noopener noreferrer nofollow" :href="monitoringUrl" :title="title" - :aria-label="title"> + :aria-label="title" + > <i class="fa fa-area-chart" aria-hidden="true" - /> + > + </i> </a> </template> diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue index 92a596bfd33..605a88e997e 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.vue +++ b/app/assets/javascripts/environments/components/environment_rollback.vue @@ -1,57 +1,58 @@ <script> -/** - * Renders Rollback or Re deploy button in environments table depending - * of the provided property `isLastDeployment`. - * - * Makes a post request when the button is clicked. - */ -import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - -export default { - props: { - retryUrl: { - type: String, - default: '', + /** + * Renders Rollback or Re deploy button in environments table depending + * of the provided property `isLastDeployment`. + * + * Makes a post request when the button is clicked. + */ + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + + export default { + components: { + loadingIcon, }, - isLastDeployment: { - type: Boolean, - default: true, - }, - }, + props: { + retryUrl: { + type: String, + default: '', + }, - components: { - loadingIcon, - }, + isLastDeployment: { + type: Boolean, + default: true, + }, + }, - data() { - return { - isLoading: false, - }; - }, + data() { + return { + isLoading: false, + }; + }, - methods: { - onClick() { - this.isLoading = true; + methods: { + onClick() { + this.isLoading = true; - eventHub.$emit('postAction', this.retryUrl); + eventHub.$emit('postAction', this.retryUrl); + }, }, - }, -}; + }; </script> <template> <button type="button" class="btn hidden-xs hidden-sm" @click="onClick" - :disabled="isLoading"> + :disabled="isLoading" + > <span v-if="isLastDeployment"> - {{s__("Environments|Re-deploy")}} + {{ s__("Environments|Re-deploy") }} </span> <span v-else> - {{s__("Environments|Rollback")}} + {{ s__("Environments|Rollback") }} </span> <loading-icon v-if="isLoading" /> diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue index 85f11d2071b..1eef17bf1fe 100644 --- a/app/assets/javascripts/environments/components/environment_stop.vue +++ b/app/assets/javascripts/environments/components/environment_stop.vue @@ -1,53 +1,53 @@ <script> -/** - * Renders the stop "button" that allows stop an environment. - * Used in environments table. - */ -import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; + /** + * Renders the stop "button" that allows stop an environment. + * Used in environments table. + */ + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; -export default { - props: { - stopUrl: { - type: String, - default: '', + export default { + components: { + loadingIcon, }, - }, - directives: { - tooltip, - }, + directives: { + tooltip, + }, - data() { - return { - isLoading: false, - }; - }, + props: { + stopUrl: { + type: String, + default: '', + }, + }, - components: { - loadingIcon, - }, + data() { + return { + isLoading: false, + }; + }, - computed: { - title() { - return 'Stop'; + computed: { + title() { + return 'Stop'; + }, }, - }, - methods: { - onClick() { - // eslint-disable-next-line no-alert - if (confirm('Are you sure you want to stop this environment?')) { - this.isLoading = true; + methods: { + onClick() { + // eslint-disable-next-line no-alert + if (confirm('Are you sure you want to stop this environment?')) { + this.isLoading = true; - $(this.$el).tooltip('destroy'); + $(this.$el).tooltip('destroy'); - eventHub.$emit('postAction', this.stopUrl); - } + eventHub.$emit('postAction', this.stopUrl); + } + }, }, - }, -}; + }; </script> <template> <button @@ -58,10 +58,13 @@ export default { @click="onClick" :disabled="isLoading" :title="title" - :aria-label="title"> + :aria-label="title" + > <i class="fa fa-stop stop-env-icon" - aria-hidden="true" /> + aria-hidden="true" + > + </i> <loading-icon v-if="isLoading" /> </button> </template> diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue index 2037bf618e3..407d5333c0e 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.vue +++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue @@ -1,36 +1,36 @@ <script> -/** - * Renders a terminal button to open a web terminal. - * Used in environments table. - */ -import terminalIconSvg from 'icons/_icon_terminal.svg'; -import tooltip from '../../vue_shared/directives/tooltip'; + /** + * Renders a terminal button to open a web terminal. + * Used in environments table. + */ + import terminalIconSvg from 'icons/_icon_terminal.svg'; + import tooltip from '../../vue_shared/directives/tooltip'; -export default { - props: { - terminalPath: { - type: String, - required: false, - default: '', + export default { + directives: { + tooltip, }, - }, - directives: { - tooltip, - }, + props: { + terminalPath: { + type: String, + required: false, + default: '', + }, + }, - data() { - return { - terminalIconSvg, - }; - }, + data() { + return { + terminalIconSvg, + }; + }, - computed: { - title() { - return 'Terminal'; + computed: { + title() { + return 'Terminal'; + }, }, - }, -}; + }; </script> <template> <a @@ -40,6 +40,7 @@ export default { :title="title" :aria-label="title" :href="terminalPath" - v-html="terminalIconSvg"> + v-html="terminalIconSvg" + > </a> </template> diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index 2592909734f..c0be72f7401 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -7,6 +7,15 @@ import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; export default { + components: { + emptyState, + }, + + mixins: [ + CIPaginationMixin, + environmentsMixin, + ], + props: { endpoint: { type: String, @@ -37,14 +46,6 @@ required: true, }, }, - components: { - emptyState, - }, - - mixins: [ - CIPaginationMixin, - environmentsMixin, - ], created() { eventHub.$on('toggleFolder', this.toggleFolder); @@ -95,15 +96,17 @@ :tabs="tabs" @onChangeTab="onChangeTab" scope="environments" - /> + /> <div v-if="canCreateEnvironment && !isLoading" - class="nav-controls"> + class="nav-controls" + > <a :href="newEnvironmentPath" - class="btn btn-create"> - {{s__("Environments|New environment")}} + class="btn btn-create" + > + {{ s__("Environments|New environment") }} </a> </div> </div> @@ -116,13 +119,13 @@ :can-read-environment="canReadEnvironment" @onChangePage="onChangePage" > - <empty-state + <empty-state slot="emptyState" v-if="!isLoading && state.environments.length === 0" :new-path="newEnvironmentPath" :help-path="helpPagePath" :can-create-environment="canCreateEnvironment" - /> + /> </container> </div> </template> diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index c04da4b81b7..b4eca47957e 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -30,63 +30,96 @@ export default { default: false, }, }, - methods: { folderUrl(model) { return `${window.location.pathname}/folders/${model.folderName}`; }, + shouldRenderFolderContent(env) { + return env.isFolder && + env.isOpen && + env.children && + env.children.length > 0; + }, }, }; </script> <template> - <div class="ci-table" role="grid"> - <div class="gl-responsive-table-row table-row-header" role="row"> - <div class="table-section section-10 environments-name" role="columnheader"> - {{s__("Environments|Environment")}} + <div + class="ci-table" + role="grid" + > + <div + class="gl-responsive-table-row table-row-header" + role="row" + > + <div + class="table-section section-10 environments-name" + role="columnheader" + > + {{ s__("Environments|Environment") }} </div> - <div class="table-section section-10 environments-deploy" role="columnheader"> - {{s__("Environments|Deployment")}} + <div + class="table-section section-10 environments-deploy" + role="columnheader" + > + {{ s__("Environments|Deployment") }} </div> - <div class="table-section section-15 environments-build" role="columnheader"> - {{s__("Environments|Job")}} + <div + class="table-section section-15 environments-build" + role="columnheader" + > + {{ s__("Environments|Job") }} </div> - <div class="table-section section-25 environments-commit" role="columnheader"> - {{s__("Environments|Commit")}} + <div + class="table-section section-25 environments-commit" + role="columnheader" + > + {{ s__("Environments|Commit") }} </div> - <div class="table-section section-10 environments-date" role="columnheader"> - {{s__("Environments|Updated")}} + <div + class="table-section section-10 environments-date" + role="columnheader" + > + {{ s__("Environments|Updated") }} </div> </div> <template - v-for="model in environments" - v-bind:model="model"> + v-for="(model, i) in environments" + :model="model"> <div is="environment-item" :model="model" :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" - /> + :key="i" + /> - <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0"> - <div v-if="model.isLoadingFolderContent"> + <template + v-if="shouldRenderFolderContent(model)" + > + <div + v-if="model.isLoadingFolderContent" + :key="`loading-item-${i}`"> <loading-icon size="2" /> </div> <template v-else> <div is="environment-item" - v-for="children in model.children" + v-for="(children, index) in model.children" :model="children" :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" - /> + :key="`env-item-${i}-${index}`" + /> - <div> + <div :key="`sub-div-${i}`"> <div class="text-center prepend-top-10"> <a :href="folderUrl(model)" - class="btn btn-default"> - {{s__("Environments|Show all")}} + class="btn btn-default" + > + {{ s__("Environments|Show all") }} </a> </div> </div> diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 27418bad01a..5ef5e347387 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -3,6 +3,10 @@ import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; export default { + mixins: [ + environmentsMixin, + CIPaginationMixin, + ], props: { endpoint: { type: String, @@ -25,10 +29,6 @@ required: true, }, }, - mixins: [ - environmentsMixin, - CIPaginationMixin, - ], methods: { successCallback(resp) { this.saveData(resp); @@ -40,17 +40,18 @@ <div :class="cssContainerClass"> <div class="top-area" - v-if="!isLoading"> + v-if="!isLoading" + > <h4 class="js-folder-name environments-folder-name"> - {{s__("Environments|Environments")}} / <b>{{folderName}}</b> + {{ s__("Environments|Environments") }} / <b>{{ folderName }}</b> </h4> <tabs :tabs="tabs" @onChangeTab="onChangeTab" scope="environments" - /> + /> </div> <container diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js index 7219b076721..34d18d55120 100644 --- a/app/assets/javascripts/environments/mixins/environments_mixin.js +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -1,7 +1,7 @@ /** * Common code between environmets app and folder view */ - +import _ from 'underscore'; import Visibility from 'visibilityjs'; import Poll from '../../lib/utils/poll'; import { diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index 9e91f72b2ea..a10f027de53 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import axios from './lib/utils/axios_utils'; /** * Makes search request for content when user types a value in the search input. @@ -54,32 +55,26 @@ export default class FilterableList { this.listFilterElement.removeEventListener('input', this.debounceFilter); } - filterResults(queryData) { + filterResults(params) { if (this.isBusy) { return false; } $(this.listHolderElement).fadeTo(250, 0.5); - return $.ajax({ - url: this.getFilterEndpoint(), - data: queryData, - type: 'GET', - dataType: 'json', - context: this, - complete: this.onFilterComplete, - beforeSend: () => { - this.isBusy = true; - }, - success: (response, textStatus, xhr) => { - this.onFilterSuccess(response, xhr, queryData); - }, - }); + this.isBusy = true; + + return axios.get(this.getFilterEndpoint(), { + params, + }).then((res) => { + this.onFilterSuccess(res, params); + this.onFilterComplete(); + }).catch(() => this.onFilterComplete()); } - onFilterSuccess(response, xhr, queryData) { - if (response.html) { - this.listHolderElement.innerHTML = response.html; + onFilterSuccess(response, queryData) { + if (response.data.html) { + this.listHolderElement.innerHTML = response.data.html; } // Change url so if user reload a page - search results are saved 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 46c80dfd45e..ff046aa286a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import DropLab from '~/droplab/drop_lab'; import FilteredSearchContainer from './container'; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 2ba85c7da97..58ed0012f01 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import { visitUrl } from '../lib/utils/url_utility'; import Flash from '../flash'; import FilteredSearchContainer from './container'; @@ -127,7 +128,7 @@ class FilteredSearchManager { this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this); this.checkForEnterWrapper = this.checkForEnter.bind(this); this.onClearSearchWrapper = this.onClearSearch.bind(this); - this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); + this.checkForBackspaceWrapper = this.checkForBackspace.call(this); this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this); this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this); this.editTokenWrapper = this.editToken.bind(this); @@ -180,22 +181,34 @@ class FilteredSearchManager { this.unbindStateEvents(); } - checkForBackspace(e) { - // 8 = Backspace Key - // 46 = Delete Key - if (e.keyCode === 8 || e.keyCode === 46) { - const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + checkForBackspace() { + let backspaceCount = 0; + + // closure for keeping track of the number of backspace keystrokes + return (e) => { + // 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 canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue); + + if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) { + backspaceCount += 1; + + if (backspaceCount === 2) { + backspaceCount = 0; + this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + } + } - const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken); - const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue); - if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) { - this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); - gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + // Reposition dropdown so that it is aligned with cursor + this.dropdownManager.updateCurrentDropdownOffset(); + } else { + backspaceCount = 0; } - - // Reposition dropdown so that it is aligned with cursor - this.dropdownManager.updateCurrentDropdownOffset(); - } + }; } checkForEnter(e) { 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 6139e81fe6d..2e859d2de3a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import AjaxCache from '../lib/utils/ajax_cache'; import Flash from '../flash'; import FilteredSearchContainer from './container'; diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js index 27e49d4fb96..c99ed63c4af 100644 --- a/app/assets/javascripts/filtered_search/recent_searches_root.js +++ b/app/assets/javascripts/filtered_search/recent_searches_root.js @@ -32,6 +32,9 @@ class RecentSearchesRoot { const state = this.store.state; this.vm = new Vue({ el: this.wrapperElement, + components: { + 'recent-searches-dropdown-content': RecentSearchesDropdownContent, + }, data() { return state; }, template: ` <recent-searches-dropdown-content @@ -40,9 +43,6 @@ class RecentSearchesRoot { :allowed-keys="allowedKeys" /> `, - components: { - 'recent-searches-dropdown-content': RecentSearchesDropdownContent, - }, }); } diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index 44deab9288e..a0af2875ab5 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -10,6 +10,7 @@ const hideFlash = (flashEl, fadeTransition = true) => { flashEl.addEventListener('transitionend', () => { flashEl.remove(); + if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown'); }, { once: true, passive: true, @@ -64,6 +65,7 @@ const createFlash = function createFlash( parent = document, actionConfig = null, fadeTransition = true, + addBodyClass = false, ) { const flashContainer = parent.querySelector('.flash-container'); @@ -86,6 +88,8 @@ const createFlash = function createFlash( flashContainer.style.display = 'block'; + if (addBodyClass) document.body.classList.add('flash-shown'); + return flashContainer; }; diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index abb04d77f8f..8b4f3b05ee7 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -118,14 +118,14 @@ export const showSubLevelItems = (el) => { moveSubItemsToPosition(el, subItems); }; -export const mouseEnterTopItems = (el) => { +export const mouseEnterTopItems = (el, timeout = getHideSubItemsInterval()) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => { if (currentOpenMenu) hideMenu(currentOpenMenu); showSubLevelItems(el); - }, getHideSubItemsInterval()); + }, timeout); }; export const mouseLeaveTopItem = (el) => { diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index d918d80df8d..57a1fa107e5 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -57,12 +57,12 @@ class GfmAutoComplete { displayTpl(value) { if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template; // eslint-disable-next-line no-template-curly-in-string - let tpl = '<li>/${name}'; + let tpl = '<li><span class="name">/${name}</span>'; if (value.aliases.length > 0) { - tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>'; + tpl += ' <small class="aliases">(or /<%- aliases.join(", /") %>)</small>'; } if (value.params.length > 0) { - tpl += ' <small><%- params.join(" ") %></small>'; + tpl += ' <small class="params"><%- params.join(" ") %></small>'; } if (value.description !== '') { tpl += '<small class="description"><i><%- description %></i></small>'; @@ -461,7 +461,7 @@ class GfmAutoComplete { const accentAChar = decodeURI('%C3%80'); const accentYChar = decodeURI('%C3%BF'); - const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi'); + const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi'); return regexp.exec(targetSubtext); } diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 64f258aed64..15df7a7f989 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -2,6 +2,7 @@ /* global fuzzaldrinPlus */ import _ from 'underscore'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import axios from './lib/utils/axios_utils'; import { visitUrl } from './lib/utils/url_utility'; import { isObject } from './lib/utils/type_utility'; @@ -212,25 +213,17 @@ GitLabDropdownRemote = (function() { }; GitLabDropdownRemote.prototype.fetchData = function() { - return $.ajax({ - url: this.dataEndpoint, - dataType: this.options.dataType, - beforeSend: (function(_this) { - return function() { - if (_this.options.beforeSend) { - return _this.options.beforeSend(); - } - }; - })(this), - success: (function(_this) { - return function(data) { - if (_this.options.success) { - return _this.options.success(data); - } - }; - })(this) - }); - // Fetch the data through ajax if the data is a string + if (this.options.beforeSend) { + this.options.beforeSend(); + } + + // Fetch the data through ajax if the data is a string + return axios.get(this.dataEndpoint) + .then(({ data }) => { + if (this.options.success) { + return this.options.success(data); + } + }); }; return GitLabDropdownRemote; diff --git a/app/assets/javascripts/graphs/graphs_show.js b/app/assets/javascripts/graphs/graphs_show.js index 36bad6db3e1..b670e907a5c 100644 --- a/app/assets/javascripts/graphs/graphs_show.js +++ b/app/assets/javascripts/graphs/graphs_show.js @@ -1,11 +1,13 @@ +import flash from '../flash'; +import { __ } from '../locale'; +import axios from '../lib/utils/axios_utils'; import ContributorsStatGraph from './stat_graph_contributors'; document.addEventListener('DOMContentLoaded', () => { - $.ajax({ - type: 'GET', - url: document.querySelector('.js-graphs-show').dataset.projectGraphPath, - dataType: 'json', - success(data) { + const url = document.querySelector('.js-graphs-show').dataset.projectGraphPath; + + axios.get(url) + .then(({ data }) => { const graph = new ContributorsStatGraph(); graph.init(data); @@ -16,6 +18,6 @@ document.addEventListener('DOMContentLoaded', () => { $('.stat-graph').fadeIn(); $('.loading-graph').hide(); - }, - }); + }) + .catch(() => flash(__('Error fetching contributors data.'))); }); diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js index befaebb635e..df9429b1e02 100644 --- a/app/assets/javascripts/group_label_subscription.js +++ b/app/assets/javascripts/group_label_subscription.js @@ -1,3 +1,7 @@ +import axios from './lib/utils/axios_utils'; +import flash from './flash'; +import { __ } from './locale'; + export default class GroupLabelSubscription { constructor(container) { const $container = $(container); @@ -13,14 +17,12 @@ export default class GroupLabelSubscription { event.preventDefault(); const url = this.$unsubscribeButtons.attr('data-url'); - - $.ajax({ - type: 'POST', - url, - }).done(() => { - this.toggleSubscriptionButtons(); - this.$unsubscribeButtons.removeAttr('data-url'); - }); + axios.post(url) + .then(() => { + this.toggleSubscriptionButtons(); + this.$unsubscribeButtons.removeAttr('data-url'); + }) + .catch(() => flash(__('There was an error when unsubscribing from this label.'))); } subscribe(event) { @@ -31,12 +33,9 @@ export default class GroupLabelSubscription { this.$unsubscribeButtons.attr('data-url', url); - $.ajax({ - type: 'POST', - url, - }).done(() => { - this.toggleSubscriptionButtons(); - }); + axios.post(url) + .then(() => this.toggleSubscriptionButtons()) + .catch(() => flash(__('There was an error when subscribing to this label.'))); } toggleSubscriptionButtons() { diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index 241e026b84c..e035ba462db 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -1,16 +1,20 @@ <script> /* global Flash */ +import { s__ } from '~/locale'; +import loadingIcon from '~/vue_shared/components/loading_icon.vue'; +import modal from '~/vue_shared/components/modal.vue'; +import { getParameterByName } from '~/lib/utils/common_utils'; +import { mergeUrlParams } from '~/lib/utils/url_utility'; + import eventHub from '../event_hub'; -import { getParameterByName } from '../../lib/utils/common_utils'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import { COMMON_STR } from '../constants'; -import { mergeUrlParams } from '../../lib/utils/url_utility'; import groupsComponent from './groups.vue'; export default { components: { loadingIcon, + modal, groupsComponent, }, props: { @@ -32,6 +36,10 @@ export default { isLoading: true, isSearchEmpty: false, searchEmptyMessage: '', + showModal: false, + groupLeaveConfirmationMessage: '', + targetGroup: null, + targetParentGroup: null, }; }, computed: { @@ -42,6 +50,26 @@ export default { return this.store.getPaginationInfo(); }, }, + created() { + this.searchEmptyMessage = this.hideProjects ? + COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY; + + eventHub.$on('fetchPage', this.fetchPage); + eventHub.$on('toggleChildren', this.toggleChildren); + eventHub.$on('showLeaveGroupModal', this.showLeaveGroupModal); + eventHub.$on('updatePagination', this.updatePagination); + eventHub.$on('updateGroups', this.updateGroups); + }, + mounted() { + this.fetchAllGroups(); + }, + beforeDestroy() { + eventHub.$off('fetchPage', this.fetchPage); + eventHub.$off('toggleChildren', this.toggleChildren); + eventHub.$off('showLeaveGroupModal', this.showLeaveGroupModal); + eventHub.$off('updatePagination', this.updatePagination); + eventHub.$off('updateGroups', this.updateGroups); + }, methods: { fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) { return this.service.getGroups(parentId, page, filterGroupsBy, sortBy, archived) @@ -121,14 +149,23 @@ export default { parentGroup.isOpen = false; } }, - leaveGroup(group, parentGroup) { - const targetGroup = group; - targetGroup.isBeingRemoved = true; - this.service.leaveGroup(targetGroup.leavePath) + showLeaveGroupModal(group, parentGroup) { + this.targetGroup = group; + this.targetParentGroup = parentGroup; + this.showModal = true; + this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`); + }, + hideLeaveGroupModal() { + this.showModal = false; + }, + leaveGroup() { + this.showModal = false; + this.targetGroup.isBeingRemoved = true; + this.service.leaveGroup(this.targetGroup.leavePath) .then(res => res.json()) .then((res) => { $.scrollTo(0); - this.store.removeGroup(targetGroup, parentGroup); + this.store.removeGroup(this.targetGroup, this.targetParentGroup); Flash(res.notice, 'notice'); }) .catch((err) => { @@ -137,7 +174,7 @@ export default { message = COMMON_STR.LEAVE_FORBIDDEN; } Flash(message); - targetGroup.isBeingRemoved = false; + this.targetGroup.isBeingRemoved = false; }); }, updatePagination(headers) { @@ -152,26 +189,6 @@ export default { } }, }, - created() { - this.searchEmptyMessage = this.hideProjects ? - COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY; - - eventHub.$on('fetchPage', this.fetchPage); - eventHub.$on('toggleChildren', this.toggleChildren); - eventHub.$on('leaveGroup', this.leaveGroup); - eventHub.$on('updatePagination', this.updatePagination); - eventHub.$on('updateGroups', this.updateGroups); - }, - mounted() { - this.fetchAllGroups(); - }, - beforeDestroy() { - eventHub.$off('fetchPage', this.fetchPage); - eventHub.$off('toggleChildren', this.toggleChildren); - eventHub.$off('leaveGroup', this.leaveGroup); - eventHub.$off('updatePagination', this.updatePagination); - eventHub.$off('updateGroups', this.updateGroups); - }, }; </script> @@ -190,5 +207,14 @@ export default { :search-empty-message="searchEmptyMessage" :page-info="pageInfo" /> + <modal + v-show="showModal" + :primary-button-label="__('Leave')" + kind="warning" + :title="__('Are you sure?')" + :text="groupLeaveConfirmationMessage" + @cancel="hideLeaveGroupModal" + @submit="leaveGroup" + /> </div> </template> diff --git a/app/assets/javascripts/groups/components/group_folder.vue b/app/assets/javascripts/groups/components/group_folder.vue index e60221fa08d..647c9d0046d 100644 --- a/app/assets/javascripts/groups/components/group_folder.vue +++ b/app/assets/javascripts/groups/components/group_folder.vue @@ -20,7 +20,11 @@ export default { return this.parentGroup.childrenCount > MAX_CHILDREN_COUNT; }, moreChildrenStats() { - return n__('One more item', '%d more items', this.parentGroup.childrenCount - this.parentGroup.children.length); + return n__( + 'One more item', + '%d more items', + this.parentGroup.childrenCount - this.parentGroup.children.length, + ); }, }, }; @@ -43,8 +47,9 @@ export default { <i class="fa fa-external-link" aria-hidden="true" - /> - {{moreChildrenStats}} + > + </i> + {{ moreChildrenStats }} </a> </li> </ul> diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 6421547bbde..764b130fdb8 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -75,9 +75,10 @@ export default { :id="groupDomId" :class="rowClass" class="group-row" - > + > <div - class="group-row-contents"> + class="group-row-contents" + :class="{ 'project-row-contents': !isGroup }"> <item-actions v-if="isGroup" :group="group" @@ -87,7 +88,8 @@ export default { :item="group" /> <div - class="folder-toggle-wrap"> + class="folder-toggle-wrap" + > <item-caret :is-group-open="group.isOpen" /> @@ -97,7 +99,7 @@ export default { /> </div> <div - class="avatar-container s40 hidden-xs" + class="avatar-container prepend-top-8 prepend-left-5 s24 hidden-xs" :class="{ 'content-loading': group.isChildrenLoading }" > <a @@ -106,24 +108,26 @@ export default { > <img v-if="hasAvatar" - class="avatar s40" + class="avatar s24" :src="group.avatarUrl" /> <identicon v-else - :entity-id=group.id + size-class="s24" + :entity-id="group.id" :entity-name="group.name" /> </a> </div> <div - class="title namespace-title"> + class="title namespace-title" + > <a v-tooltip :href="group.relativePath" :title="group.fullName" class="no-expand" - data-placement="top" + data-placement="bottom" >{{ // ending bracket must be by closing tag to prevent // link hover text-decoration from over-extending @@ -133,13 +137,14 @@ export default { v-if="group.permission" class="user-access-role" > - {{group.permission}} + {{ group.permission }} </span> </div> <div v-if="group.description" class="description"> - {{group.description}} + <span v-html="group.description"> + </span> </div> </div> <group-folder diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 75a2bf34887..adde8c8cdb3 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -1,47 +1,48 @@ <script> -import tablePagination from '~/vue_shared/components/table_pagination.vue'; -import eventHub from '../event_hub'; -import { getParameterByName } from '../../lib/utils/common_utils'; + import tablePagination from '~/vue_shared/components/table_pagination.vue'; + import eventHub from '../event_hub'; + import { getParameterByName } from '../../lib/utils/common_utils'; -export default { - components: { - tablePagination, - }, - props: { - groups: { - type: Array, - required: true, + export default { + components: { + tablePagination, }, - pageInfo: { - type: Object, - required: true, + props: { + groups: { + type: Array, + required: true, + }, + pageInfo: { + type: Object, + required: true, + }, + searchEmpty: { + type: Boolean, + required: true, + }, + searchEmptyMessage: { + type: String, + required: true, + }, }, - searchEmpty: { - type: Boolean, - required: true, + methods: { + change(page) { + const filterGroupsParam = getParameterByName('filter_groups'); + const sortParam = getParameterByName('sort'); + const archivedParam = getParameterByName('archived'); + eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam); + }, }, - searchEmptyMessage: { - type: String, - required: true, - }, - }, - methods: { - change(page) { - const filterGroupsParam = getParameterByName('filter_groups'); - const sortParam = getParameterByName('sort'); - const archivedParam = getParameterByName('archived'); - eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam); - }, - }, -}; + }; </script> <template> <div class="groups-list-tree-container"> <div v-if="searchEmpty" - class="has-no-search-results"> - {{searchEmptyMessage}} + class="has-no-search-results" + > + {{ searchEmptyMessage }} </div> <group-folder v-if="!searchEmpty" @@ -50,7 +51,7 @@ export default { <table-pagination v-if="!searchEmpty" :change="change" - :pageInfo="pageInfo" + :page-info="pageInfo" /> </div> </template> diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue index 58ba5aff7cf..87065b3d6e3 100644 --- a/app/assets/javascripts/groups/components/item_actions.vue +++ b/app/assets/javascripts/groups/components/item_actions.vue @@ -1,15 +1,12 @@ <script> -import { s__ } from '../../locale'; -import tooltip from '../../vue_shared/directives/tooltip'; -import modal from '../../vue_shared/components/modal.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import icon from '~/vue_shared/components/icon.vue'; import eventHub from '../event_hub'; import { COMMON_STR } from '../constants'; -import Icon from '../../vue_shared/components/icon.vue'; export default { components: { - Icon, - modal, + icon, }, directives: { tooltip, @@ -25,11 +22,6 @@ export default { required: true, }, }, - data() { - return { - modalStatus: false, - }; - }, computed: { leaveBtnTitle() { return COMMON_STR.LEAVE_BTN_TITLE; @@ -37,19 +29,10 @@ export default { editBtnTitle() { return COMMON_STR.EDIT_BTN_TITLE; }, - leaveConfirmationMessage() { - return s__(`GroupsTree|Are you sure you want to leave the "${this.group.fullName}" group?`); - }, }, methods: { onLeaveGroup() { - this.modalStatus = true; - }, - leaveGroup(leaveConfirmed) { - this.modalStatus = false; - if (leaveConfirmed) { - eventHub.$emit('leaveGroup', this.group, this.parentGroup); - } + eventHub.$emit('showLeaveGroupModal', this.group, this.parentGroup); }, }, }; @@ -64,10 +47,9 @@ export default { :title="editBtnTitle" :aria-label="editBtnTitle" data-container="body" + data-placement="bottom" class="edit-group btn no-expand"> - <icon - name="settings"> - </icon> + <icon name="settings"/> </a> <a v-tooltip @@ -77,19 +59,9 @@ export default { :title="leaveBtnTitle" :aria-label="leaveBtnTitle" data-container="body" + data-placement="bottom" class="leave-group btn no-expand"> - <i - class="fa fa-sign-out" - aria-hidden="true"/> + <icon name="leave"/> </a> - <modal - v-show="modalStatus" - :primary-button-label="__('Leave')" - kind="warning" - :title="__('Are you sure?')" - :text="__('Are you sure you want to leave this group?')" - :body="leaveConfirmationMessage" - @submit="leaveGroup" - /> </div> </template> diff --git a/app/assets/javascripts/groups/components/item_caret.vue b/app/assets/javascripts/groups/components/item_caret.vue index 959b984816f..2a5bec5e86c 100644 --- a/app/assets/javascripts/groups/components/item_caret.vue +++ b/app/assets/javascripts/groups/components/item_caret.vue @@ -1,5 +1,10 @@ <script> +import icon from '~/vue_shared/components/icon.vue'; + export default { + components: { + icon, + }, props: { isGroupOpen: { type: Boolean, @@ -9,7 +14,7 @@ export default { }, computed: { iconClass() { - return this.isGroupOpen ? 'fa-caret-down' : 'fa-caret-right'; + return this.isGroupOpen ? 'angle-down' : 'angle-right'; }, }, }; @@ -17,9 +22,9 @@ export default { <template> <span class="folder-caret"> - <i - :class="iconClass" - class="fa" - aria-hidden="true"/> + <icon + :size="12" + :name="iconClass" + /> </span> </template> diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue index 9f8ac138fc3..168b4e4af2c 100644 --- a/app/assets/javascripts/groups/components/item_stats.vue +++ b/app/assets/javascripts/groups/components/item_stats.vue @@ -1,98 +1,89 @@ <script> -import tooltip from '../../vue_shared/directives/tooltip'; -import { ITEM_TYPE, VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE, PROJECT_VISIBILITY_TYPE } from '../constants'; + import icon from '~/vue_shared/components/icon.vue'; + import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + import { + ITEM_TYPE, + VISIBILITY_TYPE_ICON, + GROUP_VISIBILITY_TYPE, + PROJECT_VISIBILITY_TYPE, + } from '../constants'; + import itemStatsValue from './item_stats_value.vue'; -export default { - directives: { - tooltip, - }, - props: { - item: { - type: Object, - required: true, + export default { + components: { + icon, + timeAgoTooltip, + itemStatsValue, }, - }, - computed: { - visibilityIcon() { - return VISIBILITY_TYPE_ICON[this.item.visibility]; + props: { + item: { + type: Object, + required: true, + }, }, - visibilityTooltip() { - if (this.item.type === ITEM_TYPE.GROUP) { - return GROUP_VISIBILITY_TYPE[this.item.visibility]; - } - return PROJECT_VISIBILITY_TYPE[this.item.visibility]; + computed: { + visibilityIcon() { + return VISIBILITY_TYPE_ICON[this.item.visibility]; + }, + visibilityTooltip() { + if (this.item.type === ITEM_TYPE.GROUP) { + return GROUP_VISIBILITY_TYPE[this.item.visibility]; + } + return PROJECT_VISIBILITY_TYPE[this.item.visibility]; + }, + isProject() { + return this.item.type === ITEM_TYPE.PROJECT; + }, + isGroup() { + return this.item.type === ITEM_TYPE.GROUP; + }, }, - isProject() { - return this.item.type === ITEM_TYPE.PROJECT; - }, - isGroup() { - return this.item.type === ITEM_TYPE.GROUP; - }, - }, -}; + }; </script> <template> <div class="stats"> - <span - v-tooltip + <item-stats-value v-if="isGroup" - :title="s__('Subgroups')" - class="number-subgroups" - data-placement="top" - data-container="body"> - <i - class="fa fa-folder" - aria-hidden="true" - /> - {{item.subgroupCount}} - </span> - <span - v-tooltip + css-class="number-subgroups" + icon-name="folder" + :title="__('Subgroups')" + :value="item.subgroupCount" + /> + <item-stats-value v-if="isGroup" - :title="s__('Projects')" - class="number-projects" - data-placement="top" - data-container="body"> - <i - class="fa fa-bookmark" - aria-hidden="true" - /> - {{item.projectCount}} - </span> - <span - v-tooltip + css-class="number-projects" + icon-name="bookmark" + :title="__('Projects')" + :value="item.projectCount" + /> + <item-stats-value v-if="isGroup" - :title="s__('Members')" - class="number-users" - data-placement="top" - data-container="body"> - <i - class="fa fa-users" - aria-hidden="true" - /> - {{item.memberCount}} - </span> - <span + css-class="number-users" + icon-name="users" + :title="__('Members')" + :value="item.memberCount" + /> + <item-stats-value v-if="isProject" - class="project-stars"> - <i - class="fa fa-star" - aria-hidden="true" - /> - {{item.starCount}} - </span> - <span - v-tooltip + css-class="project-stars" + icon-name="star" + :value="item.starCount" + /> + <item-stats-value + css-class="item-visibility" + tooltip-placement="left" + :icon-name="visibilityIcon" :title="visibilityTooltip" - data-placement="left" - data-container="body" - class="item-visibility"> - <i - :class="visibilityIcon" - class="fa" - aria-hidden="true" + /> + <div + class="last-updated" + v-if="isProject" + > + <time-ago-tooltip + tooltip-placement="bottom" + :time="item.updatedAt" /> - </span> + </div> </div> </template> diff --git a/app/assets/javascripts/groups/components/item_stats_value.vue b/app/assets/javascripts/groups/components/item_stats_value.vue new file mode 100644 index 00000000000..08d0bf6e344 --- /dev/null +++ b/app/assets/javascripts/groups/components/item_stats_value.vue @@ -0,0 +1,68 @@ +<script> + import tooltip from '~/vue_shared/directives/tooltip'; + import icon from '~/vue_shared/components/icon.vue'; + + export default { + components: { + icon, + }, + directives: { + tooltip, + }, + props: { + title: { + type: String, + required: false, + default: '', + }, + cssClass: { + type: String, + required: false, + default: '', + }, + iconName: { + type: String, + required: true, + }, + tooltipPlacement: { + type: String, + required: false, + default: 'bottom', + }, + /** + * 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, + default: '', + }, + }, + computed: { + isValuePresent() { + return this.value !== ''; + }, + }, + }; +</script> + +<template> + <span + v-tooltip + data-container="body" + :data-placement="tooltipPlacement" + :class="cssClass" + :title="title" + > + <icon :name="iconName" /> + <span + v-if="isValuePresent" + class="stat-value" + > + {{ value }} + </span> + </span> +</template> diff --git a/app/assets/javascripts/groups/components/item_type_icon.vue b/app/assets/javascripts/groups/components/item_type_icon.vue index c02a8ad6d8c..118d94d4937 100644 --- a/app/assets/javascripts/groups/components/item_type_icon.vue +++ b/app/assets/javascripts/groups/components/item_type_icon.vue @@ -1,7 +1,11 @@ <script> +import icon from '~/vue_shared/components/icon.vue'; import { ITEM_TYPE } from '../constants'; export default { + components: { + icon, + }, props: { itemType: { type: String, @@ -16,9 +20,9 @@ export default { computed: { iconClass() { if (this.itemType === ITEM_TYPE.GROUP) { - return this.isGroupOpen ? 'fa-folder-open' : 'fa-folder'; + return this.isGroupOpen ? 'folder-open' : 'folder'; } - return 'fa-bookmark'; + return 'bookmark'; }, }, }; @@ -26,9 +30,6 @@ export default { <template> <span class="item-type-icon"> - <i - :class="iconClass" - class="fa" - aria-hidden="true"/> + <icon :name="iconClass"/> </span> </template> diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js index 6fde41414b3..b8baed682f5 100644 --- a/app/assets/javascripts/groups/constants.js +++ b/app/assets/javascripts/groups/constants.js @@ -29,7 +29,7 @@ export const PROJECT_VISIBILITY_TYPE = { }; export const VISIBILITY_TYPE_ICON = { - public: 'fa-globe', - internal: 'fa-shield', - private: 'fa-lock', + public: 'earth', + internal: 'shield', + private: 'lock', }; diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index 2db233b09da..31d56d15c23 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -1,6 +1,6 @@ import FilterableList from '~/filterable_list'; import eventHub from './event_hub'; -import { getParameterByName } from '../lib/utils/common_utils'; +import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils'; export default class GroupFilterableList extends FilterableList { constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) { @@ -94,23 +94,14 @@ export default class GroupFilterableList extends FilterableList { this.form.querySelector(`[name="${this.filterInputField}"]`).value = ''; } - onFilterSuccess(data, xhr, queryData) { + onFilterSuccess(res, queryData) { const currentPath = this.getPagePath(queryData); - const paginationData = { - 'X-Per-Page': xhr.getResponseHeader('X-Per-Page'), - 'X-Page': xhr.getResponseHeader('X-Page'), - 'X-Total': xhr.getResponseHeader('X-Total'), - 'X-Total-Pages': xhr.getResponseHeader('X-Total-Pages'), - 'X-Next-Page': xhr.getResponseHeader('X-Next-Page'), - 'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'), - }; - window.history.replaceState({ page: currentPath, }, document.title, currentPath); - eventHub.$emit('updateGroups', data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField)); - eventHub.$emit('updatePagination', paginationData); + eventHub.$emit('updateGroups', res.data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField)); + eventHub.$emit('updatePagination', normalizeHeaders(res.headers)); } } diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 8b850765a1b..57eaac72906 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -10,7 +10,7 @@ import groupItemComponent from './components/group_item.vue'; Vue.use(Translate); -document.addEventListener('DOMContentLoaded', () => { +export default () => { const el = document.getElementById('js-groups-tree'); // Don't do anything if element doesn't exist (No groups) @@ -71,4 +71,4 @@ document.addEventListener('DOMContentLoaded', () => { }); }, }); -}); +}; diff --git a/app/assets/javascripts/groups/service/groups_service.js b/app/assets/javascripts/groups/service/groups_service.js index 639410384c2..b79ba291463 100644 --- a/app/assets/javascripts/groups/service/groups_service.js +++ b/app/assets/javascripts/groups/service/groups_service.js @@ -1,7 +1,5 @@ import Vue from 'vue'; -import VueResource from 'vue-resource'; - -Vue.use(VueResource); +import '../../vue_shared/vue_resource_interceptor'; export default class GroupsService { constructor(endpoint) { diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js index a1689f4c5cc..4a7569078a1 100644 --- a/app/assets/javascripts/groups/store/groups_store.js +++ b/app/assets/javascripts/groups/store/groups_store.js @@ -71,7 +71,7 @@ export default class GroupsStore { id: rawGroupItem.id, name: rawGroupItem.name, fullName: rawGroupItem.full_name, - description: rawGroupItem.description, + description: rawGroupItem.markdown_description, visibility: rawGroupItem.visibility, avatarUrl: rawGroupItem.avatar_url, relativePath: rawGroupItem.relative_path, @@ -91,6 +91,7 @@ export default class GroupsStore { subgroupCount: rawGroupItem.subgroup_count, memberCount: rawGroupItem.number_users_with_delimiter, starCount: rawGroupItem.star_count, + updatedAt: rawGroupItem.updated_at, }; } diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue index 704dff981df..a8459b011df 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -32,7 +32,6 @@ this.$emit('toggleCollapsed'); }, }, - }; </script> diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 7f29a355eca..89981ab2c65 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,70 +1,96 @@ <script> -import { mapState, mapGetters } from 'vuex'; -import ideSidebar from './ide_side_bar.vue'; -import ideContextbar from './ide_context_bar.vue'; -import repoTabs from './repo_tabs.vue'; -import repoFileButtons from './repo_file_buttons.vue'; -import ideStatusBar from './ide_status_bar.vue'; -import repoPreview from './repo_preview.vue'; -import repoEditor from './repo_editor.vue'; + import { mapState, mapGetters } from 'vuex'; + import ideSidebar from './ide_side_bar.vue'; + import ideContextbar from './ide_context_bar.vue'; + import repoTabs from './repo_tabs.vue'; + import repoFileButtons from './repo_file_buttons.vue'; + import ideStatusBar from './ide_status_bar.vue'; + import repoPreview from './repo_preview.vue'; + import repoEditor from './repo_editor.vue'; -export default { - computed: { - ...mapState([ - 'currentBlobView', - 'selectedFile', - ]), - ...mapGetters([ - 'changedFiles', - 'activeFile', - ]), - }, - components: { - ideSidebar, - ideContextbar, - repoTabs, - repoFileButtons, - ideStatusBar, - repoEditor, - repoPreview, - }, - mounted() { - const returnValue = 'Are you sure you want to lose unsaved changes?'; - window.onbeforeunload = (e) => { - if (!this.changedFiles.length) return undefined; + export default { + components: { + ideSidebar, + ideContextbar, + repoTabs, + repoFileButtons, + ideStatusBar, + repoEditor, + repoPreview, + }, + props: { + emptyStateSvgPath: { + type: String, + required: true, + }, + }, + computed: { + ...mapState([ + 'currentBlobView', + 'selectedFile', + ]), + ...mapGetters([ + 'changedFiles', + 'activeFile', + ]), + }, + mounted() { + const returnValue = 'Are you sure you want to lose unsaved changes?'; + window.onbeforeunload = (e) => { + if (!this.changedFiles.length) return undefined; - Object.assign(e, { - returnValue, - }); - return returnValue; - }; - }, -}; + Object.assign(e, { + returnValue, + }); + return returnValue; + }; + }, + }; </script> <template> - <div + <div class="ide-view" > - <ide-sidebar/> + <ide-sidebar /> <div class="multi-file-edit-pane" > <template - v-if="activeFile"> + v-if="activeFile" + > <repo-tabs/> <component class="multi-file-edit-pane-content" :is="currentBlobView" /> - <repo-file-buttons/> + <repo-file-buttons /> <ide-status-bar - :file="selectedFile"/> + :file="selectedFile" + /> </template> <template - v-else> + v-else + > <div class="ide-empty-state"> - <h2 class="clgray">Welcome to the GitLab IDE</h2> + <div class="row js-empty-state"> + <div class="col-xs-12"> + <div class="svg-content svg-250"> + <img :src="emptyStateSvgPath" /> + </div> + </div> + <div class="col-xs-12"> + <div class="text-content text-center"> + <h4> + Welcome to the GitLab IDE + </h4> + <p> + You can select a file in the left sidebar to begin + editing and use the right sidebar to commit your changes. + </p> + </div> + </div> + </div> </div> </template> </div> diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue index 5a08718e386..dd947f66969 100644 --- a/app/assets/javascripts/ide/components/ide_context_bar.vue +++ b/app/assets/javascripts/ide/components/ide_context_bar.vue @@ -1,36 +1,59 @@ <script> -import { mapGetters, mapState, mapActions } from 'vuex'; -import repoCommitSection from './repo_commit_section.vue'; -import icon from '../../vue_shared/components/icon.vue'; + import { mapGetters, mapState, mapActions } from 'vuex'; + 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: { - repoCommitSection, - icon, - }, - computed: { - ...mapState([ - 'rightPanelCollapsed', - ]), - ...mapGetters([ - 'changedFiles', - ]), - currentIcon() { - return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right'; + export default { + components: { + repoCommitSection, + icon, + panelResizer, }, - }, - methods: { - ...mapActions([ - 'setPanelCollapsedStatus', - ]), - toggleCollapsed() { - this.setPanelCollapsedStatus({ - side: 'right', - collapsed: !this.rightPanelCollapsed, - }); + data() { + return { + width: 290, + }; }, - }, -}; + computed: { + ...mapState([ + 'rightPanelCollapsed', + ]), + ...mapGetters([ + 'changedFiles', + ]), + currentIcon() { + return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right'; + }, + maxSize() { + return window.innerWidth / 2; + }, + panelStyle() { + if (!this.rightPanelCollapsed) { + return { width: `${this.width}px` }; + } + return {}; + }, + }, + methods: { + ...mapActions([ + 'setPanelCollapsedStatus', + 'setResizingStatus', + ]), + toggleCollapsed() { + this.setPanelCollapsedStatus({ + side: 'right', + collapsed: !this.rightPanelCollapsed, + }); + }, + resizingStarted() { + this.setResizingStatus(true); + }, + resizingEnded() { + this.setResizingStatus(false); + }, + }, + }; </script> <template> @@ -39,18 +62,19 @@ export default { :class="{ 'is-collapsed': rightPanelCollapsed, }" + :style="panelStyle" > - <div - class="multi-file-commit-panel-section"> + <div class="multi-file-commit-panel-section"> <header class="multi-file-commit-panel-header" :class="{ - 'is-collapsed': rightPanelCollapsed, - }" - > + 'is-collapsed': rightPanelCollapsed, + }" + > <div class="multi-file-commit-panel-header-title" - v-if="!rightPanelCollapsed"> + v-if="!rightPanelCollapsed" + > <icon name="list-bulleted" :size="18" @@ -68,8 +92,17 @@ export default { /> </button> </header> - <repo-commit-section - class=""/> + <repo-commit-section /> </div> + <panel-resizer + :size.sync="width" + :enabled="!rightPanelCollapsed" + :start-size="290" + :min-size="200" + :max-size="maxSize" + @resize-start="resizingStarted" + @resize-end="resizingEnded" + side="left" + /> </div> </template> 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 bd3a521ff43..af2f7341a91 100644 --- a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue +++ b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue @@ -28,20 +28,20 @@ export default { <div class="branch-header-title"> <icon name="branch" - :size="12"> - </icon> + :size="12" + /> {{ branch.name }} </div> <div class="branch-header-btns"> <new-dropdown :project-id="projectId" :branch="branch.name" - path=""/> + path="" + /> </div> </div> <div> - <repo-tree - :treeId="branch.treeId"/> + <repo-tree :tree-id="branch.treeId" /> </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue index 61daba6d176..ed49a0e72a2 100644 --- a/app/assets/javascripts/ide/components/ide_project_tree.vue +++ b/app/assets/javascripts/ide/components/ide_project_tree.vue @@ -19,9 +19,10 @@ export default { <template> <div class="projects-sidebar"> <div class="context-header"> - <a - :title="project.name" - :href="project.web_url"> + <a + :title="project.name" + :href="project.web_url" + > <div class="avatar-container s40 project-avatar"> <project-avatar-image class="avatar-container project-avatar" @@ -29,7 +30,7 @@ export default { :img-src="project.avatar_url" :img-alt="project.name" :img-size="40" - /> + /> </div> <div class="sidebar-context-title"> {{ project.name }} @@ -38,10 +39,11 @@ export default { </div> <div class="multi-file-commit-panel-inner-scroll"> <branches-tree - v-for="(branch, index) in project.branches" + v-for="branch in project.branches" :key="branch.name" :project-id="project.path_with_namespace" - :branch="branch"/> + :branch="branch" + /> </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue index b6b089e6b25..4651e345d75 100644 --- a/app/assets/javascripts/ide/components/ide_repo_tree.vue +++ b/app/assets/javascripts/ide/components/ide_repo_tree.vue @@ -1,15 +1,15 @@ <script> import { mapState } from 'vuex'; -import RepoPreviousDirectory from './repo_prev_directory.vue'; -import RepoFile from './repo_file.vue'; -import RepoLoadingFile from './repo_loading_file.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 { components: { - 'repo-previous-directory': RepoPreviousDirectory, - 'repo-file': RepoFile, - 'repo-loading-file': RepoLoadingFile, + repoPreviousDirectory, + repoFile, + skeletonLoadingContainer, }, props: { treeId: { @@ -19,7 +19,7 @@ export default { }, computed: { ...mapState([ - 'loading', + 'trees', 'isRoot', ]), ...mapState({ @@ -34,33 +34,41 @@ export default { return !this.isRoot && this.fetchedList.length; }, showLoading() { - return this.loading; + if (this.trees[this.treeId]) { + return this.trees[this.treeId].loading; + } + return true; }, }, }; </script> <template> -<div> - <div class="ide-file-list"> - <table class="table"> - <tbody - v-if="treeId"> - <repo-previous-directory - v-if="hasPreviousDirectory" - /> - <repo-loading-file - v-if="showLoading" - v-for="n in 5" - :key="n" - /> - <repo-file - v-for="file in fetchedList" - :key="file.key" - :file="file" - /> - </tbody> - </table> + <div> + <div class="ide-file-list"> + <table class="table"> + <tbody + v-if="treeId" + > + <repo-previous-directory + v-if="hasPreviousDirectory" + /> + <template v-if="showLoading"> + <div + class="multi-file-loading-container" + v-for="n in 3" + :key="n" + > + <skeleton-loading-container /> + </div> + </template> + <repo-file + v-for="file in fetchedList" + :key="file.key" + :file="file" + /> + </tbody> + </table> + </div> </div> -</div> </template> diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index 535398d98c2..a68f8ce0169 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,48 +1,88 @@ <script> -import { mapState, mapActions } from 'vuex'; -import projectTree from './ide_project_tree.vue'; -import icon from '../../vue_shared/components/icon.vue'; + import { mapState, mapActions } from 'vuex'; + 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: { - projectTree, - icon, - }, - computed: { - ...mapState([ - 'projects', - 'leftPanelCollapsed', - ]), - currentIcon() { - return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left'; + export default { + components: { + projectTree, + icon, + panelResizer, + skeletonLoadingContainer, }, - }, - methods: { - ...mapActions([ - 'setPanelCollapsedStatus', - ]), - toggleCollapsed() { - this.setPanelCollapsedStatus({ - side: 'left', - collapsed: !this.leftPanelCollapsed, - }); + data() { + return { + width: 290, + }; }, - }, -}; + computed: { + ...mapState([ + 'loading', + 'projects', + 'leftPanelCollapsed', + ]), + currentIcon() { + return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left'; + }, + maxSize() { + return window.innerWidth / 2; + }, + panelStyle() { + if (!this.leftPanelCollapsed) { + return { width: `${this.width}px` }; + } + return {}; + }, + showLoading() { + return this.loading; + }, + }, + methods: { + ...mapActions([ + 'setPanelCollapsedStatus', + 'setResizingStatus', + ]), + toggleCollapsed() { + this.setPanelCollapsedStatus({ + side: 'left', + collapsed: !this.leftPanelCollapsed, + }); + }, + resizingStarted() { + this.setResizingStatus(true); + }, + resizingEnded() { + this.setResizingStatus(false); + }, + }, + }; </script> <template> <div - class="multi-file-commit-panel" - :class="{ - 'is-collapsed': leftPanelCollapsed, - }" - > + class="multi-file-commit-panel" + :class="{ + 'is-collapsed': leftPanelCollapsed, + }" + :style="panelStyle" + > <div class="multi-file-commit-panel-inner"> + <template v-if="showLoading"> + <div + class="multi-file-loading-container" + v-for="n in 3" + :key="n" + > + <skeleton-loading-container /> + </div> + </template> <project-tree - v-for="(project, index) in projects" + v-for="project in projects" :key="project.id" - :project="project"/> + :project="project" + /> </div> <button type="button" @@ -56,7 +96,19 @@ export default { <span v-if="!leftPanelCollapsed" class="collapse-text" - >Collapse sidebar</span> + > + Collapse sidebar + </span> </button> + <panel-resizer + :size.sync="width" + :enabled="!leftPanelCollapsed" + :start-size="290" + :min-size="200" + :max-size="maxSize" + @resize-start="resizingStarted" + @resize-end="resizingEnded" + side="right" + /> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index a24abadd936..e48c446c4a4 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -1,70 +1,65 @@ <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 { 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'; -export default { - props: { - file: { - type: Object, - required: true, + export default { + components: { + icon, }, - }, - components: { - icon, - }, - directives: { - tooltip, - }, - mixins: [ - timeAgoMixin, - ], - computed: { - ...mapState([ - 'selectedFile', - ]), - }, -}; + directives: { + tooltip, + }, + mixins: [ + timeAgoMixin, + ], + props: { + file: { + type: Object, + required: true, + }, + }, + computed: { + ...mapState([ + 'selectedFile', + ]), + }, + }; </script> <template> - <div - class="ide-status-bar"> + <div class="ide-status-bar"> <div> <icon name="branch" - :size="12"> - </icon> + :size="12" + /> {{ selectedFile.branchId }} </div> <div> - <div - v-if="selectedFile.lastCommit && selectedFile.lastCommit.id"> + <div v-if="selectedFile.lastCommit && selectedFile.lastCommit.id"> Last commit: <a v-tooltip :title="selectedFile.lastCommit.message" - :href="selectedFile.lastCommit.url"> - {{ timeFormated(selectedFile.lastCommit.updatedAt) }} by + :href="selectedFile.lastCommit.url" + > + {{ timeFormated(selectedFile.lastCommit.updatedAt) }} by {{ selectedFile.lastCommit.author }} </a> - </div> + </div> </div> - <div - class="text-right"> + <div class="text-right"> {{ selectedFile.name }} </div> - <div - class="text-right"> + <div class="text-right"> {{ selectedFile.eol }} </div> - <div - class="text-right"> + <div class="text-right"> {{ file.editorRow }}:{{ file.editorColumn }} </div> - <div - class="text-right"> + <div class="text-right"> {{ selectedFile.fileLanguage }} </div> </div> diff --git a/app/assets/javascripts/ide/components/new_branch_form.vue b/app/assets/javascripts/ide/components/new_branch_form.vue index 2119d373d31..56e31256132 100644 --- a/app/assets/javascripts/ide/components/new_branch_form.vue +++ b/app/assets/javascripts/ide/components/new_branch_form.vue @@ -21,6 +21,13 @@ return this.loading || this.branchName === ''; }, }, + created() { + // Dropdown is outside of Vue instance & is controlled by Bootstrap + this.$dropdown = $('.git-revision-dropdown'); + + // text element is outside Vue app + this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text'); + }, methods: { ...mapActions([ 'createNewBranch', @@ -55,13 +62,6 @@ })); }, }, - created() { - // Dropdown is outside of Vue instance & is controlled by Bootstrap - this.$dropdown = $('.git-revision-dropdown'); - - // text element is outside Vue app - this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text'); - }, }; </script> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index 6e67e99a70f..ef653357f5f 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -4,6 +4,11 @@ import icon from '../../../vue_shared/components/icon.vue'; export default { + components: { + icon, + newModal, + upload, + }, props: { branch: { type: String, @@ -18,11 +23,6 @@ default: null, }, }, - components: { - icon, - newModal, - upload, - }, data() { return { openModal: false, @@ -32,10 +32,10 @@ methods: { createNewItem(type) { this.modalType = type; - this.toggleModalOpen(); + this.openModal = true; }, - toggleModalOpen() { - this.openModal = !this.openModal; + hideModal() { + this.openModal = false; }, }, }; @@ -95,7 +95,7 @@ :branch-id="branch" :path="path" :parent="parent" - @toggle="toggleModalOpen" + @hide="hideModal" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index a0650d37690..36cd825c6dd 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -4,6 +4,9 @@ import modal from '../../../vue_shared/components/modal.vue'; export default { + components: { + modal, + }, props: { branchId: { type: String, @@ -27,28 +30,6 @@ entryName: this.path !== '' ? `${this.path}/` : '', }; }, - components: { - modal, - }, - methods: { - ...mapActions([ - 'createTempEntry', - ]), - createEntryInStore() { - this.createTempEntry({ - projectId: this.currentProjectId, - branchId: this.branchId, - parent: this.parent, - name: this.entryName.replace(new RegExp(`^${this.path}/`), ''), - type: this.type, - }); - - this.toggleModalOpen(); - }, - toggleModalOpen() { - this.$emit('toggle'); - }, - }, computed: { ...mapState([ 'currentProjectId', @@ -78,6 +59,25 @@ mounted() { this.$refs.fieldName.focus(); }, + methods: { + ...mapActions([ + 'createTempEntry', + ]), + createEntryInStore() { + this.createTempEntry({ + projectId: this.currentProjectId, + branchId: this.branchId, + parent: this.parent, + name: this.entryName.replace(new RegExp(`^${this.path}/`), ''), + type: this.type, + }); + + this.hideModal(); + }, + hideModal() { + this.$emit('hide'); + }, + }, }; </script> @@ -86,7 +86,7 @@ :title="modalTitle" :primary-button-label="buttonLabel" kind="success" - @toggle="toggleModalOpen" + @cancel="hideModal" @submit="createEntryInStore" > <form diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue index 2a2f2a241fc..6244737fa43 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -18,6 +18,12 @@ 'currentProjectId', ]), }, + mounted() { + this.$refs.fileUpload.addEventListener('change', this.openFile); + }, + beforeDestroy() { + this.$refs.fileUpload.removeEventListener('change', this.openFile); + }, methods: { ...mapActions([ 'createTempEntry', @@ -59,12 +65,6 @@ this.$refs.fileUpload.click(); }, }, - mounted() { - this.$refs.fileUpload.addEventListener('change', this.openFile); - }, - beforeDestroy() { - this.$refs.fileUpload.removeEventListener('change', this.openFile); - }, }; </script> diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 470db2c9650..96b1bb78c1d 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -49,7 +49,9 @@ export default { const createNewBranch = newBranch || this.startNewMR; const payload = { - branch: createNewBranch ? `${this.currentBranchId}-${new Date().getTime().toString()}` : this.currentBranchId, + branch: createNewBranch ? + `${this.currentBranchId}-${new Date().getTime().toString()}` : + this.currentBranchId, commit_message: this.commitMessage, actions: this.changedFiles.map(f => ({ action: f.tempFile ? 'create' : 'update', @@ -66,12 +68,8 @@ export default { this.commitChanges({ payload, newMr: this.startNewMR }) .then(() => { this.submitCommitsLoading = false; - this.$store.dispatch('getTreeData', { - projectId: this.currentProjectId, - branch: this.currentBranchId, - endpoint: `/tree/${this.currentBranchId}`, - force: true, - }); + this.commitMessage = ''; + this.startNewMR = false; }) .catch(() => { this.submitCommitsLoading = false; @@ -103,69 +101,71 @@ export default { </script> <template> -<div class="multi-file-commit-panel-section"> - <modal - v-if="showNewBranchModal" - :primary-button-label="__('Create new branch')" - kind="primary" - :title="__('Branch has changed')" - :text="__('This branch has changed since you started editing. Would you like to create a new branch?')" - @toggle="showNewBranchModal = false" - @submit="makeCommit(true)" - /> - <commit-files-list - title="Staged" - :file-list="changedFiles" - :collapsed="rightPanelCollapsed" - @toggleCollapsed="toggleCollapsed" - /> - <form - class="form-horizontal multi-file-commit-form" - @submit.prevent="tryCommit" - v-if="!rightPanelCollapsed" - > - <div class="multi-file-commit-fieldset"> - <textarea - class="form-control multi-file-commit-message" - name="commit-message" - v-model="commitMessage" - placeholder="Commit message" - > - </textarea> - </div> - <div class="multi-file-commit-fieldset"> - <label - v-tooltip - title="Create a new merge request with these changes" - data-container="body" - data-placement="top" - > - <input - type="checkbox" - v-model="startNewMR" - /> - Merge Request - </label> - <button - type="submit" - :disabled="commitButtonDisabled" - class="btn btn-default btn-sm append-right-10 prepend-left-10" - > - <i - v-if="submitCommitsLoading" - class="js-commit-loading-icon fa fa-spinner fa-spin" - aria-hidden="true" - aria-label="loading" + <div class="multi-file-commit-panel-section"> + <modal + v-if="showNewBranchModal" + :primary-button-label="__('Create new branch')" + kind="primary" + :title="__('Branch has changed')" + :text="__(`This branch has changed since +you started editing. Would you like to create a new branch?`)" + @cancel="showNewBranchModal = false" + @submit="makeCommit(true)" + /> + <commit-files-list + title="Staged" + :file-list="changedFiles" + :collapsed="rightPanelCollapsed" + @toggleCollapsed="toggleCollapsed" + /> + <form + class="form-horizontal multi-file-commit-form" + @submit.prevent="tryCommit" + v-if="!rightPanelCollapsed" + > + <div class="multi-file-commit-fieldset"> + <textarea + class="form-control multi-file-commit-message" + name="commit-message" + v-model="commitMessage" + placeholder="Commit message" > - </i> - Commit - </button> - <div - class="multi-file-commit-message-count" - > - {{ commitMessageCount }} + </textarea> </div> - </div> - </form> -</div> + <div class="multi-file-commit-fieldset"> + <label + v-tooltip + title="Create a new merge request with these changes" + data-container="body" + data-placement="top" + > + <input + type="checkbox" + v-model="startNewMR" + /> + Merge Request + </label> + <button + type="submit" + :disabled="commitButtonDisabled" + class="btn btn-default btn-sm append-right-10 prepend-left-10" + :class="{ disabled: submitCommitsLoading }" + > + <i + v-if="submitCommitsLoading" + class="js-commit-loading-icon fa fa-spinner fa-spin" + aria-hidden="true" + aria-label="loading" + > + </i> + Commit + </button> + <div + class="multi-file-commit-message-count" + > + {{ commitMessageCount }} + </div> + </div> + </form> + </div> </template> diff --git a/app/assets/javascripts/ide/components/repo_edit_button.vue b/app/assets/javascripts/ide/components/repo_edit_button.vue index 37bd9003e96..c43e9163340 100644 --- a/app/assets/javascripts/ide/components/repo_edit_button.vue +++ b/app/assets/javascripts/ide/components/repo_edit_button.vue @@ -40,7 +40,7 @@ export default { aria-hidden="true"> </i> <span> - {{buttonLabel}} + {{ buttonLabel }} </span> </button> <modal @@ -50,7 +50,7 @@ export default { kind="warning" :title="__('Are you sure?')" :text="__('Are you sure you want to discard your changes?')" - @toggle="closeDiscardPopup" + @cancel="closeDiscardPopup" @submit="toggleEditMode(true)" /> </div> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 221be4b9074..f99228012f4 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -6,6 +6,38 @@ import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; export default { + computed: { + ...mapGetters([ + 'activeFile', + 'activeFileExtension', + ]), + ...mapState([ + 'leftPanelCollapsed', + 'rightPanelCollapsed', + 'panelResizing', + ]), + shouldHideEditor() { + return this.activeFile.binary && !this.activeFile.raw; + }, + }, + watch: { + activeFile(oldVal, newVal) { + if (newVal && !newVal.active) { + this.initMonaco(); + } + }, + leftPanelCollapsed() { + this.editor.updateDimensions(); + }, + rightPanelCollapsed() { + this.editor.updateDimensions(); + }, + panelResizing(isResizing) { + if (isResizing === false) { + this.editor.updateDimensions(); + } + }, + }, beforeDestroy() { this.editor.dispose(); }, @@ -38,7 +70,10 @@ export default { this.editor.createInstance(this.$refs.editor); }) .then(() => this.setupEditor()) - .catch(() => flash('Error setting up monaco. Please try again.')); + .catch((err) => { + flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true); + throw err; + }); }, setupEditor() { if (!this.activeFile) return; @@ -78,32 +113,6 @@ export default { }); }, }, - watch: { - activeFile(oldVal, newVal) { - if (newVal && !newVal.active) { - this.initMonaco(); - } - }, - leftPanelCollapsed() { - this.editor.updateDimensions(); - }, - rightPanelCollapsed() { - this.editor.updateDimensions(); - }, - }, - computed: { - ...mapGetters([ - 'activeFile', - 'activeFileExtension', - ]), - ...mapState([ - 'leftPanelCollapsed', - 'rightPanelCollapsed', - ]), - shouldHideEditor() { - return this.activeFile.binary && !this.activeFile.raw; - }, - }, }; </script> diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index 09ca11531b1..110918872fb 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -3,15 +3,17 @@ import timeAgoMixin from '../../vue_shared/mixins/timeago'; import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; import newDropdown from './new_dropdown/index.vue'; + import fileIcon from '../../vue_shared/components/file_icon.vue'; export default { - mixins: [ - timeAgoMixin, - ], components: { skeletonLoadingContainer, newDropdown, + fileIcon, }, + mixins: [ + timeAgoMixin, + ], props: { file: { type: Object, @@ -26,13 +28,6 @@ ...mapState([ 'leftPanelCollapsed', ]), - fileIcon() { - return { - 'fa-spinner fa-spin': this.file.loading, - [this.file.icon]: !this.file.loading, - 'fa-folder-open': !this.file.loading && this.file.opened, - }; - }, isSubmodule() { return this.file.type === 'submodule'; }, @@ -40,9 +35,12 @@ return this.file.type === 'tree'; }, levelIndentation() { - return { - marginLeft: `${this.file.level * 16}px`, - }; + if (this.file.level > 0) { + return { + marginLeft: `${this.file.level * 16}px`, + }; + } + return {}; }, shortId() { return this.file.id.substr(0, 8); @@ -65,6 +63,11 @@ }; }, }, + updated() { + if (this.file.type === 'blob' && this.file.active) { + this.$el.scrollIntoView(); + } + }, methods: { clickFile(row) { // Manual Action if a tree is selected/opened @@ -77,11 +80,6 @@ this.$router.push(`/project${row.url}`); }, }, - updated() { - if (this.file.type === 'blob' && this.file.active) { - this.$el.scrollIntoView(); - } - }, }; </script> @@ -94,16 +92,17 @@ class="multi-file-table-name" :colspan="submoduleColSpan" > - <i - class="fa fa-fw file-icon" - :class="fileIcon" - :style="levelIndentation" - aria-hidden="true" - > - </i> <a class="repo-file-name" > + <file-icon + :file-name="file.name" + :loading="file.loading" + :folder="file.type === 'tree'" + :opened="file.opened" + :style="levelIndentation" + :size="16" + /> {{ file.name }} </a> <new-dropdown @@ -111,10 +110,11 @@ :project-id="file.projectId" :branch="file.branchId" :path="file.path" - :parent="file"/> + :parent="file" + /> <i class="fa" - v-if="changedClass" + v-if="file.changed || file.tempFile" :class="changedClass" aria-hidden="true" > diff --git a/app/assets/javascripts/ide/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue index 34f0d51819a..aabc0d8eada 100644 --- a/app/assets/javascripts/ide/components/repo_file_buttons.vue +++ b/app/assets/javascripts/ide/components/repo_file_buttons.vue @@ -35,20 +35,24 @@ export default { <div class="btn-group" role="group" - aria-label="File actions"> + aria-label="File actions" + > <a :href="activeFile.blamePath" - class="btn btn-default btn-sm blame"> + class="btn btn-default btn-sm blame" + > Blame </a> <a :href="activeFile.commitsPath" - class="btn btn-default btn-sm history"> + class="btn btn-default btn-sm history" + > History </a> <a :href="activeFile.permalink" - class="btn btn-default btn-sm permalink"> + class="btn btn-default btn-sm permalink" + > Permalink </a> </div> diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue index 7eb840c7608..3aeb6f0b28f 100644 --- a/app/assets/javascripts/ide/components/repo_loading_file.vue +++ b/app/assets/javascripts/ide/components/repo_loading_file.vue @@ -25,15 +25,13 @@ /> </td> <template v-if="!leftPanelCollapsed"> - <td - class="hidden-sm hidden-xs"> + <td class="hidden-sm hidden-xs"> <skeleton-loading-container :small="true" /> </td> - <td - class="hidden-xs"> + <td class="hidden-xs"> <skeleton-loading-container class="animation-container-right" :small="true" diff --git a/app/assets/javascripts/ide/components/repo_preview.vue b/app/assets/javascripts/ide/components/repo_preview.vue index 3d1e0297bd5..e47270a9855 100644 --- a/app/assets/javascripts/ide/components/repo_preview.vue +++ b/app/assets/javascripts/ide/components/repo_preview.vue @@ -1,65 +1,71 @@ <script> -import { mapGetters } from 'vuex'; -import LineHighlighter from '../../line_highlighter'; -import syntaxHighlight from '../../syntax_highlight'; + import { mapGetters } from 'vuex'; + import LineHighlighter from '../../line_highlighter'; + import syntaxHighlight from '../../syntax_highlight'; -export default { - computed: { - ...mapGetters([ - 'activeFile', - ]), - renderErrorTooLarge() { - return this.activeFile.renderError === 'too_large'; + export default { + computed: { + ...mapGetters([ + 'activeFile', + ]), + renderErrorTooLarge() { + return this.activeFile.renderError === 'too_large'; + }, }, - }, - methods: { - highlightFile() { - syntaxHighlight($(this.$el).find('.file-content')); - }, - }, - mounted() { - this.highlightFile(); - this.lineHighlighter = new LineHighlighter({ - fileHolderSelector: '.blob-viewer-container', - scrollFileHolder: true, - }); - }, - updated() { - this.$nextTick(() => { + mounted() { this.highlightFile(); - }); - }, -}; + this.lineHighlighter = new LineHighlighter({ + fileHolderSelector: '.blob-viewer-container', + scrollFileHolder: true, + }); + }, + updated() { + this.$nextTick(() => { + this.highlightFile(); + }); + }, + methods: { + highlightFile() { + syntaxHighlight($(this.$el).find('.file-content')); + }, + }, + }; </script> <template> -<div> - <div - v-if="!activeFile.renderError" - v-html="activeFile.html" - class="multi-file-preview-holder" - > - </div> - <div - v-else-if="activeFile.tempFile" - class="vertical-center render-error"> - <p class="text-center"> - The source could not be displayed for this temporary file. - </p> - </div> - <div - v-else-if="renderErrorTooLarge" - class="vertical-center render-error"> - <p class="text-center"> - The source could not be displayed because it is too large. You can <a :href="activeFile.rawPath" download>download</a> it instead. - </p> - </div> - <div - v-else - class="vertical-center render-error"> - <p class="text-center"> - The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.rawPath" download>download</a> it instead. - </p> + <div> + <div + v-if="!activeFile.renderError" + v-html="activeFile.html" + class="multi-file-preview-holder" + > + </div> + <div + v-else-if="activeFile.tempFile" + class="vertical-center render-error"> + <p class="text-center"> + The source could not be displayed for this temporary file. + </p> + </div> + <div + v-else-if="renderErrorTooLarge" + class="vertical-center render-error"> + <p class="text-center"> + The source could not be displayed because it is too large. + You can <a + :href="activeFile.rawPath" + download>download</a> it instead. + </p> + </div> + <div + v-else + class="vertical-center render-error"> + <p class="text-center"> + The source could not be displayed because a rendering error occurred. + You can <a + :href="activeFile.rawPath" + download>download</a> it instead. + </p> + </div> </div> -</div> </template> diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index 5bd63ac9ec5..5ed7bddf6ae 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -1,45 +1,46 @@ <script> -import { mapActions } from 'vuex'; + import { mapActions } from 'vuex'; + import fileIcon from '../../vue_shared/components/file_icon.vue'; -export default { - props: { - tab: { - type: Object, - required: true, + export default { + components: { + fileIcon, }, - }, - - computed: { - closeLabel() { - if (this.tab.changed || this.tab.tempFile) { - return `${this.tab.name} changed`; - } - return `Close ${this.tab.name}`; + props: { + tab: { + type: Object, + required: true, + }, }, - changedClass() { - const tabChangedObj = { - 'fa-times close-icon': !this.tab.changed && !this.tab.tempFile, - 'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile, - }; - return tabChangedObj; + computed: { + closeLabel() { + if (this.tab.changed || this.tab.tempFile) { + return `${this.tab.name} changed`; + } + return `Close ${this.tab.name}`; + }, + changedClass() { + const tabChangedObj = { + 'fa-times close-icon': !this.tab.changed && !this.tab.tempFile, + 'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile, + }; + return tabChangedObj; + }, }, - }, - methods: { - ...mapActions([ - 'closeFile', - ]), - clickFile(tab) { - this.$router.push(`/project${tab.url}`); + methods: { + ...mapActions([ + 'closeFile', + ]), + clickFile(tab) { + this.$router.push(`/project${tab.url}`); + }, }, - }, -}; + }; </script> <template> - <li - @click="clickFile(tab)" - > + <li @click="clickFile(tab)"> <button type="button" class="multi-file-tab-close" @@ -63,6 +64,10 @@ export default { :class="{active : tab.active }" :title="tab.url" > + <file-icon + :file-name="tab.name" + :size="16" + /> {{ tab.name }} </div> </li> diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue index ab0bef4f0ac..ca363bba0ef 100644 --- a/app/assets/javascripts/ide/components/repo_tabs.vue +++ b/app/assets/javascripts/ide/components/repo_tabs.vue @@ -20,7 +20,7 @@ > <repo-tab v-for="tab in openFiles" - :key="tab.id" + :key="tab.key" :tab="tab" /> </ul> diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index a9cbf8e370f..a7fb9e0588a 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -84,13 +84,13 @@ router.beforeEach((to, from, next) => { } }) .catch((e) => { - flash('Error while loading the branch files. Please try again.'); + flash('Error while loading the branch files. Please try again.', 'alert', document, null, false, true); throw e; }); } }) .catch((e) => { - flash('Error while loading the project data. Please try again.'); + flash('Error while loading the project data. Please try again.', 'alert', document, null, false, true); throw e; }); } diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index a96bd339f51..e8a19f47cee 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -1,12 +1,8 @@ import Vue from 'vue'; -import { mapActions } from 'vuex'; -import { convertPermissionToBoolean } from '../lib/utils/common_utils'; import ide from './components/ide.vue'; - import store from './stores'; import router from './ide_router'; import Translate from '../vue_shared/translate'; -import ContextualSidebar from '../contextual_sidebar'; function initIde(el) { if (!el) return null; @@ -18,30 +14,13 @@ function initIde(el) { components: { ide, }, - methods: { - ...mapActions([ - 'setInitialData', - ]), - }, - created() { - const data = el.dataset; - - this.setInitialData({ - endpoints: { - rootEndpoint: data.url, - newMergeRequestUrl: data.newMergeRequestUrl, - rootUrl: data.rootUrl, + render(createElement) { + return createElement('ide', { + props: { + emptyStateSvgPath: el.dataset.emptyStateSvgPath, }, - canCommit: convertPermissionToBoolean(data.canCommit), - onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch), - path: data.currentPath, - isRoot: convertPermissionToBoolean(data.root), - isInitialRoot: convertPermissionToBoolean(data.root), }); }, - render(createElement) { - return createElement('ide'); - }, }); } @@ -50,6 +29,3 @@ const ideElement = document.getElementById('ide'); Vue.use(Translate); initIde(ideElement); - -const contextualSidebar = new ContextualSidebar(); -contextualSidebar.bindEvents(); diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 51e202b9348..51255f15658 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import DecorationsController from './decorations/controller'; import DirtyDiffController from './diff/controller'; import Disposable from './common/disposable'; @@ -54,7 +55,7 @@ export default class Editor { attachModel(model) { this.instance.setModel(model.getModel()); - this.dirtyDiffController.attachModel(model); + if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model); this.currentModel = model; @@ -67,7 +68,7 @@ export default class Editor { return acc; }, {})); - this.dirtyDiffController.reDecorate(model); + if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model); } clearEditor() { diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index c01046c8c76..d007d0ae78f 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -3,6 +3,7 @@ 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'; export const redirectToUrl = (_, url) => visitUrl(url); @@ -63,10 +64,14 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => { } }; +export const setResizingStatus = ({ commit }, resizing) => { + commit(types.SET_RESIZING_STATUS, resizing); +}; + export const checkCommitStatus = ({ state }) => service .getBranchData(state.currentProjectId, state.currentBranchId) - .then((data) => { + .then(({ data }) => { const { id } = data.commit; const selectedBranch = state.projects[state.currentProjectId].branches[state.currentBranchId]; @@ -77,7 +82,7 @@ export const checkCommitStatus = ({ state }) => return false; }) - .catch(() => flash('Error checking branch data. Please try again.')); + .catch(() => flash('Error checking branch data. Please try again.', 'alert', document, null, false, true)); export const commitChanges = ( { commit, state, dispatch, getters }, @@ -85,10 +90,10 @@ export const commitChanges = ( ) => service .commit(state.currentProjectId, payload) - .then((data) => { + .then(({ data }) => { const { branch } = payload; if (!data.short_id) { - flash(data.message); + flash(data.message, 'alert', document, null, false, true); return; } @@ -101,19 +106,25 @@ export const commitChanges = ( }, }; + let commitMsg = `Your changes have been committed. Commit ${data.short_id}`; + if (data.stats) { + commitMsg += ` with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`; + } + flash( - `Your changes have been committed. Commit ${data.short_id} with ${ - data.stats.additions - } additions, ${data.stats.deletions} deletions.`, + commitMsg, 'notice', - ); + document, + null, + false, + true); + window.dispatchEvent(new Event('resize')); if (newMr) { + dispatch('discardAllChanges'); dispatch( 'redirectToUrl', - `${ - selectedProject.web_url - }/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`, + `${selectedProject.web_url}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`, ); } else { commit(types.SET_BRANCH_WORKING_REFERENCE, { @@ -130,12 +141,18 @@ export const commitChanges = ( }); dispatch('discardAllChanges'); - dispatch('closeAllFiles'); window.scrollTo(0, 0); } }) - .catch(() => flash('Error committing changes. Please try again.')); + .catch((err) => { + let errMsg = 'Error committing changes. Please try again.'; + if (err.response.data && err.response.data.message) { + errMsg += ` (${stripHtml(err.response.data.message)})`; + } + flash(errMsg, 'alert', document, null, false, true); + window.dispatchEvent(new Event('resize')); + }); export const createTempEntry = ( { state, dispatch }, diff --git a/app/assets/javascripts/ide/stores/actions/branch.js b/app/assets/javascripts/ide/stores/actions/branch.js index 32bdf7fec22..bc6fd2d4163 100644 --- a/app/assets/javascripts/ide/stores/actions/branch.js +++ b/app/assets/javascripts/ide/stores/actions/branch.js @@ -10,14 +10,14 @@ export const getBranchData = ( !state.projects[`${projectId}`].branches[branchId]) || force) { service.getBranchData(`${projectId}`, branchId) - .then((data) => { + .then(({ data }) => { const { id } = data.commit; commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data }); commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); resolve(data); }) .catch(() => { - flash('Error loading branch data. Please try again.'); + flash('Error loading branch data. Please try again.', 'alert', document, null, false, true); reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); }); } else { diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 0f27d5bf1c3..670af2fb89e 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -69,7 +69,7 @@ export const getFileData = ({ state, commit, dispatch }, file) => { }) .catch(() => { commit(types.TOGGLE_LOADING, file); - flash('Error loading file data. Please try again.'); + flash('Error loading file data. Please try again.', 'alert', document, null, false, true); }); }; @@ -77,22 +77,28 @@ export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFile .then((raw) => { commit(types.SET_FILE_RAW_DATA, { file, raw }); }) - .catch(() => flash('Error loading file content. Please try again.')); + .catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true)); export const changeFileContent = ({ commit }, { file, content }) => { commit(types.UPDATE_FILE_CONTENT, { file, content }); }; export const setFileLanguage = ({ state, commit }, { fileLanguage }) => { - commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage }); + if (state.selectedFile) { + commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage }); + } }; export const setFileEOL = ({ state, commit }, { eol }) => { - commit(types.SET_FILE_EOL, { file: state.selectedFile, eol }); + if (state.selectedFile) { + commit(types.SET_FILE_EOL, { file: state.selectedFile, eol }); + } }; export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => { - commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn }); + if (state.selectedFile) { + commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn }); + } }; export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => { @@ -112,7 +118,7 @@ export const createTempFile = ({ state, commit, dispatch }, { projectId, branchI url: newUrl, }); - if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`); + if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`, 'alert', document, null, false, true); commit(types.CREATE_TMP_FILE, { parent, diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 75e332090cb..faeceb430a2 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -8,15 +8,17 @@ export const getProjectData = ( { namespace, projectId, force = false } = {}, ) => new Promise((resolve, reject) => { if (!state.projects[`${namespace}/${projectId}`] || force) { + commit(types.TOGGLE_LOADING, state); service.getProjectData(namespace, projectId) .then(res => res.data) .then((data) => { + commit(types.TOGGLE_LOADING, state); commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); resolve(data); }) .catch(() => { - flash('Error loading project data. Please try again.'); + flash('Error loading project data. Please try again.', 'alert', document, null, false, true); reject(new Error(`Project not loaded ${namespace}/${projectId}`)); }); } else { diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index 25909400a75..302ba45edee 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -52,7 +52,7 @@ export const getTreeData = ( resolve(data); }) .catch((e) => { - flash('Error loading tree data. Please try again.'); + flash('Error loading tree data. Please try again.', 'alert', document, null, false, true); if (tree) commit(types.TOGGLE_LOADING, tree); reject(e); }); @@ -151,7 +151,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s dispatch('getLastCommitData', tree); }) - .catch(() => flash('Error fetching log data.')); + .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true)); }; export const updateDirectoryData = ( diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 4e3c10972ba..69b218a5e7d 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -5,6 +5,7 @@ export const SET_ROOT = 'SET_ROOT'; export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA'; export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED'; export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED'; +export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS'; // Project Mutation Types export const SET_PROJECT = 'SET_PROJECT'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 2fed9019cb6..03d81be10a1 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -49,6 +49,11 @@ export default { rightPanelCollapsed: collapsed, }); }, + [types.SET_RESIZING_STATUS](state, resizing) { + Object.assign(state, { + panelResizing: resizing, + }); + }, [types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) { Object.assign(entry.lastCommit, { id: lastCommit.commit.id, diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 5f3655b0092..72db1c180c9 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -64,7 +64,7 @@ export default { }, [types.DISCARD_FILE_CHANGES](state, file) { Object.assign(file, { - content: '', + content: file.raw, changed: false, }); }, diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index 539e382830f..61d12096946 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -19,4 +19,5 @@ export default () => ({ projects: {}, leftPanelCollapsed: false, rightPanelCollapsed: true, + panelResizing: false, }); diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 29e3ab5d040..d556404faa5 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -1,3 +1,5 @@ +import _ from 'underscore'; + export const dataStructure = () => ({ id: '', key: '', diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js index 5d4c1851fe5..e61b37a2d1f 100644 --- a/app/assets/javascripts/init_issuable_sidebar.js +++ b/app/assets/javascripts/init_issuable_sidebar.js @@ -1,5 +1,6 @@ /* eslint-disable no-new */ -/* global MilestoneSelect */ + +import MilestoneSelect from './milestone_select'; import LabelsSelect from './labels_select'; import IssuableContext from './issuable_context'; import Sidebar from './right_sidebar'; diff --git a/app/assets/javascripts/init_labels.js b/app/assets/javascripts/init_labels.js new file mode 100644 index 00000000000..5f20055510f --- /dev/null +++ b/app/assets/javascripts/init_labels.js @@ -0,0 +1,18 @@ +import LabelManager from './label_manager'; +import GroupLabelSubscription from './group_label_subscription'; +import ProjectLabelSubscription from './project_label_subscription'; + +export default () => { + if ($('.prioritized-labels').length) { + new LabelManager(); // eslint-disable-line no-new + } + $('.label-subscription').each((i, el) => { + const $el = $(el); + + if ($el.find('.dropdown-group-label').length) { + new GroupLabelSubscription($el); // eslint-disable-line no-new + } else { + new ProjectLabelSubscription($el); // eslint-disable-line no-new + } + }); +}; diff --git a/app/assets/javascripts/init_legacy_filters.js b/app/assets/javascripts/init_legacy_filters.js index 2cbb70220d0..b6ff97d1279 100644 --- a/app/assets/javascripts/init_legacy_filters.js +++ b/app/assets/javascripts/init_legacy_filters.js @@ -1,9 +1,9 @@ /* eslint-disable no-new */ import LabelsSelect from './labels_select'; -/* global MilestoneSelect */ import subscriptionSelect from './subscription_select'; import UsersSelect from './users_select'; import issueStatusSelect from './issue_status_select'; +import MilestoneSelect from './milestone_select'; export default () => { new UsersSelect(); diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js index 32415a8791f..3f27cfc2f6d 100644 --- a/app/assets/javascripts/integrations/integration_settings_form.js +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -1,4 +1,5 @@ -import Flash from '../flash'; +import axios from '../lib/utils/axios_utils'; +import flash from '../flash'; export default class IntegrationSettingsForm { constructor(formSelector) { @@ -95,29 +96,26 @@ export default class IntegrationSettingsForm { */ testSettings(formData) { this.toggleSubmitBtnState(true); - $.ajax({ - type: 'PUT', - url: this.testEndPoint, - data: formData, - }) - .done((res) => { - if (res.error) { - new Flash(`${res.message} ${res.service_response}`, 'alert', document, { - title: 'Save anyway', - clickHandler: (e) => { - e.preventDefault(); - this.$form.submit(); - }, - }); - } else { - this.$form.submit(); - } - }) - .fail(() => { - new Flash('Something went wrong on our end.'); - }) - .always(() => { - this.toggleSubmitBtnState(false); - }); + + return axios.put(this.testEndPoint, formData) + .then(({ data }) => { + if (data.error) { + flash(`${data.message} ${data.service_response}`, 'alert', document, { + title: 'Save anyway', + clickHandler: (e) => { + e.preventDefault(); + this.$form.submit(); + }, + }); + } else { + this.$form.submit(); + } + + this.toggleSubmitBtnState(false); + }) + .catch(() => { + flash('Something went wrong on our end.'); + this.toggleSubmitBtnState(false); + }); } } diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js index 2203a56315e..14a2bfbe4e0 100644 --- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js +++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js @@ -11,6 +11,14 @@ class AutoWidthDropdownSelect { const dropdownClass = this.dropdownClass; this.$selectElement.select2({ dropdownCssClass: dropdownClass, + ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass), + }); + + return this; + } + + static selectOptions(dropdownClass) { + return { dropdownCss() { let resultantWidth = 'auto'; const $dropdown = $(`.${dropdownClass}`); @@ -29,9 +37,7 @@ class AutoWidthDropdownSelect { maxWidth: offsetParentWidth, }; }, - }); - - return this; + }; } } diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index b124fafec70..8c1b2e78ca4 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -1,5 +1,6 @@ /* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ import _ from 'underscore'; +import axios from './lib/utils/axios_utils'; import Flash from './flash'; export default { @@ -22,15 +23,9 @@ export default { }, submit() { - const _this = this; - const xhr = $.ajax({ - url: this.form.attr('action'), - method: this.form.attr('method'), - dataType: 'JSON', - data: this.getFormDataAsObject() - }); - xhr.done(() => window.location.reload()); - xhr.fail(() => this.onFormSubmitFailure()); + axios[this.form.attr('method')](this.form.attr('action'), this.getFormDataAsObject()) + .then(() => window.location.reload()) + .catch(() => this.onFormSubmitFailure()); }, onFormSubmitFailure() { diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index bf77b93b643..2056efe701b 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -1,8 +1,7 @@ /* eslint-disable class-methods-use-this, no-new */ -/* global MilestoneSelect */ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; -import './milestone_select'; +import MilestoneSelect from './milestone_select'; import issueStatusSelect from './issue_status_select'; import subscriptionSelect from './subscription_select'; import LabelsSelect from './labels_select'; diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 57dcaa0e1ac..fdfad0b6a4f 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -6,6 +6,7 @@ import Autosave from './autosave'; import UsersSelect from './users_select'; import GfmAutoComplete from './gfm_auto_complete'; import ZenMode from './zen_mode'; +import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; export default class IssuableForm { @@ -46,6 +47,12 @@ export default class IssuableForm { }); calendar.setDate(parsePikadayDate($issuableDueDate.val())); } + + this.$targetBranchSelect = $('.js-target-branch-select', this.form); + + if (this.$targetBranchSelect.length) { + this.initTargetBranchDropdown(); + } } initAutosave() { @@ -104,4 +111,37 @@ export default class IssuableForm { addWip() { this.titleField.val(`WIP: ${(this.titleField.val())}`); } + + initTargetBranchDropdown() { + this.$targetBranchSelect.select2({ + ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'), + ajax: { + url: this.$targetBranchSelect.data('endpoint'), + dataType: 'JSON', + quietMillis: 250, + data(search) { + return { + search, + }; + }, + results(data) { + return { + // `data` keys are translated so we can't just access them with a string based key + results: data[Object.keys(data)[0]].map(name => ({ + id: name, + text: name, + })), + }; + }, + }, + initSelection(el, callback) { + const val = el.val(); + + callback({ + id: val, + text: val, + }); + }, + }); + } } diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js index c3e0acdff66..0683ca82a38 100644 --- a/app/assets/javascripts/issuable_index.js +++ b/app/assets/javascripts/issuable_index.js @@ -1,3 +1,6 @@ +import axios from './lib/utils/axios_utils'; +import flash from './flash'; +import { __ } from './locale'; import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; @@ -20,23 +23,24 @@ export default class IssuableIndex { } static resetIncomingEmailToken() { - $('.incoming-email-token-reset').on('click', (e) => { + const $resetToken = $('.incoming-email-token-reset'); + + $resetToken.on('click', (e) => { e.preventDefault(); - $.ajax({ - type: 'PUT', - url: $('.incoming-email-token-reset').attr('href'), - dataType: 'json', - success(response) { - $('#issuable_email').val(response.new_address).focus(); - }, - beforeSend() { - $('.incoming-email-token-reset').text('resetting...'); - }, - complete() { - $('.incoming-email-token-reset').text('reset it'); - }, - }); + $resetToken.text('resetting...'); + + axios.put($resetToken.attr('href')) + .then(({ data }) => { + $('#issuable_email').val(data.new_address).focus(); + + $resetToken.text('reset it'); + }) + .catch(() => { + flash(__('There was an error when reseting email token.')); + + $resetToken.text('reset it'); + }); }); } } diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 411c820cc43..ff65ea99e9a 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,7 +1,8 @@ /* 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'; +import flash from './flash'; import TaskList from './task_list'; import CreateMergeRequestDropdown from './create_merge_request_dropdown'; import IssuablesHelper from './helpers/issuables_helper'; @@ -42,12 +43,8 @@ export default class Issue { this.disableCloseReopenButton($button); url = $button.attr('href'); - return $.ajax({ - type: 'PUT', - url: url - }) - .fail(() => new Flash(issueFailMessage)) - .done((data) => { + return axios.put(url) + .then(({ data }) => { const isClosedBadge = $('div.status-box-issue-closed'); const isOpenBadge = $('div.status-box-open'); const projectIssuesCounter = $('.issue_counter'); @@ -74,9 +71,10 @@ export default class Issue { } } } else { - new Flash(issueFailMessage); + flash(issueFailMessage); } }) + .catch(() => flash(issueFailMessage)) .then(() => { this.disableCloseReopenButton($button, false); }); @@ -115,24 +113,22 @@ export default class Issue { static initMergeRequests() { var $container; $container = $('#merge-requests'); - return $.getJSON($container.data('url')).fail(function() { - return new Flash('Failed to load referenced merge requests'); - }).done(function(data) { - if ('html' in data) { - return $container.html(data.html); - } - }); + return axios.get($container.data('url')) + .then(({ data }) => { + if ('html' in data) { + $container.html(data.html); + } + }).catch(() => flash('Failed to load referenced merge requests')); } static initRelatedBranches() { var $container; $container = $('#related-branches'); - return $.getJSON($container.data('url')).fail(function() { - return new Flash('Failed to load related branches'); - }).done(function(data) { - if ('html' in data) { - return $container.html(data.html); - } - }); + return axios.get($container.data('url')) + .then(({ data }) => { + if ('html' in data) { + $container.html(data.html); + } + }).catch(() => flash('Failed to load related branches')); } } diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 952f49d522e..e87a8ed7fea 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -1,308 +1,324 @@ <script> -import Visibility from 'visibilityjs'; -import { visitUrl } from '../../lib/utils/url_utility'; -import Poll from '../../lib/utils/poll'; -import eventHub from '../event_hub'; -import Service from '../services/index'; -import Store from '../stores'; -import titleComponent from './title.vue'; -import descriptionComponent from './description.vue'; -import editedComponent from './edited.vue'; -import formComponent from './form.vue'; -import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; + import Visibility from 'visibilityjs'; + import { visitUrl } from '../../lib/utils/url_utility'; + import Poll from '../../lib/utils/poll'; + import eventHub from '../event_hub'; + import Service from '../services/index'; + import Store from '../stores'; + import titleComponent from './title.vue'; + import descriptionComponent from './description.vue'; + import editedComponent from './edited.vue'; + import formComponent from './form.vue'; + import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; -export default { - props: { - endpoint: { - required: true, - type: String, + export default { + components: { + descriptionComponent, + titleComponent, + editedComponent, + formComponent, }, - updateEndpoint: { - required: true, - type: String, - }, - canUpdate: { - required: true, - type: Boolean, - }, - canDestroy: { - required: true, - type: Boolean, - }, - showInlineEditButton: { - type: Boolean, - required: false, - default: true, - }, - showDeleteButton: { - type: Boolean, - required: false, - default: true, - }, - enableAutocomplete: { - type: Boolean, - required: false, - default: true, - }, - issuableRef: { - type: String, - required: true, - }, - initialTitleHtml: { - type: String, - required: true, - }, - initialTitleText: { - type: String, - required: true, - }, - initialDescriptionHtml: { - type: String, - required: false, - default: '', - }, - initialDescriptionText: { - type: String, - required: false, - default: '', - }, - initialTaskStatus: { - type: String, - required: false, - default: '', - }, - updatedAt: { - type: String, - required: false, - default: '', - }, - updatedByName: { - type: String, - required: false, - default: '', - }, - updatedByPath: { - type: String, - required: false, - default: '', - }, - issuableTemplates: { - type: Array, - required: false, - default: () => [], - }, - markdownPreviewPath: { - type: String, - required: true, - }, - markdownDocsPath: { - type: String, - required: true, - }, - projectPath: { - type: String, - required: true, - }, - projectNamespace: { - type: String, - required: true, - }, - issuableType: { - type: String, - required: false, - default: 'issue', - }, - canAttachFile: { - type: Boolean, - required: false, - default: true, + mixins: [ + recaptchaModalImplementor, + ], + props: { + endpoint: { + required: true, + type: String, + }, + updateEndpoint: { + required: true, + type: String, + }, + canUpdate: { + required: true, + type: Boolean, + }, + canDestroy: { + required: true, + type: Boolean, + }, + showInlineEditButton: { + type: Boolean, + required: false, + default: true, + }, + showDeleteButton: { + type: Boolean, + required: false, + default: true, + }, + enableAutocomplete: { + type: Boolean, + required: false, + default: true, + }, + issuableRef: { + type: String, + required: true, + }, + initialTitleHtml: { + type: String, + required: true, + }, + initialTitleText: { + type: String, + required: true, + }, + initialDescriptionHtml: { + type: String, + required: false, + default: '', + }, + initialDescriptionText: { + type: String, + required: false, + default: '', + }, + initialTaskStatus: { + type: String, + required: false, + default: '', + }, + updatedAt: { + type: String, + required: false, + default: '', + }, + updatedByName: { + type: String, + required: false, + default: '', + }, + updatedByPath: { + type: String, + required: false, + default: '', + }, + issuableTemplates: { + type: Array, + required: false, + default: () => [], + }, + markdownPreviewPath: { + type: String, + required: true, + }, + markdownDocsPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + projectNamespace: { + type: String, + required: true, + }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, }, - }, - data() { - const store = new Store({ - titleHtml: this.initialTitleHtml, - titleText: this.initialTitleText, - descriptionHtml: this.initialDescriptionHtml, - descriptionText: this.initialDescriptionText, - updatedAt: this.updatedAt, - updatedByName: this.updatedByName, - updatedByPath: this.updatedByPath, - taskStatus: this.initialTaskStatus, - }); + data() { + const store = new Store({ + titleHtml: this.initialTitleHtml, + titleText: this.initialTitleText, + descriptionHtml: this.initialDescriptionHtml, + descriptionText: this.initialDescriptionText, + updatedAt: this.updatedAt, + updatedByName: this.updatedByName, + updatedByPath: this.updatedByPath, + taskStatus: this.initialTaskStatus, + }); - return { - store, - state: store.state, - showForm: false, - }; - }, - computed: { - formState() { - return this.store.formState; + return { + store, + state: store.state, + showForm: false, + }; }, - hasUpdated() { - return !!this.state.updatedAt; + computed: { + formState() { + return this.store.formState; + }, + hasUpdated() { + return !!this.state.updatedAt; + }, + issueChanged() { + const descriptionChanged = + this.initialDescriptionText !== this.store.formState.description; + const titleChanged = + this.initialTitleText !== this.store.formState.title; + return descriptionChanged || titleChanged; + }, }, - }, - components: { - descriptionComponent, - titleComponent, - editedComponent, - formComponent, - }, - - mixins: [ - recaptchaModalImplementor, - ], + created() { + this.service = new Service(this.endpoint); + this.poll = new Poll({ + resource: this.service, + method: 'getData', + successCallback: res => this.store.updateState(res.data), + errorCallback(err) { + throw new Error(err); + }, + }); - methods: { - openForm() { - if (!this.showForm) { - this.showForm = true; - this.store.setFormState({ - title: this.state.titleText, - description: this.state.descriptionText, - lockedWarningVisible: false, - updateLoading: false, - }); + if (!Visibility.hidden()) { + this.poll.makeRequest(); } - }, - closeForm() { - this.showForm = false; - }, - updateIssuable() { - this.service.updateIssuable(this.store.formState) - .then(res => res.json()) - .then(data => this.checkForSpam(data)) - .then((data) => { - if (location.pathname !== data.web_url) { - visitUrl(data.web_url); - } + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); - return this.service.getData(); - }) - .then(res => res.json()) - .then((data) => { - this.store.updateState(data); - eventHub.$emit('close.form'); - }) - .catch((error) => { - if (error && error.name === 'SpamError') { - this.openRecaptcha(); - } else { - eventHub.$emit('close.form'); - window.Flash(`Error updating ${this.issuableType}`); - } - }); + window.addEventListener('beforeunload', this.handleBeforeUnloadEvent); + + eventHub.$on('delete.issuable', this.deleteIssuable); + eventHub.$on('update.issuable', this.updateIssuable); + eventHub.$on('close.form', this.closeForm); + eventHub.$on('open.form', this.openForm); }, + beforeDestroy() { + eventHub.$off('delete.issuable', this.deleteIssuable); + eventHub.$off('update.issuable', this.updateIssuable); + eventHub.$off('close.form', this.closeForm); + eventHub.$off('open.form', this.openForm); + window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent); + }, + methods: { + handleBeforeUnloadEvent(e) { + const event = e; + if (this.showForm && this.issueChanged) { + event.returnValue = 'Are you sure you want to lose your issue information?'; + } + return undefined; + }, - closeRecaptchaModal() { - this.store.setFormState({ - updateLoading: false, - }); + openForm() { + if (!this.showForm) { + this.showForm = true; + this.store.setFormState({ + title: this.state.titleText, + description: this.state.descriptionText, + lockedWarningVisible: false, + updateLoading: false, + }); + } + }, + closeForm() { + this.showForm = false; + }, - this.closeRecaptcha(); - }, + updateIssuable() { + return this.service.updateIssuable(this.store.formState) + .then(res => res.data) + .then(data => this.checkForSpam(data)) + .then((data) => { + if (location.pathname !== data.web_url) { + visitUrl(data.web_url); + } - deleteIssuable() { - this.service.deleteIssuable() - .then(res => res.json()) - .then((data) => { - // Stop the poll so we don't get 404's with the issuable not existing - this.poll.stop(); + return this.service.getData(); + }) + .then(res => res.data) + .then((data) => { + this.store.updateState(data); + eventHub.$emit('close.form'); + }) + .catch((error) => { + if (error && error.name === 'SpamError') { + this.openRecaptcha(); + } else { + eventHub.$emit('close.form'); + window.Flash(`Error updating ${this.issuableType}`); + } + }); + }, - visitUrl(data.web_url); - }) - .catch(() => { - eventHub.$emit('close.form'); - window.Flash(`Error deleting ${this.issuableType}`); + closeRecaptchaModal() { + this.store.setFormState({ + updateLoading: false, }); - }, - }, - created() { - this.service = new Service(this.endpoint); - this.poll = new Poll({ - resource: this.service, - method: 'getData', - successCallback: res => res.json().then(data => this.store.updateState(data)), - errorCallback(err) { - throw new Error(err); - }, - }); - if (!Visibility.hidden()) { - this.poll.makeRequest(); - } + this.closeRecaptcha(); + }, - Visibility.change(() => { - if (!Visibility.hidden()) { - this.poll.restart(); - } else { - this.poll.stop(); - } - }); + deleteIssuable() { + this.service.deleteIssuable() + .then(res => res.data) + .then((data) => { + // Stop the poll so we don't get 404's with the issuable not existing + this.poll.stop(); - eventHub.$on('delete.issuable', this.deleteIssuable); - eventHub.$on('update.issuable', this.updateIssuable); - eventHub.$on('close.form', this.closeForm); - eventHub.$on('open.form', this.openForm); - }, - beforeDestroy() { - eventHub.$off('delete.issuable', this.deleteIssuable); - eventHub.$off('update.issuable', this.updateIssuable); - eventHub.$off('close.form', this.closeForm); - eventHub.$off('open.form', this.openForm); - }, -}; + visitUrl(data.web_url); + }) + .catch(() => { + eventHub.$emit('close.form'); + window.Flash(`Error deleting ${this.issuableType}`); + }); + }, + }, + }; </script> <template> -<div> - <div v-if="canUpdate && showForm"> - <form-component - :form-state="formState" - :can-destroy="canDestroy" - :issuable-templates="issuableTemplates" - :markdown-docs-path="markdownDocsPath" - :markdown-preview-path="markdownPreviewPath" - :project-path="projectPath" - :project-namespace="projectNamespace" - :show-delete-button="showDeleteButton" - :can-attach-file="canAttachFile" - :enable-autocomplete="enableAutocomplete" - /> + <div> + <div v-if="canUpdate && showForm"> + <form-component + :form-state="formState" + :can-destroy="canDestroy" + :issuable-templates="issuableTemplates" + :markdown-docs-path="markdownDocsPath" + :markdown-preview-path="markdownPreviewPath" + :project-path="projectPath" + :project-namespace="projectNamespace" + :show-delete-button="showDeleteButton" + :can-attach-file="canAttachFile" + :enable-autocomplete="enableAutocomplete" + /> - <recaptcha-modal - v-show="showRecaptcha" - :html="recaptchaHTML" - @close="closeRecaptchaModal" - /> - </div> - <div v-else> - <title-component - :issuable-ref="issuableRef" - :can-update="canUpdate" - :title-html="state.titleHtml" - :title-text="state.titleText" - :show-inline-edit-button="showInlineEditButton" - /> - <description-component - v-if="state.descriptionHtml" - :can-update="canUpdate" - :description-html="state.descriptionHtml" - :description-text="state.descriptionText" - :updated-at="state.updatedAt" - :task-status="state.taskStatus" - :issuable-type="issuableType" - :update-url="updateEndpoint" - /> - <edited-component - v-if="hasUpdated" - :updated-at="state.updatedAt" - :updated-by-name="state.updatedByName" - :updated-by-path="state.updatedByPath" - /> + <recaptcha-modal + v-show="showRecaptcha" + :html="recaptchaHTML" + @close="closeRecaptchaModal" + /> + </div> + <div v-else> + <title-component + :issuable-ref="issuableRef" + :can-update="canUpdate" + :title-html="state.titleHtml" + :title-text="state.titleText" + :show-inline-edit-button="showInlineEditButton" + /> + <description-component + v-if="state.descriptionHtml" + :can-update="canUpdate" + :description-html="state.descriptionHtml" + :description-text="state.descriptionText" + :updated-at="state.updatedAt" + :task-status="state.taskStatus" + :issuable-type="issuableType" + :update-url="updateEndpoint" + /> + <edited-component + v-if="hasUpdated" + :updated-at="state.updatedAt" + :updated-by-name="state.updatedByName" + :updated-by-path="state.updatedByPath" + /> + </div> </div> -</div> </template> diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index c3f2bf130bb..9afa9dea126 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -56,7 +56,10 @@ this.updateTaskStatusText(); }, }, - + mounted() { + this.renderGFM(); + this.updateTaskStatusText(); + }, methods: { renderGFM() { $(this.$refs['gfm-content']).renderGFM(); @@ -88,17 +91,17 @@ if (taskRegexMatches) { $tasks.text(this.taskStatus); - $tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`); + $tasksShort.text( + `${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? + 's' : + ''}`, + ); } else { $tasks.text(''); $tasksShort.text(''); } }, }, - mounted() { - this.renderGFM(); - this.updateTaskStatusText(); - }, }; </script> @@ -108,7 +111,8 @@ class="description" :class="{ 'js-task-list-container': canUpdate - }"> + }" + > <div class="wiki" :class="{ diff --git a/app/assets/javascripts/issue_show/components/edited.vue b/app/assets/javascripts/issue_show/components/edited.vue index 992b7064c13..01097b5b35e 100644 --- a/app/assets/javascripts/issue_show/components/edited.vue +++ b/app/assets/javascripts/issue_show/components/edited.vue @@ -1,33 +1,33 @@ <script> -import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; + import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; -export default { - props: { - updatedAt: { - type: String, - required: false, - default: '', + export default { + components: { + timeAgoTooltip, }, - updatedByName: { - type: String, - required: false, - default: '', + props: { + updatedAt: { + type: String, + required: false, + default: '', + }, + updatedByName: { + type: String, + required: false, + default: '', + }, + updatedByPath: { + type: String, + required: false, + default: '', + }, }, - updatedByPath: { - type: String, - required: false, - default: '', + computed: { + hasUpdatedBy() { + return this.updatedByName && this.updatedByPath; + }, }, - }, - components: { - timeAgoTooltip, - }, - computed: { - hasUpdatedBy() { - return this.updatedByName && this.updatedByPath; - }, - }, -}; + }; </script> <template> @@ -48,7 +48,7 @@ export default { class="author_link" :href="updatedByPath" > - <span>{{updatedByName}}</span> + <span>{{ updatedByName }}</span> </a> </span> </small> diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 4e577546551..d9fa2764d65 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -3,6 +3,9 @@ import markdownField from '../../../vue_shared/components/markdown/field.vue'; export default { + components: { + markdownField, + }, mixins: [updateMixin], props: { formState: { @@ -28,9 +31,6 @@ default: true, }, }, - components: { - markdownField, - }, mounted() { this.$refs.textarea.focus(); }, diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue index 0fa19022336..779705e19ac 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -6,6 +6,13 @@ import descriptionTemplate from './fields/description_template.vue'; export default { + components: { + lockedWarning, + titleField, + descriptionField, + descriptionTemplate, + editActions, + }, props: { canDestroy: { type: Boolean, @@ -52,13 +59,6 @@ default: true, }, }, - components: { - lockedWarning, - titleField, - descriptionField, - descriptionTemplate, - editActions, - }, computed: { hasIssuableTemplates() { return this.issuableTemplates.length; @@ -78,16 +78,19 @@ :form-state="formState" :issuable-templates="issuableTemplates" :project-path="projectPath" - :project-namespace="projectNamespace" /> + :project-namespace="projectNamespace" + /> </div> <div :class="{ 'col-sm-8 col-lg-9': hasIssuableTemplates, 'col-xs-12': !hasIssuableTemplates, - }"> + }" + > <title-field :form-state="formState" - :issuable-templates="issuableTemplates" /> + :issuable-templates="issuableTemplates" + /> </div> </div> <description-field @@ -100,6 +103,7 @@ <edit-actions :form-state="formState" :can-destroy="canDestroy" - :show-delete-button="showDeleteButton" /> + :show-delete-button="showDeleteButton" + /> </form> </template> diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue index b7e6eadd440..aec890a2ff6 100644 --- a/app/assets/javascripts/issue_show/components/title.vue +++ b/app/assets/javascripts/issue_show/components/title.vue @@ -5,14 +5,10 @@ import { spriteIcon } from '../../lib/utils/common_utils'; export default { - mixins: [animateMixin], - data() { - return { - preAnimation: false, - pulseAnimation: false, - titleEl: document.querySelector('title'), - }; + directives: { + tooltip, }, + mixins: [animateMixin], props: { issuableRef: { type: String, @@ -37,8 +33,17 @@ default: false, }, }, - directives: { - tooltip, + data() { + return { + preAnimation: false, + pulseAnimation: false, + titleEl: document.querySelector('title'), + }; + }, + computed: { + pencilIcon() { + return spriteIcon('pencil', 'link-highlight'); + }, }, watch: { titleHtml() { @@ -46,11 +51,6 @@ this.animateChange(); }, }, - computed: { - pencilIcon() { - return spriteIcon('pencil', 'link-highlight'); - }, - }, methods: { setPageTitle() { const currentPageTitleScope = this.titleEl.innerText.split('·'); @@ -85,7 +85,7 @@ data-placement="bottom" data-container="body" @click="edit" - > + > </button> </div> </template> diff --git a/app/assets/javascripts/issue_show/services/index.js b/app/assets/javascripts/issue_show/services/index.js index 6f0fd0b1768..9546eb22c27 100644 --- a/app/assets/javascripts/issue_show/services/index.js +++ b/app/assets/javascripts/issue_show/services/index.js @@ -1,29 +1,20 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; - -Vue.use(VueResource); +import axios from '../../lib/utils/axios_utils'; export default class Service { constructor(endpoint) { - this.endpoint = endpoint; - - this.resource = Vue.resource(`${this.endpoint}.json`, {}, { - realtimeChanges: { - method: 'GET', - url: `${this.endpoint}/realtime_changes`, - }, - }); + this.endpoint = `${endpoint}.json`; + this.realtimeEndpoint = `${endpoint}/realtime_changes`; } getData() { - return this.resource.realtimeChanges(); + return axios.get(this.realtimeEndpoint); } deleteIssuable() { - return this.resource.delete(); + return axios.delete(this.endpoint); } updateIssuable(data) { - return this.resource.update(data); + return axios.put(this.endpoint, data); } } diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index 198a7823381..d0b7ea75082 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -1,14 +1,15 @@ import _ from 'underscore'; +import axios from './lib/utils/axios_utils'; import { visitUrl } from './lib/utils/url_utility'; import bp from './breakpoints'; -import { bytesToKiB } from './lib/utils/number_utils'; +import { numberToHumanSize } from './lib/utils/number_utils'; import { setCiStatusFavicon } from './lib/utils/common_utils'; -import { timeFor } from './lib/utils/datetime_utility'; export default class Job { constructor(options) { this.timeout = null; this.state = null; + this.fetchingStatusFavicon = false; this.options = options || $('.js-build-options').data(); this.pagePath = this.options.pagePath; @@ -71,7 +72,6 @@ export default class Job { .off('resize.build') .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100)); - this.updateArtifactRemoveDate(); this.initAffixTopArea(); this.getBuildTrace(); @@ -96,14 +96,15 @@ export default class Job { // eslint-disable-next-line class-methods-use-this canScroll() { - return this.$document.height() > this.$window.height(); + return $(document).height() > $(window).height(); } toggleScroll() { - const currentPosition = this.$document.scrollTop(); - const scrollHeight = this.$document.height(); + const $document = $(document); + const currentPosition = $document.scrollTop(); + const scrollHeight = $document.height(); - const windowHeight = this.$window.height(); + const windowHeight = $(window).height(); if (this.canScroll()) { if (currentPosition > 0 && (scrollHeight - currentPosition !== windowHeight)) { @@ -127,18 +128,22 @@ export default class Job { this.toggleDisableButton(this.$scrollBottomBtn, true); } } - + // eslint-disable-next-line class-methods-use-this isScrolledToBottom() { - const currentPosition = this.$document.scrollTop(); - const scrollHeight = this.$document.height(); + const $document = $(document); + + const currentPosition = $document.scrollTop(); + const scrollHeight = $document.height(); + + const windowHeight = $(window).height(); - const windowHeight = this.$window.height(); return scrollHeight - currentPosition === windowHeight; } // eslint-disable-next-line class-methods-use-this scrollDown() { - this.$document.scrollTop(this.$document.height()); + const $document = $(document); + $document.scrollTop($document.height()); } scrollToBottom() { @@ -148,7 +153,7 @@ export default class Job { } scrollToTop() { - this.$document.scrollTop(0); + $(document).scrollTop(0); this.hasBeenScrolled = true; this.toggleScroll(); } @@ -168,12 +173,23 @@ export default class Job { } getBuildTrace() { - return $.ajax({ - url: `${this.pagePath}/trace.json`, - data: { state: this.state }, + return axios.get(`${this.pagePath}/trace.json`, { + params: { state: this.state }, }) - .done((log) => { - setCiStatusFavicon(`${this.pagePath}/status.json`); + .then((res) => { + const log = res.data; + + if (!this.fetchingStatusFavicon) { + this.fetchingStatusFavicon = true; + + setCiStatusFavicon(`${this.pagePath}/status.json`) + .then(() => { + this.fetchingStatusFavicon = false; + }) + .catch(() => { + this.fetchingStatusFavicon = false; + }); + } if (log.state) { this.state = log.state; @@ -193,7 +209,7 @@ export default class Job { // we need to show a message warning the user about that. if (this.logBytes < log.total) { // size is in bytes, we need to calculate KiB - const size = bytesToKiB(this.logBytes); + const size = numberToHumanSize(this.logBytes); $('.js-truncated-info-size').html(`${size}`); this.$truncatedInfo.removeClass('hidden'); } else { @@ -214,7 +230,7 @@ export default class Job { visitUrl(this.pagePath); } }) - .fail(() => { + .catch(() => { this.$buildRefreshAnimation.remove(); }) .then(() => { @@ -256,16 +272,7 @@ export default class Job { sidebarOnClick() { if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); } - // eslint-disable-next-line class-methods-use-this, consistent-return - updateArtifactRemoveDate() { - const $date = $('.js-artifacts-remove'); - if ($date.length) { - const date = $date.text(); - return $date.text( - timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3'))), - ); - } - } + // eslint-disable-next-line class-methods-use-this populateJobs(stage) { $('.build-job').hide(); diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue index 6d671845f8e..357bc9aab17 100644 --- a/app/assets/javascripts/jobs/components/header.vue +++ b/app/assets/javascripts/jobs/components/header.vue @@ -3,7 +3,11 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { - name: 'jobHeaderSection', + name: 'JobHeaderSection', + components: { + ciHeader, + loadingIcon, + }, props: { job: { type: Object, @@ -14,10 +18,6 @@ required: true, }, }, - components: { - ciHeader, - loadingIcon, - }, data() { return { actions: this.getActions(), @@ -30,6 +30,18 @@ shouldRenderContent() { return !this.isLoading && Object.keys(this.job).length; }, + /** + * When job has not started the key will be `false` + * When job started the key will be a string with a date. + */ + jobStarted() { + return !this.job.started === false; + }, + }, + watch: { + job() { + this.actions = this.getActions(); + }, }, methods: { getActions() { @@ -46,11 +58,6 @@ return actions; }, }, - watch: { - job() { - this.actions = this.getActions(); - }, - }, }; </script> <template> @@ -63,11 +70,13 @@ :time="job.created_at" :user="job.user" :actions="actions" - :hasSidebarButton="true" - /> + :has-sidebar-button="true" + :should-render-triggered-label="jobStarted" + /> <loading-icon v-if="isLoading" size="2" - /> + class="prepend-top-default append-bottom-default" + /> </div> </template> diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue index ab2bcd728a8..a6819aaeb12 100644 --- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue +++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue @@ -23,9 +23,10 @@ <p class="build-detail-row"> <span v-if="hasTitle" - class="build-light-text"> - {{title}}: + class="build-light-text" + > + {{ title }}: </span> - {{value}} + {{ value }} </p> </template> diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue index d0145fed396..56814a52525 100644 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -6,6 +6,13 @@ export default { name: 'SidebarDetailsBlock', + components: { + detailRow, + loadingIcon, + }, + mixins: [ + timeagoMixin, + ], props: { job: { type: Object, @@ -16,13 +23,6 @@ required: true, }, }, - mixins: [ - timeagoMixin, - ], - components: { - detailRow, - loadingIcon, - }, computed: { shouldRenderContent() { return !this.isLoading && Object.keys(this.job).length > 0; @@ -58,11 +58,13 @@ <template v-if="shouldRenderContent"> <div class="block retry-link" - v-if="job.retry_path || job.new_issue_path"> + v-if="job.retry_path || job.new_issue_path" + > <a v-if="job.new_issue_path" class="js-new-issue btn btn-new btn-inverted" - :href="job.new_issue_path"> + :href="job.new_issue_path" + > New issue </a> <a @@ -70,20 +72,21 @@ class="js-retry-job btn btn-inverted-secondary" :href="job.retry_path" data-method="post" - rel="nofollow"> + rel="nofollow" + > Retry </a> </div> <div :class="{block : renderBlock }"> <p class="build-detail-row js-job-mr" - v-if="job.merge_request"> - <span - class="build-light-text"> + v-if="job.merge_request" + > + <span class="build-light-text"> Merge Request: </span> <a :href="job.merge_request.path"> - !{{job.merge_request.iid}} + !{{ job.merge_request.iid }} </a> </p> @@ -92,49 +95,49 @@ v-if="job.duration" title="Duration" :value="duration" - /> + /> <detail-row class="js-job-finished" v-if="job.finished_at" title="Finished" :value="timeFormated(job.finished_at)" - /> + /> <detail-row class="js-job-erased" v-if="job.erased_at" title="Erased" :value="timeFormated(job.erased_at)" - /> + /> <detail-row class="js-job-queued" v-if="job.queued" title="Queued" :value="queued" - /> + /> <detail-row class="js-job-runner" v-if="job.runner" title="Runner" :value="runnerId" - /> + /> <detail-row class="js-job-coverage" v-if="job.coverage" title="Coverage" :value="coverage" - /> + /> <p class="build-detail-row js-job-tags" - v-if="job.tags.length"> - <span - class="build-light-text"> + v-if="job.tags.length" + > + <span class="build-light-text"> Tags: </span> <span - v-for="tag in job.tags" - key="tag" + v-for="(tag, i) in job.tags" + :key="i" class="label label-primary"> - {{tag}} + {{ tag }} </span> </p> @@ -146,7 +149,8 @@ class="js-cancel-job btn btn-sm btn-default" :href="job.cancel_path" data-method="post" - rel="nofollow"> + rel="nofollow" + > Cancel </a> </div> @@ -156,6 +160,6 @@ class="prepend-top-10" v-if="isLoading" size="2" - /> + /> </div> </template> diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index baaf5641200..db53b04de0e 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -13,14 +13,14 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line no-new new Vue({ el: '#js-build-header-vue', + components: { + jobHeader, + }, data() { return { mediator, }; }, - components: { - jobHeader, - }, mounted() { this.mediator.initBuildClass(); }, @@ -38,14 +38,14 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line new Vue({ el: '#js-details-block-vue', + components: { + detailsBlock, + }, data() { return { mediator, }; }, - components: { - detailsBlock, - }, render(createElement) { return createElement('details-block', { props: { diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index c929dc98c10..61b40f79db1 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -1,7 +1,8 @@ /* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */ -/* global Sortable */ +import Sortable from 'vendor/Sortable'; -import Flash from './flash'; +import flash from './flash'; +import axios from './lib/utils/axios_utils'; export default class LabelManager { constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { @@ -50,11 +51,12 @@ export default class LabelManager { if (persistState == null) { persistState = true; } - let xhr; const _this = this; const url = $label.find('.js-toggle-priority').data('url'); let $target = this.prioritizedLabels; let $from = this.otherLabels; + const rollbackLabelPosition = this.rollbackLabelPosition.bind(this, $label, action); + if (action === 'remove') { $target = this.otherLabels; $from = this.prioritizedLabels; @@ -71,40 +73,34 @@ export default class LabelManager { return; } if (action === 'remove') { - xhr = $.ajax({ - url, - type: 'DELETE' - }); + axios.delete(url) + .catch(rollbackLabelPosition); + // Restore empty message if (!$from.find('li').length) { $from.find('.empty-message').removeClass('hidden'); } } else { - xhr = this.savePrioritySort($label, action); + this.savePrioritySort($label, action) + .catch(rollbackLabelPosition); } - return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); } onPrioritySortUpdate() { - const xhr = this.savePrioritySort(); - return xhr.fail(function() { - return new Flash(this.errorMessage, 'alert'); - }); + this.savePrioritySort() + .catch(() => flash(this.errorMessage)); } savePrioritySort() { - return $.post({ - url: this.prioritizedLabels.data('url'), - data: { - label_ids: this.getSortedLabelsIds() - } + return axios.post(this.prioritizedLabels.data('url'), { + label_ids: this.getSortedLabelsIds(), }); } rollbackLabelPosition($label, originalAction) { const action = originalAction === 'remove' ? 'add' : 'remove'; this.toggleLabelPriority($label, action, false); - return new Flash(this.errorMessage, 'alert'); + flash(this.errorMessage); } getSortedLabelsIds() { diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index f7a1c9f1e40..5ecf81ad11d 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -2,9 +2,12 @@ /* global Issuable */ /* global ListLabel */ import _ from 'underscore'; +import { __ } from './locale'; +import axios from './lib/utils/axios_utils'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import DropdownUtils from './filtered_search/dropdown_utils'; import CreateLabelDropdown from './create_label'; +import flash from './flash'; export default class LabelsSelect { constructor(els, options = {}) { @@ -82,99 +85,96 @@ export default class LabelsSelect { } $loading.removeClass('hidden').fadeIn(); $dropdown.trigger('loading.gl.dropdown'); - return $.ajax({ - type: 'PUT', - url: issueUpdateURL, - dataType: 'JSON', - data: data - }).done(function(data) { - var labelCount, template, labelTooltipTitle, labelTitles; - $loading.fadeOut(); - $dropdown.trigger('loaded.gl.dropdown'); - $selectbox.hide(); - data.issueURLSplit = issueURLSplit; - labelCount = 0; - if (data.labels.length) { - template = labelHTMLTemplate(data); - labelCount = data.labels.length; - } - else { - template = labelNoneHTMLTemplate; - } - $value.removeAttr('style').html(template); - $sidebarCollapsedValue.text(labelCount); + axios.put(issueUpdateURL, data) + .then(({ data }) => { + var labelCount, template, labelTooltipTitle, labelTitles; + $loading.fadeOut(); + $dropdown.trigger('loaded.gl.dropdown'); + $selectbox.hide(); + data.issueURLSplit = issueURLSplit; + labelCount = 0; + if (data.labels.length) { + template = labelHTMLTemplate(data); + labelCount = data.labels.length; + } + else { + template = labelNoneHTMLTemplate; + } + $value.removeAttr('style').html(template); + $sidebarCollapsedValue.text(labelCount); - if (data.labels.length) { - labelTitles = data.labels.map(function(label) { - return label.title; - }); + if (data.labels.length) { + labelTitles = data.labels.map(function(label) { + return label.title; + }); - if (labelTitles.length > 5) { - labelTitles = labelTitles.slice(0, 5); - labelTitles.push('and ' + (data.labels.length - 5) + ' more'); - } + if (labelTitles.length > 5) { + labelTitles = labelTitles.slice(0, 5); + labelTitles.push('and ' + (data.labels.length - 5) + ' more'); + } - labelTooltipTitle = labelTitles.join(', '); - } - else { - labelTooltipTitle = ''; - $sidebarLabelTooltip.tooltip('destroy'); - } + labelTooltipTitle = labelTitles.join(', '); + } + else { + labelTooltipTitle = ''; + $sidebarLabelTooltip.tooltip('destroy'); + } - $sidebarLabelTooltip - .attr('title', labelTooltipTitle) - .tooltip('fixTitle'); + $sidebarLabelTooltip + .attr('title', labelTooltipTitle) + .tooltip('fixTitle'); - $('.has-tooltip', $value).tooltip({ - container: 'body' - }); - }); + $('.has-tooltip', $value).tooltip({ + container: 'body' + }); + }) + .catch(() => flash(__('Error saving label update.'))); }; $dropdown.glDropdown({ showMenuAbove: showMenuAbove, data: function(term, callback) { - return $.ajax({ - url: labelUrl - }).done(function(data) { - data = _.chain(data).groupBy(function(label) { - return label.title; - }).map(function(label) { - var color; - color = _.map(label, function(dup) { - return dup.color; - }); - return { - id: label[0].id, - title: label[0].title, - color: color, - duplicate: color.length > 1 - }; - }).value(); - if ($dropdown.hasClass('js-extra-options')) { - var extraData = []; - if (showNo) { - extraData.unshift({ - id: 0, - title: 'No Label' + axios.get(labelUrl) + .then((res) => { + let data = _.chain(res.data).groupBy(function(label) { + return label.title; + }).map(function(label) { + var color; + color = _.map(label, function(dup) { + return dup.color; }); + return { + id: label[0].id, + title: label[0].title, + color: color, + duplicate: color.length > 1 + }; + }).value(); + if ($dropdown.hasClass('js-extra-options')) { + var extraData = []; + if (showNo) { + extraData.unshift({ + id: 0, + title: 'No Label' + }); + } + if (showAny) { + extraData.unshift({ + isAny: true, + title: 'Any Label' + }); + } + if (extraData.length) { + extraData.push('divider'); + data = extraData.concat(data); + } } - if (showAny) { - extraData.unshift({ - isAny: true, - title: 'Any Label' - }); - } - if (extraData.length) { - extraData.push('divider'); - data = extraData.concat(data); - } - } - callback(data); - if (showMenuAbove) { - $dropdown.data('glDropdown').positionMenuAbove(); - } - }); + callback(data); + if (showMenuAbove) { + $dropdown.data('glDropdown').positionMenuAbove(); + } + }) + .catch(() => flash(__('Error fetching labels.'))); }, renderRow: function(label, instance) { var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue; @@ -231,7 +231,7 @@ export default class LabelsSelect { selectedClass.push('label-item'); $a.attr('data-label-id', label.id); } - $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title); + $a.addClass(selectedClass.join(' ')).html(`${colorEl} ${_.escape(label.title)}`); // Return generated html return $li.html($a).prop('outerHTML'); }, diff --git a/app/assets/javascripts/lib/utils/ajax_cache.js b/app/assets/javascripts/lib/utils/ajax_cache.js index 629d8f44e18..616d8952ada 100644 --- a/app/assets/javascripts/lib/utils/ajax_cache.js +++ b/app/assets/javascripts/lib/utils/ajax_cache.js @@ -1,3 +1,4 @@ +import axios from './axios_utils'; import Cache from './cache'; class AjaxCache extends Cache { @@ -18,25 +19,18 @@ class AjaxCache extends Cache { let pendingRequest = this.pendingRequests[endpoint]; if (!pendingRequest) { - pendingRequest = new Promise((resolve, reject) => { - // jQuery 2 is not Promises/A+ compatible (missing catch) - $.ajax(endpoint) // eslint-disable-line promise/catch-or-return - .then(data => resolve(data), - (jqXHR, textStatus, errorThrown) => { - const error = new Error(`${endpoint}: ${errorThrown}`); - error.textStatus = textStatus; - reject(error); - }, - ); - }) - .then((data) => { - this.internalStorage[endpoint] = data; - delete this.pendingRequests[endpoint]; - }) - .catch((error) => { - delete this.pendingRequests[endpoint]; - throw error; - }); + pendingRequest = axios.get(endpoint) + .then(({ data }) => { + this.internalStorage[endpoint] = data; + delete this.pendingRequests[endpoint]; + }) + .catch((e) => { + const error = new Error(`${endpoint}: ${e.message}`); + error.textStatus = e.message; + + delete this.pendingRequests[endpoint]; + throw error; + }); this.pendingRequests[endpoint] = pendingRequest; } diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js index 7aeeca3b283..792871e2ecf 100644 --- a/app/assets/javascripts/lib/utils/axios_utils.js +++ b/app/assets/javascripts/lib/utils/axios_utils.js @@ -2,6 +2,8 @@ import axios from 'axios'; import csrf from './csrf'; axios.defaults.headers.common[csrf.headerKey] = csrf.token; +// Used by Rails to check if it is a valid XHR request +axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; // Maintain a global counter for active requests // see: spec/support/wait_for_requests.rb @@ -17,6 +19,18 @@ axios.interceptors.response.use((config) => { window.activeVueResources -= 1; return config; +}, (e) => { + window.activeVueResources -= 1; + + return Promise.reject(e); }); export default axios; + +/** + * @return The adapter that axios uses for dispatching requests. This may be overwritten in tests. + * + * @see https://github.com/axios/axios/tree/master/lib/adapters + * @see https://github.com/ctimmerm/axios-mock-adapter/blob/v1.12.0/src/index.js#L39 + */ +export const getDefaultAdapter = () => axios.defaults.adapter; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index b5328c77b25..5811d059e0b 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,3 +1,4 @@ +import axios from './axios_utils'; import { getLocationHash } from './url_utility'; export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index]; @@ -27,16 +28,11 @@ export const isInIssuePage = () => { return page === 'issues' && action === 'show'; }; -export const ajaxGet = url => $.ajax({ - type: 'GET', - url, - dataType: 'script', -}); - -export const ajaxPost = (url, data) => $.ajax({ - type: 'POST', - url, - data, +export const ajaxGet = url => axios.get(url, { + params: { format: 'js' }, + responseType: 'text', +}).then(({ data }) => { + $.globalEval(data); }); export const rstrip = (val) => { @@ -232,7 +228,7 @@ export const nodeMatchesSelector = (node, selector) => { export const normalizeHeaders = (headers) => { const upperCaseHeaders = {}; - Object.keys(headers).forEach((e) => { + Object.keys(headers || {}).forEach((e) => { upperCaseHeaders[e.toUpperCase()] = headers[e]; }); @@ -382,22 +378,16 @@ export const resetFavicon = () => { } }; -export const setCiStatusFavicon = (pageUrl) => { - $.ajax({ - url: pageUrl, - dataType: 'json', - success: (data) => { +export const setCiStatusFavicon = pageUrl => + axios.get(pageUrl) + .then(({ data }) => { if (data && data.favicon) { setFavicon(data.favicon); } else { resetFavicon(); } - }, - error: () => { - resetFavicon(); - }, - }); -}; + }) + .catch(resetFavicon); export const spriteIcon = (icon, className = '') => { const classAttribute = className.length > 0 ? `class="${className}"` : ''; @@ -417,7 +407,6 @@ window.gl.utils = { getGroupSlug, isInIssuePage, ajaxGet, - ajaxPost, rstrip, updateTooltipTitle, disableButtonIfEmptyField, diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 9280b7f150c..62d80c4a649 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -64,3 +64,12 @@ export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - export function capitalizeFirstCharacter(text) { return `${text[0].toUpperCase()}${text.slice(1)}`; } + +/** + * Replaces all html tags from a string with the given replacement. + * + * @param {String} string + * @param {*} replace + * @returns {String} + */ +export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace); diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index f1ee9c8f2e5..a266bb6771f 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -18,7 +18,7 @@ export function getParameterValues(sParam) { // @param {String} url export function mergeUrlParams(params, url) { let newUrl = Object.keys(params).reduce((acc, paramName) => { - const paramValue = params[paramName]; + const paramValue = encodeURIComponent(params[paramName]); const pattern = new RegExp(`\\b(${paramName}=).*?(&|$)`); if (paramValue === null) { diff --git a/app/assets/javascripts/lib/utils/users_cache.js b/app/assets/javascripts/lib/utils/users_cache.js index 88f8a622c00..b01ec6b81a3 100644 --- a/app/assets/javascripts/lib/utils/users_cache.js +++ b/app/assets/javascripts/lib/utils/users_cache.js @@ -8,16 +8,16 @@ class UsersCache extends Cache { } return Api.users('', { username }) - .then((users) => { - if (!users.length) { + .then(({ data }) => { + if (!data.length) { throw new Error(`User "${username}" could not be found!`); } - if (users.length > 1) { + if (data.length > 1) { throw new Error(`Expected username "${username}" to be unique!`); } - const user = users[0]; + const user = data[0]; this.internalStorage[username] = user; return user; }); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 59bfa482bb0..39445a85c77 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -1,33 +1,17 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */ +/* eslint-disable import/first */ /* global ConfirmDangerModal */ import jQuery from 'jquery'; -import _ from 'underscore'; import Cookies from 'js-cookie'; -import Dropzone from 'dropzone'; -import Sortable from 'vendor/Sortable'; import svg4everybody from 'svg4everybody'; -// libraries with import side-effects -import 'mousetrap'; -import 'mousetrap/plugins/pause/mousetrap-pause'; - // expose common libraries as globals (TODO: remove these) window.jQuery = jQuery; window.$ = jQuery; -window._ = _; -window.Dropzone = Dropzone; -window.Sortable = Sortable; - -// templates -import './templates/issuable_template_selector'; -import './templates/issuable_template_selectors'; - -import './commit/image_file'; // lib/utils import { handleLocationHash } from './lib/utils/common_utils'; -import { localTimeAgo, renderTimeago } from './lib/utils/datetime_utility'; +import { localTimeAgo } from './lib/utils/datetime_utility'; import { getLocationHash, visitUrl } from './lib/utils/url_utility'; // behaviors @@ -43,24 +27,20 @@ import initTodoToggle from './header'; import initImporterStatus from './importer_status'; import initLayoutNav from './layout_nav'; import LazyLoader from './lazy_loader'; -import './line_highlighter'; import initLogoAnimation from './logo'; import './milestone_select'; -import './preview_markdown'; import './projects_dropdown'; import './render_gfm'; import initBreadcrumbs from './breadcrumb'; -import './dispatcher'; +import initDispatcher from './dispatcher'; // eslint-disable-next-line global-require, import/no-commonjs if (process.env.NODE_ENV !== 'production') require('./test_utils/'); -Dropzone.autoDiscover = false; - svg4everybody(); -document.addEventListener('beforeunload', function () { +document.addEventListener('beforeunload', () => { // Unbind scroll events $(document).off('scroll'); // Close any open tooltips @@ -77,16 +57,15 @@ window.addEventListener('load', function onLoad() { gl.lazyLoader = new LazyLoader({ scrollContainer: window, - observerNode: '#content-body' + observerNode: '#content-body', }); -$(function () { - var $body = $('body'); - var $document = $(document); - var $window = $(window); - var $sidebarGutterToggle = $('.js-sidebar-toggle'); - var bootstrapBreakpoint = bp.getBreakpointSize(); - var fitSidebarForSize; +$(() => { + const $body = $('body'); + const $document = $(document); + const $window = $(window); + const $sidebarGutterToggle = $('.js-sidebar-toggle'); + let bootstrapBreakpoint = bp.getBreakpointSize(); initBreadcrumbs(); initLayoutNav(); @@ -98,8 +77,8 @@ $(function () { Cookies.defaults.path = gon.relative_url_root || '/'; // `hashchange` is not triggered when link target is already in window.location - $body.on('click', 'a[href^="#"]', function() { - var href = this.getAttribute('href'); + $body.on('click', 'a[href^="#"]', function clickHashLinkCallback() { + const href = this.getAttribute('href'); if (href.substr(1) === getLocationHash()) { setTimeout(handleLocationHash, 1); } @@ -114,155 +93,162 @@ $(function () { } // prevent default action for disabled buttons - $('.btn').click(function(e) { + $('.btn').click(function clickDisabledButtonCallback(e) { if ($(this).hasClass('disabled')) { e.preventDefault(); e.stopImmediatePropagation(); return false; } + + return true; }); - $('.js-select-on-focus').on('focusin', function () { - return $(this).select().one('mouseup', function (e) { - return e.preventDefault(); - }); // 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(); + }); }); - $('.remove-row').bind('ajax:success', function () { + + $('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() { $(this).tooltip('destroy') .closest('li') .fadeOut(); }); - $('.js-remove-tr').bind('ajax:before', function () { - return $(this).hide(); + + $('.js-remove-tr').on('ajax:before', function removeTRAjaxBeforeCallback() { + $(this).hide(); }); - $('.js-remove-tr').bind('ajax:success', function () { - return $(this).closest('tr').fadeOut(); + + $('.js-remove-tr').on('ajax:success', function removeTRAjaxSuccessCallback() { + $(this).closest('tr').fadeOut(); }); + + // Initialize select2 selects $('select.select2').select2({ width: 'resolve', - // Initialize select2 selects - dropdownAutoWidth: true + dropdownAutoWidth: true, }); - $('.js-select2').bind('select2-close', function () { - return setTimeout((function () { - $('.select2-container-active').removeClass('select2-container-active'); - return $(':focus').blur(); - }), 1); + // Close select2 on escape + $('.js-select2').on('select2-close', () => { + setTimeout(() => { + $('.select2-container-active').removeClass('select2-container-active'); + $(':focus').blur(); + }, 1); }); + // Initialize tooltips $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover'; $body.tooltip({ selector: '.has-tooltip, [data-toggle="tooltip"]', - placement: function (tip, el) { + placement(tip, el) { return $(el).data('placement') || 'bottom'; - } + }, }); + // Initialize popovers $body.popover({ selector: '[data-toggle="popover"]', trigger: 'focus', // set the viewport to the main content, excluding the navigation bar, so // the navigation can't overlap the popover - viewport: '.layout-page' + viewport: '.layout-page', }); - $('.trigger-submit').on('change', function () { - return $(this).parents('form').submit(); + // Form submitter + $('.trigger-submit').on('change', function triggerSubmitCallback() { + $(this).parents('form').submit(); }); + localTimeAgo($('abbr.timeago, .js-timeago'), true); + // Disable form buttons while a form is submitting - $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) { - var buttons; - buttons = $('[type="submit"], .js-disable-on-submit', this); + $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function ajaxCompleteCallback(e) { + const $buttons = $('[type="submit"], .js-disable-on-submit', this); switch (e.type) { case 'ajax:beforeSend': case 'submit': - return buttons.disable(); + return $buttons.disable(); default: - return buttons.enable(); + return $buttons.enable(); } }); - $(document).ajaxError(function (e, xhrObj) { - var ref = xhrObj.status; - if (xhrObj.status === 401) { - return new Flash('You need to be logged in.', 'alert'); + + $(document).ajaxError((e, xhrObj) => { + const ref = xhrObj.status; + + if (ref === 401) { + Flash('You need to be logged in.'); } else if (ref === 404 || ref === 500) { - return new Flash('Something went wrong on our end.', 'alert'); + Flash('Something went wrong on our end.'); } }); - $('.account-box').hover(function () { - // Show/Hide the profile menu when hovering the account box - return $(this).toggleClass('hover'); - }); - $document.on('click', '.diff-content .js-show-suppressed-diff', function () { - var $container; - $container = $(this).parent(); - $container.next('table').show(); - return $container.remove(); + // Commit show suppressed diff + $document.on('click', '.diff-content .js-show-suppressed-diff', function showDiffCallback() { + const $container = $(this).parent(); + $container.next('table').show(); + $container.remove(); }); + $('.navbar-toggle').on('click', () => { $('.header-content').toggleClass('menu-expanded'); gl.lazyLoader.loadCheck(); }); + // Show/hide comments on diff - $body.on('click', '.js-toggle-diff-comments', function (e) { - var $this = $(this); - var notesHolders = $this.closest('.diff-file').find('.notes_holder'); + $body.on('click', '.js-toggle-diff-comments', function toggleDiffCommentsCallback(e) { + const $this = $(this); + const notesHolders = $this.closest('.diff-file').find('.notes_holder'); + + e.preventDefault(); + $this.toggleClass('active'); + if ($this.hasClass('active')) { notesHolders.show().find('.hide, .content').show(); } else { notesHolders.hide().find('.content').hide(); } + $(document).trigger('toggle.comments'); - return e.preventDefault(); }); - $document.off('click', '.js-confirm-danger'); - $document.on('click', '.js-confirm-danger', function (e) { - var btn = $(e.target); - var form = btn.closest('form'); - var text = btn.data('confirm-danger-message'); + + $document.on('click', '.js-confirm-danger', (e) => { + const btn = $(e.target); + const form = btn.closest('form'); + const text = btn.data('confirm-danger-message'); e.preventDefault(); - return new ConfirmDangerModal(form, text); - }); - $('input[type="search"]').each(function () { - var $this = $(this); - $this.attr('value', $this.val()); - }); - $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () { - var $this; - $this = $(this); - return $this.attr('value', $this.val()); + + // eslint-disable-next-line no-new + new ConfirmDangerModal(form, text); }); - $document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) { - var $gutterIcon; + + $document.on('breakpoint:change', (e, breakpoint) => { if (breakpoint === 'sm' || breakpoint === 'xs') { - $gutterIcon = $sidebarGutterToggle.find('i'); + const $gutterIcon = $sidebarGutterToggle.find('i'); if ($gutterIcon.hasClass('fa-angle-double-right')) { - return $sidebarGutterToggle.trigger('click'); + $sidebarGutterToggle.trigger('click'); } } }); - fitSidebarForSize = function () { - var oldBootstrapBreakpoint; - oldBootstrapBreakpoint = bootstrapBreakpoint; + + function fitSidebarForSize() { + const oldBootstrapBreakpoint = bootstrapBreakpoint; bootstrapBreakpoint = bp.getBreakpointSize(); + if (bootstrapBreakpoint !== oldBootstrapBreakpoint) { - return $document.trigger('breakpoint:change', [bootstrapBreakpoint]); + $document.trigger('breakpoint:change', [bootstrapBreakpoint]); } - }; - $window.off('resize.app').on('resize.app', function () { - return fitSidebarForSize(); - }); - loadAwardsHandler(); + } - renderTimeago(); + $window.on('resize.app', fitSidebarForSize); - $('form.filter-form').on('submit', function (event) { + loadAwardsHandler(); + + $('form.filter-form').on('submit', function filterFormSubmitCallback(event) { const link = document.createElement('a'); link.href = this.action; @@ -279,4 +265,6 @@ $(function () { removeFlashClickListener(flashEl); }); } + + initDispatcher(); }); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js index c012b77e0bf..c68b47c9348 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js @@ -1,4 +1,5 @@ /* eslint-disable no-param-reassign, comma-dangle */ +import axios from '../lib/utils/axios_utils'; ((global) => { global.mergeConflicts = global.mergeConflicts || {}; @@ -10,20 +11,11 @@ } fetchConflictsData() { - return $.ajax({ - dataType: 'json', - url: this.conflictsPath - }); + return axios.get(this.conflictsPath); } submitResolveConflicts(data) { - return $.ajax({ - url: this.resolveConflictsPath, - data: JSON.stringify(data), - contentType: 'application/json', - dataType: 'json', - method: 'POST' - }); + return axios.post(this.resolveConflictsPath, data); } } diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index 94561d6b7c3..b4b3c15108d 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -25,12 +25,12 @@ $(() => { gl.MergeConflictsResolverApp = new Vue({ el: '#conflicts', - data: mergeConflictsStore.state, components: { 'diff-file-editor': gl.mergeConflicts.diffFileEditor, 'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines, 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines }, + data: mergeConflictsStore.state, computed: { conflictsCountText() { return mergeConflictsStore.getConflictsCountText(); }, readyToCommit() { return mergeConflictsStore.isReadyToCommit(); }, @@ -38,24 +38,23 @@ $(() => { showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); } }, created() { - mergeConflictsService - .fetchConflictsData() - .done((data) => { + mergeConflictsService.fetchConflictsData() + .then(({ data }) => { if (data.type === 'error') { mergeConflictsStore.setFailedRequest(data.message); } else { mergeConflictsStore.setConflictsData(data); } - }) - .error(() => { - mergeConflictsStore.setFailedRequest(); - }) - .always(() => { + mergeConflictsStore.setLoadingState(false); this.$nextTick(() => { syntaxHighlight($('.js-syntax-highlight')); }); + }) + .catch(() => { + mergeConflictsStore.setLoadingState(false); + mergeConflictsStore.setFailedRequest(); }); }, methods: { @@ -82,10 +81,10 @@ $(() => { mergeConflictsService .submitResolveConflicts(mergeConflictsStore.getCommitData()) - .done((data) => { + .then(({ data }) => { window.location.href = data.redirect_to; }) - .error(() => { + .catch(() => { mergeConflictsStore.setSubmitState(false); new Flash('Failed to save merge conflicts resolutions. Please try again!'); }); diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index cb3cdea8111..bedd50de1bb 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,6 +1,7 @@ /* 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'; import IssuablesHelper from './helpers/issuables_helper'; @@ -110,22 +111,22 @@ MergeRequest.prototype.initCommitMessageListeners = function() { }); }; -MergeRequest.prototype.updateStatusText = function(classToRemove, classToAdd, newStatusText) { +MergeRequest.setStatusBoxToMerged = function() { $('.detail-page-header .status-box') - .removeClass(classToRemove) - .addClass(classToAdd) + .removeClass('status-box-open') + .addClass('status-box-mr-merged') .find('span') - .text(newStatusText); + .text(__('Merged')); }; -MergeRequest.prototype.decreaseCounter = function(by = 1) { - const $el = $('.nav-links .js-merge-counter'); +MergeRequest.decreaseCounter = function(by = 1) { + const $el = $('.js-merge-counter'); const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0); $el.text(addDelimiter(count)); }; -MergeRequest.prototype.hideCloseButton = function() { +MergeRequest.hideCloseButton = function() { const el = document.querySelector('.merge-request .js-issuable-actions'); const closeDropdownItem = el.querySelector('li.close-item'); if (closeDropdownItem) { diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index acfc62fe5cb..3e97a8c758d 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -1,7 +1,8 @@ /* eslint-disable no-new, class-methods-use-this */ import Cookies from 'js-cookie'; -import Flash from './flash'; +import axios from './lib/utils/axios_utils'; +import flash from './flash'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; import initChangesDropdown from './init_changes_dropdown'; import bp from './breakpoints'; @@ -244,15 +245,22 @@ export default class MergeRequestTabs { if (this.commitsLoaded) { return; } - this.ajaxGet({ - url: `${source}.json`, - success: (data) => { + + this.toggleLoading(true); + + axios.get(`${source}.json`) + .then(({ data }) => { document.querySelector('div#commits').innerHTML = data.html; localTimeAgo($('.js-timeago', 'div#commits')); this.commitsLoaded = true; this.scrollToElement('#commits'); - }, - }); + + this.toggleLoading(false); + }) + .catch(() => { + this.toggleLoading(false); + flash('An error occurred while fetching this tab.'); + }); } mountPipelinesView() { @@ -283,9 +291,10 @@ export default class MergeRequestTabs { // some pages like MergeRequestsController#new has query parameters on that anchor const urlPathname = parseUrlPathname(source); - this.ajaxGet({ - url: `${urlPathname}.json${location.search}`, - success: (data) => { + this.toggleLoading(true); + + axios.get(`${urlPathname}.json${location.search}`) + .then(({ data }) => { const $container = $('#diffs'); $container.html(data.html); @@ -335,8 +344,13 @@ export default class MergeRequestTabs { // (discussion and diff tabs) and `:target` only applies to the first anchor.addClass('target'); } - }, - }); + + this.toggleLoading(false); + }) + .catch(() => { + this.toggleLoading(false); + flash('An error occurred while fetching this tab.'); + }); } // Show or hide the loading spinner @@ -346,17 +360,6 @@ export default class MergeRequestTabs { $('.mr-loading-status .loading').toggle(status); } - ajaxGet(options) { - const defaults = { - beforeSend: () => this.toggleLoading(true), - error: () => new Flash('An error occurred while fetching this tab.', 'alert'), - complete: () => this.toggleLoading(false), - dataType: 'json', - type: 'GET', - }; - $.ajax($.extend({}, defaults, options)); - } - diffViewType() { return $('.inline-parallel-buttons a.active').data('view-type'); } diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index f76a998bf8c..b1d74250dfd 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,6 +1,5 @@ -/* global Sortable */ - -import Flash from './flash'; +import axios from './lib/utils/axios_utils'; +import flash from './flash'; export default class Milestone { constructor() { @@ -35,15 +34,12 @@ export default class Milestone { const tabElId = $target.attr('href'); if (endpoint && !$target.hasClass('is-loaded')) { - $.ajax({ - url: endpoint, - dataType: 'JSON', - }) - .fail(() => new Flash('Error loading milestone tab')) - .done((data) => { - $(tabElId).html(data.html); - $target.addClass('is-loaded'); - }); + axios.get(endpoint) + .then(({ data }) => { + $(tabElId).html(data.html); + $target.addClass('is-loaded'); + }) + .catch(() => flash('Error loading milestone tab')); } } } diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 2e5e818d61d..6581be606eb 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -1,222 +1,213 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */ /* global Issuable */ /* global ListMilestone */ import _ from 'underscore'; +import axios from './lib/utils/axios_utils'; import { timeFor } from './lib/utils/datetime_utility'; -(function() { - this.MilestoneSelect = (function() { - function MilestoneSelect(currentProject, els, options = {}) { - var _this, $els; - if (currentProject != null) { - _this = this; - this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject; - } +export default class MilestoneSelect { + constructor(currentProject, els, options = {}) { + if (currentProject !== null) { + this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject; + } - $els = $(els); + this.init(els, options); + } - if (!els) { - $els = $('.js-milestone-select'); - } + init(els, options) { + let $els = $(els); - $els.each(function(i, dropdown) { - var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, defaultNo, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, selectedMilestoneDefault, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove; - $dropdown = $(dropdown); - projectId = $dropdown.data('project-id'); - milestonesUrl = $dropdown.data('milestones'); - issueUpdateURL = $dropdown.data('issueUpdate'); - showNo = $dropdown.data('show-no'); - showAny = $dropdown.data('show-any'); - showMenuAbove = $dropdown.data('showMenuAbove'); - showUpcoming = $dropdown.data('show-upcoming'); - showStarted = $dropdown.data('show-started'); - useId = $dropdown.data('use-id'); - defaultLabel = $dropdown.data('default-label'); - defaultNo = $dropdown.data('default-no'); - issuableId = $dropdown.data('issuable-id'); - abilityName = $dropdown.data('ability-name'); - $selectbox = $dropdown.closest('.selectbox'); - $block = $selectbox.closest('.block'); - $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon'); - $value = $block.find('.value'); - $loading = $block.find('.block-loading').fadeOut(); - selectedMilestoneDefault = (showAny ? '' : null); - selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault); - selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault; - if (issueUpdateURL) { - milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'); - milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; - collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>'); - } - return $dropdown.glDropdown({ - showMenuAbove: showMenuAbove, - data: function(term, callback) { - return $.ajax({ - url: milestonesUrl - }).done(function(data) { - var extraOptions = []; - if (showAny) { - extraOptions.push({ - id: 0, - name: '', - title: 'Any Milestone' - }); - } - if (showNo) { - extraOptions.push({ - id: -1, - name: 'No Milestone', - title: 'No Milestone' - }); - } - if (showUpcoming) { - extraOptions.push({ - id: -2, - name: '#upcoming', - title: 'Upcoming' - }); - } - if (showStarted) { - extraOptions.push({ - id: -3, - name: '#started', - title: 'Started' - }); - } - if (extraOptions.length) { - extraOptions.push('divider'); - } + if (!els) { + $els = $('.js-milestone-select'); + } - callback(extraOptions.concat(data)); - if (showMenuAbove) { - $dropdown.data('glDropdown').positionMenuAbove(); - } - $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active'); - }); - }, - renderRow: function(milestone) { - return ` - <li data-milestone-id="${milestone.name}"> - <a href='#' class='dropdown-menu-milestone-link'> - ${_.escape(milestone.title)} - </a> - </li> - `; - }, - filterable: true, - search: { - fields: ['title'] - }, - selectable: true, - toggleLabel: function(selected, el, e) { - if (selected && 'id' in selected && $(el).hasClass('is-active')) { - return selected.title; - } else { - return defaultLabel; + $els.each((i, dropdown) => { + let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault; + const $dropdown = $(dropdown); + const projectId = $dropdown.data('project-id'); + const milestonesUrl = $dropdown.data('milestones'); + const issueUpdateURL = $dropdown.data('issueUpdate'); + const showNo = $dropdown.data('show-no'); + const showAny = $dropdown.data('show-any'); + 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 $selectBox = $dropdown.closest('.selectbox'); + const $block = $selectBox.closest('.block'); + const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon'); + const $value = $block.find('.value'); + const $loading = $block.find('.block-loading').fadeOut(); + selectedMilestoneDefault = (showAny ? '' : null); + selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault); + selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault; + + if (issueUpdateURL) { + milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'); + milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; + collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>'); + } + return $dropdown.glDropdown({ + showMenuAbove: showMenuAbove, + data: (term, callback) => axios.get(milestonesUrl) + .then(({ data }) => { + const extraOptions = []; + if (showAny) { + extraOptions.push({ + id: 0, + name: '', + title: 'Any Milestone' + }); } - }, - defaultLabel: defaultLabel, - fieldName: $dropdown.data('field-name'), - text: function(milestone) { - return _.escape(milestone.title); - }, - id: function(milestone) { - if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) { - return milestone.name; - } else { - return milestone.id; + if (showNo) { + extraOptions.push({ + id: -1, + name: 'No Milestone', + title: 'No Milestone' + }); } - }, - isSelected: function(milestone) { - return milestone.name === selectedMilestone; - }, - hidden: function() { - $selectbox.hide(); - // display:block overrides the hide-collapse rule - return $value.css('display', ''); - }, - opened: function(e) { - const $el = $(e.currentTarget); - if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) { - selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; + if (showUpcoming) { + extraOptions.push({ + id: -2, + name: '#upcoming', + title: 'Upcoming' + }); } - $('a.is-active', $el).removeClass('is-active'); - $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active'); - }, - vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function(clickEvent) { - const { $el, e } = clickEvent; - let selected = clickEvent.selectedObj; - - var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore; - if (!selected) return; - - if (options.handleClick) { - e.preventDefault(); - options.handleClick(selected); - return; + if (showStarted) { + extraOptions.push({ + id: -3, + name: '#started', + title: 'Started' + }); } - - page = $('body').attr('data-page'); - isIssueIndex = page === 'projects:issues:index'; - isMRIndex = (page === page && page === 'projects:merge_requests:index'); - isSelecting = (selected.name !== selectedMilestone); - selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault; - if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { - e.preventDefault(); - return; + if (extraOptions.length) { + extraOptions.push('divider'); } - if ($dropdown.closest('.add-issues-modal').length) { - boardsStore = gl.issueBoards.ModalStore.store.filter; + callback(extraOptions.concat(data)); + if (showMenuAbove) { + $dropdown.data('glDropdown').positionMenuAbove(); } + $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active'); + }), + renderRow: milestone => ` + <li data-milestone-id="${milestone.name}"> + <a href='#' class='dropdown-menu-milestone-link'> + ${_.escape(milestone.title)} + </a> + </li> + `, + filterable: true, + search: { + fields: ['title'] + }, + selectable: true, + toggleLabel: (selected, el, e) => { + if (selected && 'id' in selected && $(el).hasClass('is-active')) { + return selected.title; + } else { + return defaultLabel; + } + }, + defaultLabel: defaultLabel, + fieldName: $dropdown.data('field-name'), + text: milestone => _.escape(milestone.title), + id: (milestone) => { + if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) { + return milestone.name; + } else { + return milestone.id; + } + }, + isSelected: milestone => milestone.name === selectedMilestone, + hidden: () => { + $selectBox.hide(); + // display:block overrides the hide-collapse rule + return $value.css('display', ''); + }, + opened: (e) => { + const $el = $(e.currentTarget); + if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) { + selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; + } + $('a.is-active', $el).removeClass('is-active'); + $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active'); + }, + vue: $dropdown.hasClass('js-issue-board-sidebar'), + clicked: (clickEvent) => { + const { $el, e } = clickEvent; + let selected = clickEvent.selectedObj; - if (boardsStore) { - boardsStore[$dropdown.data('field-name')] = selected.name; - e.preventDefault(); - } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { - return Issuable.filterResults($dropdown.closest('form')); - } else if ($dropdown.hasClass('js-filter-submit')) { - return $dropdown.closest('form').submit(); - } else if ($dropdown.hasClass('js-issue-board-sidebar')) { - if (selected.id !== -1 && isSelecting) { - gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({ - id: selected.id, - title: selected.name - })); - } else { - gl.issueBoards.boardStoreIssueDelete('milestone'); - } + let data, boardsStore; + if (!selected) return; - $dropdown.trigger('loading.gl.dropdown'); - $loading.removeClass('hidden').fadeIn(); + if (options.handleClick) { + e.preventDefault(); + options.handleClick(selected); + return; + } - gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) - .then(function () { - $dropdown.trigger('loaded.gl.dropdown'); - $loading.fadeOut(); - }) - .catch(() => { - $loading.fadeOut(); - }); + const page = $('body').attr('data-page'); + const isIssueIndex = page === 'projects:issues:index'; + const isMRIndex = (page === page && page === 'projects:merge_requests:index'); + const isSelecting = (selected.name !== selectedMilestone); + selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault; + if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { + e.preventDefault(); + return; + } + + if ($dropdown.closest('.add-issues-modal').length) { + boardsStore = gl.issueBoards.ModalStore.store.filter; + } + + if (boardsStore) { + boardsStore[$dropdown.data('field-name')] = selected.name; + e.preventDefault(); + } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + return Issuable.filterResults($dropdown.closest('form')); + } else if ($dropdown.hasClass('js-filter-submit')) { + return $dropdown.closest('form').submit(); + } else if ($dropdown.hasClass('js-issue-board-sidebar')) { + if (selected.id !== -1 && isSelecting) { + gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({ + id: selected.id, + title: selected.name + })); } else { - selected = $selectbox.find('input[type="hidden"]').val(); - data = {}; - data[abilityName] = {}; - data[abilityName].milestone_id = selected != null ? selected : null; - $loading.removeClass('hidden').fadeIn(); - $dropdown.trigger('loading.gl.dropdown'); - return $.ajax({ - type: 'PUT', - url: issueUpdateURL, - data: data - }).done(function(data) { + gl.issueBoards.boardStoreIssueDelete('milestone'); + } + + $dropdown.trigger('loading.gl.dropdown'); + $loading.removeClass('hidden').fadeIn(); + + gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) + .then(() => { $dropdown.trigger('loaded.gl.dropdown'); $loading.fadeOut(); - $selectbox.hide(); + }) + .catch(() => { + $loading.fadeOut(); + }); + } else { + selected = $selectBox.find('input[type="hidden"]').val(); + data = {}; + data[abilityName] = {}; + data[abilityName].milestone_id = selected != null ? selected : null; + $loading.removeClass('hidden').fadeIn(); + $dropdown.trigger('loading.gl.dropdown'); + return axios.put(issueUpdateURL, data) + .then(({ data }) => { + $dropdown.trigger('loaded.gl.dropdown'); + $loading.fadeOut(); + $selectBox.hide(); $value.css('display', ''); if (data.milestone != null) { - data.milestone.full_path = _this.currentProject.full_path; + data.milestone.full_path = this.currentProject.full_path; data.milestone.remaining = timeFor(data.milestone.due_date); data.milestone.name = data.milestone.title; $value.html(milestoneLinkTemplate(data.milestone)); @@ -226,12 +217,9 @@ import { timeFor } from './lib/utils/datetime_utility'; return $sidebarCollapsedValue.find('span').text('No'); } }); - } } - }); + } }); - } - - return MilestoneSelect; - })(); -}).call(window); + }); + } +} diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js index ca3d271663b..c7bccd483ac 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -1,5 +1,6 @@ /* eslint-disable no-new */ -import Flash from './flash'; +import flash from './flash'; +import axios from './lib/utils/axios_utils'; /** * In each pipelines table we have a mini pipeline graph for each pipeline. @@ -78,27 +79,22 @@ export default class MiniPipelineGraph { const button = e.relatedTarget; const endpoint = button.dataset.stageEndpoint; - return $.ajax({ - dataType: 'json', - type: 'GET', - url: endpoint, - beforeSend: () => { - this.renderBuildsList(button, ''); - this.toggleLoading(button); - }, - success: (data) => { + this.renderBuildsList(button, ''); + this.toggleLoading(button); + + axios.get(endpoint) + .then(({ data }) => { this.toggleLoading(button); this.renderBuildsList(button, data.html); this.stopDropdownClickPropagation(); - }, - error: () => { + }) + .catch(() => { this.toggleLoading(button); if ($(button).parent().hasClass('open')) { $(button).dropdown('toggle'); } - new Flash('An error occurred while fetching the builds.', 'alert'); - }, - }); + flash('An error occurred while fetching the builds.', 'alert'); + }); } /** diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 8da723ced03..5afae93724b 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -11,6 +11,12 @@ export default { + components: { + Graph, + GraphGroup, + EmptyState, + }, + data() { const metricsData = document.querySelector('#prometheus-graphs').dataset; const store = new MonitoringStore(); @@ -36,12 +42,30 @@ }; }, - components: { - Graph, - GraphGroup, - EmptyState, + created() { + this.service = new MonitoringService({ + metricsEndpoint: this.metricsEndpoint, + deploymentEndpoint: this.deploymentEndpoint, + }); + eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); + eventHub.$on('hoverChanged', this.hoverChanged); + }, + + beforeDestroy() { + eventHub.$off('toggleAspectRatio', this.toggleAspectRatio); + eventHub.$off('hoverChanged', this.hoverChanged); + window.removeEventListener('resize', this.resizeThrottled, false); }, + mounted() { + this.resizeThrottled = _.throttle(this.resize, 600); + if (!this.hasMetrics) { + this.state = 'gettingStarted'; + } else { + this.getGraphsData(); + window.addEventListener('resize', this.resizeThrottled, false); + } + }, methods: { getGraphsData() { this.state = 'loading'; @@ -52,7 +76,13 @@ .then(data => this.store.storeDeploymentData(data)) .catch(() => new Flash('Error getting deployment information.')), ]) - .then(() => { this.showEmptyState = false; }) + .then(() => { + if (this.store.groups.length < 1) { + this.state = 'noData'; + return; + } + this.showEmptyState = false; + }) .catch(() => { this.state = 'unableToConnect'; }); }, @@ -72,36 +102,14 @@ this.hoverData = data; }, }, - - created() { - this.service = new MonitoringService({ - metricsEndpoint: this.metricsEndpoint, - deploymentEndpoint: this.deploymentEndpoint, - }); - eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); - eventHub.$on('hoverChanged', this.hoverChanged); - }, - - beforeDestroy() { - eventHub.$off('toggleAspectRatio', this.toggleAspectRatio); - eventHub.$off('hoverChanged', this.hoverChanged); - window.removeEventListener('resize', this.resizeThrottled, false); - }, - - mounted() { - this.resizeThrottled = _.throttle(this.resize, 600); - if (!this.hasMetrics) { - this.state = 'gettingStarted'; - } else { - this.getGraphsData(); - window.addEventListener('resize', this.resizeThrottled, false); - } - }, }; </script> <template> - <div v-if="!showEmptyState" class="prometheus-graphs"> + <div + v-if="!showEmptyState" + class="prometheus-graphs" + > <graph-group v-for="(groupData, index) in store.groups" :key="index" diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index a18164482a2..56cd60c583b 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -33,15 +33,24 @@ gettingStarted: { svgUrl: this.emptyGettingStartedSvgPath, title: 'Get started with performance monitoring', - description: 'Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.', + description: `Stay updated about the performance and health + of your environment by configuring Prometheus to monitor your deployments.`, buttonText: 'Configure Prometheus', }, loading: { svgUrl: this.emptyLoadingSvgPath, title: 'Waiting for performance data', - description: 'Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.', + description: `Creating graphs uses the data from the Prometheus server. + If this takes a long time, ensure that data is available.`, buttonText: 'View documentation', }, + noData: { + svgUrl: this.emptyUnableToConnectSvgPath, + title: 'No data found', + description: `You are connected to the Prometheus server, but there is currently + no data to display.`, + buttonText: 'Configure Prometheus', + }, unableToConnect: { svgUrl: this.emptyUnableToConnectSvgPath, title: 'Unable to connect to Prometheus server', @@ -74,20 +83,26 @@ <template> <div class="prometheus-state"> <div class="state-svg svg-content"> - <img :src="currentState.svgUrl"/> + <img :src="currentState.svgUrl" /> </div> <h4 class="state-title"> - {{currentState.title}} + {{ currentState.title }} </h4> <p class="state-description"> - {{currentState.description}} - <a v-if="showButtonDescription" :href="settingsPath"> + {{ currentState.description }} + <a + v-if="showButtonDescription" + :href="settingsPath" + > Prometheus server </a> </p> <div class="state-button"> - <a class="btn btn-success" :href="buttonPath"> - {{currentState.buttonText}} + <a + class="btn btn-success" + :href="buttonPath" + > + {{ currentState.buttonText }} </a> </div> </div> diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index eede04a06cd..ea5c24efaf9 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -17,6 +17,15 @@ const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select }; export default { + components: { + GraphLegend, + GraphFlag, + GraphDeployment, + GraphPath, + }, + + mixins: [MonitoringMixin], + props: { graphData: { type: Object, @@ -45,8 +54,6 @@ }, }, - mixins: [MonitoringMixin], - data() { return { baseGraphHeight: 450, @@ -69,28 +76,18 @@ currentFlagPosition: 0, showFlag: false, showFlagContent: false, - showDeployInfo: true, timeSeries: [], + realPixelRatio: 1, }; }, - components: { - GraphLegend, - GraphFlag, - GraphDeployment, - GraphPath, - }, - computed: { outerViewBox() { return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`; }, innerViewBox() { - if ((this.baseGraphWidth - 150) > 0) { - return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`; - } - return '0 0 0 0'; + return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`; }, axisTransform() { @@ -102,6 +99,30 @@ paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`, }; }, + + deploymentFlagData() { + return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag); + }, + }, + + watch: { + updateAspectRatio() { + if (this.updateAspectRatio) { + this.graphHeight = 450; + this.graphWidth = 600; + this.measurements = measurements.large; + this.draw(); + eventHub.$emit('toggleAspectRatio'); + } + }, + + hoverData() { + this.positionFlag(); + }, + }, + + mounted() { + this.draw(); }, methods: { @@ -122,6 +143,10 @@ this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; this.baseGraphHeight = this.graphHeight; this.baseGraphWidth = this.graphWidth; + + // pixel offsets inside the svg and outside are not 1:1 + this.realPixelRatio = (this.$refs.baseSvg.clientWidth / this.baseGraphWidth); + this.renderAxesPaths(); this.formatDeployments(); }, @@ -192,51 +217,34 @@ }); // This will select all of the ticks once they're rendered }, }, - - watch: { - updateAspectRatio() { - if (this.updateAspectRatio) { - this.graphHeight = 450; - this.graphWidth = 600; - this.measurements = measurements.large; - this.draw(); - eventHub.$emit('toggleAspectRatio'); - } - }, - - hoverData() { - this.positionFlag(); - }, - }, - - mounted() { - this.draw(); - }, }; </script> <template> - <div + <div class="prometheus-graph" @mouseover="showFlagContent = true" - @mouseleave="showFlagContent = false"> + @mouseleave="showFlagContent = false" + > <h5 class="text-center graph-title"> - {{graphData.title}} + {{ graphData.title }} </h5> <div class="prometheus-svg-container" - :style="paddingBottomRootSvg"> + :style="paddingBottomRootSvg" + > <svg :viewBox="outerViewBox" - ref="baseSvg"> + ref="baseSvg" + > <g class="x-axis" - :transform="axisTransform"> - </g> + :transform="axisTransform" + /> <g class="y-axis" - transform="translate(70, 20)"> - </g> + transform="translate(70, 20)" + /> <graph-legend :graph-width="graphWidth" :graph-height="graphHeight" @@ -251,42 +259,45 @@ <svg class="graph-data" :viewBox="innerViewBox" - ref="graphData"> - <graph-path - v-for="(path, index) in timeSeries" - :key="index" - :generated-line-path="path.linePath" - :generated-area-path="path.areaPath" - :line-style="path.lineStyle" - :line-color="path.lineColor" - :area-color="path.areaColor" - /> - <rect - class="prometheus-graph-overlay" - :width="(graphWidth - 70)" - :height="(graphHeight - 100)" - transform="translate(-5, 20)" - ref="graphOverlay" - @mousemove="handleMouseOverGraph($event)"> - </rect> - <graph-deployment - :show-deploy-info="showDeployInfo" - :deployment-data="reducedDeploymentData" - :graph-width="graphWidth" - :graph-height="graphHeight" - :graph-height-offset="graphHeightOffset" - /> - <graph-flag - v-if="showFlag" - :current-x-coordinate="currentXCoordinate" - :current-data="currentData" - :current-flag-position="currentFlagPosition" - :graph-height="graphHeight" - :graph-height-offset="graphHeightOffset" - :show-flag-content="showFlagContent" - /> + ref="graphData" + > + <graph-path + v-for="(path, index) in timeSeries" + :key="index" + :generated-line-path="path.linePath" + :generated-area-path="path.areaPath" + :line-style="path.lineStyle" + :line-color="path.lineColor" + :area-color="path.areaColor" + /> + <graph-deployment + :deployment-data="reducedDeploymentData" + :graph-height="graphHeight" + :graph-height-offset="graphHeightOffset" + /> + <rect + class="prometheus-graph-overlay" + :width="(graphWidth - 70)" + :height="(graphHeight - 100)" + transform="translate(-5, 20)" + ref="graphOverlay" + @mousemove="handleMouseOverGraph($event)" + /> </svg> </svg> + <graph-flag + :real-pixel-ratio="realPixelRatio" + :current-x-coordinate="currentXCoordinate" + :current-data="currentData" + :graph-height="graphHeight" + :graph-height-offset="graphHeightOffset" + :show-flag-content="showFlagContent" + :time-series="timeSeries" + :unit-of-display="unitOfDisplay" + :current-data-index="currentDataIndex" + :legend-title="legendTitle" + :deployment-flag-data="deploymentFlagData" + /> </div> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue index 026e2fd0c49..98c25307b74 100644 --- a/app/assets/javascripts/monitoring/components/graph/deployment.vue +++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue @@ -1,13 +1,6 @@ <script> - import { dateFormatWithName, timeFormat } from '../../utils/date_time_formatters'; - import Icon from '../../../vue_shared/components/icon.vue'; - export default { props: { - showDeployInfo: { - type: Boolean, - required: true, - }, deploymentData: { type: Array, required: true, @@ -20,14 +13,6 @@ type: Number, required: true, }, - graphWidth: { - type: Number, - required: true, - }, - }, - - components: { - Icon, }, computed: { @@ -37,160 +22,52 @@ }, methods: { - refText(d) { - return d.tag ? d.ref : d.sha.slice(0, 8); - }, - - formatTime(deploymentTime) { - return timeFormat(deploymentTime); - }, - - formatDate(deploymentTime) { - return dateFormatWithName(deploymentTime); - }, - - nameDeploymentClass(deployment) { - return `deploy-info-${deployment.id}`; - }, - transformDeploymentGroup(deployment) { - return `translate(${Math.floor(deployment.xPos) + 1}, 20)`; - }, - - positionFlag(deployment) { - let xPosition = 3; - if (deployment.xPos > (this.graphWidth - 225)) { - xPosition = -142; - } - return xPosition; - }, - - svgContainerHeight(tag) { - let svgHeight = 80; - if (!tag) { - svgHeight -= 20; - } - return svgHeight; + return `translate(${Math.floor(deployment.xPos) - 5}, 20)`; }, }, }; </script> <template> - <g - class="deploy-info" - v-if="showDeployInfo"> + <g class="deploy-info"> <g v-for="(deployment, index) in deploymentData" :key="index" - :class="nameDeploymentClass(deployment)" :transform="transformDeploymentGroup(deployment)"> <rect x="0" y="0" :height="calculatedHeight" width="3" - fill="url(#shadow-gradient)"> - </rect> + fill="url(#shadow-gradient)" + /> <line class="deployment-line" x1="0" y1="0" x2="0" :y2="calculatedHeight" - stroke="#000"> - </line> - <svg - v-if="deployment.showDeploymentFlag" - class="js-deploy-info-box" - :x="positionFlag(deployment)" - y="0" - width="134" - :height="svgContainerHeight(deployment.tag)"> - <rect - class="rect-text-metric deploy-info-rect rect-metric" - x="1" - y="1" - rx="2" - width="132" - :height="svgContainerHeight(deployment.tag) - 2"> - </rect> - <text - class="deploy-info-text text-metric-bold" - transform="translate(5, 2)"> - Deployed - </text> - <!--The date info--> - <g transform="translate(5, 20)"> - <text class="deploy-info-text"> - {{formatDate(deployment.time)}} - </text> - <text - class="deploy-info-text text-metric-bold" - x="62"> - {{formatTime(deployment.time)}} - </text> - </g> - <line - class="divider-line" - x1="0" - y1="38" - x2="132" - :y2="38" - stroke="#000"> - </line> - <!--Commit information--> - <g transform="translate(5, 40)"> - <icon - name="commit" - :width="12" - :height="12" - :y="3"> - </icon> - <a :xlink:href="deployment.commitUrl"> - <text - class="deploy-info-text deploy-info-text-link" - transform="translate(20, 2)"> - {{refText(deployment)}} - </text> - </a> - </g> - <!--Tag information--> - <g - transform="translate(5, 55)" - v-if="deployment.tag"> - <icon - name="label" - :width="12" - :height="12" - :y="5"> - </icon> - <a :xlink:href="deployment.tagUrl"> - <text - class="deploy-info-text deploy-info-text-link" - transform="translate(20, 2)" - y="2"> - {{deployment.tag}} - </text> - </a> - </g> - </svg> + stroke="#000" + /> </g> <svg height="0" - width="0"> + width="0" + > <defs> <linearGradient - id="shadow-gradient"> + id="shadow-gradient" + > <stop offset="0%" stop-color="#000" - stop-opacity="0.4"> - </stop> + stop-opacity="0.4" + /> <stop offset="100%" stop-color="#000" - stop-opacity="0"> - </stop> + stop-opacity="0" + /> </linearGradient> </defs> </svg> diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index 10fb7ff6803..07aa6a3e5de 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -1,20 +1,26 @@ <script> import { dateFormat, timeFormat } from '../../utils/date_time_formatters'; + import { formatRelevantDigits } from '../../../lib/utils/number_utils'; + import icon from '../../../vue_shared/components/icon.vue'; export default { + components: { + icon, + }, props: { currentXCoordinate: { type: Number, required: true, }, - currentFlagPosition: { - type: Number, - required: true, - }, currentData: { type: Object, required: true, }, + deploymentFlagData: { + type: Object, + required: false, + default: null, + }, graphHeight: { type: Number, required: true, @@ -23,71 +29,173 @@ type: Number, required: true, }, + realPixelRatio: { + type: Number, + required: true, + }, showFlagContent: { type: Boolean, required: true, }, - }, - - data() { - return { - circleColorRgb: '#8fbce8', - }; + timeSeries: { + type: Array, + required: true, + }, + unitOfDisplay: { + type: String, + required: true, + }, + currentDataIndex: { + type: Number, + required: true, + }, + legendTitle: { + type: String, + required: true, + }, }, computed: { formatTime() { - return timeFormat(this.currentData.time); + return this.deploymentFlagData ? + timeFormat(this.deploymentFlagData.time) : + timeFormat(this.currentData.time); }, formatDate() { - return dateFormat(this.currentData.time); + return this.deploymentFlagData ? + dateFormat(this.deploymentFlagData.time) : + dateFormat(this.currentData.time); + }, + + cursorStyle() { + const xCoordinate = this.deploymentFlagData ? + this.deploymentFlagData.xPos : + this.currentXCoordinate; + + const offsetTop = 20 * this.realPixelRatio; + const offsetLeft = (70 + xCoordinate) * this.realPixelRatio; + const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio; + + return { + top: `${offsetTop}px`, + left: `${offsetLeft}px`, + height: `${height}px`, + }; }, - calculatedHeight() { - return this.graphHeight - this.graphHeightOffset; + flagOrientation() { + if (this.currentXCoordinate * this.realPixelRatio > 120) { + return 'left'; + } + return 'right'; + }, + }, + + methods: { + seriesMetricValue(series) { + const index = this.deploymentFlagData ? + this.deploymentFlagData.seriesIndex : + this.currentDataIndex; + const value = series.values[index] && + series.values[index].value; + if (isNaN(value)) { + return '-'; + } + return `${formatRelevantDigits(value)}${this.unitOfDisplay}`; + }, + + seriesMetricLabel(index, series) { + if (this.timeSeries.length < 2) { + return this.legendTitle; + } + if (series.metricTag) { + return series.metricTag; + } + return `series ${index + 1}`; + }, + + strokeDashArray(type) { + if (type === 'dashed') return '6, 3'; + if (type === 'dotted') return '3, 3'; + return null; }, }, }; </script> + <template> - <g class="mouse-over-flag"> - <line - class="selected-metric-line" - :x1="currentXCoordinate" - :y1="0" - :x2="currentXCoordinate" - :y2="calculatedHeight" - transform="translate(-5, 20)"> - </line> - <svg + <div + class="prometheus-graph-cursor" + :style="cursorStyle" + > + <div v-if="showFlagContent" - class="rect-text-metric" - :x="currentFlagPosition" - y="0"> - <rect - class="rect-metric" - x="4" - y="1" - rx="2" - width="90" - height="40" - transform="translate(-3, 20)"> - </rect> - <text - class="text-metric text-metric-bold" - x="16" - y="35" - transform="translate(-5, 20)"> - {{formatTime}} - </text> - <text - class="text-metric" - x="16" - y="15" - transform="translate(-5, 20)"> - {{formatDate}} - </text> - </svg> - </g> + class="prometheus-graph-flag popover" + :class="flagOrientation" + > + <div class="arrow"></div> + <div class="popover-title"> + <h5 v-if="deploymentFlagData"> + Deployed + </h5> + {{ formatDate }} at + <strong>{{ formatTime }}</strong> + </div> + <div + v-if="deploymentFlagData" + class="popover-content deploy-meta-content" + > + <div> + <icon + name="commit" + :size="12" + /> + <a :href="deploymentFlagData.commitUrl"> + {{ deploymentFlagData.sha.slice(0, 8) }} + </a> + </div> + <div + v-if="deploymentFlagData.tag" + > + <icon + name="label" + :size="12" + /> + <a :href="deploymentFlagData.tagUrl"> + {{ deploymentFlagData.ref }} + </a> + </div> + </div> + <div class="popover-content"> + <table> + <tr + v-for="(series, index) in timeSeries" + :key="index" + > + <td> + <svg + width="15" + height="6" + > + <line + :stroke="series.lineColor" + :stroke-dasharray="strokeDashArray(series.lineStyle)" + stroke-width="4" + x1="0" + x2="15" + y1="2" + y2="2" + /> + </svg> + </td> + <td>{{ seriesMetricLabel(index, series) }}</td> + <td> + <strong>{{ seriesMetricValue(series) }}</strong> + </td> + </tr> + </table> + </div> + </div> + </div> </template> diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue index 440b1b12631..c6e8d726ffc 100644 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ b/app/assets/javascripts/monitoring/components/graph/legend.vue @@ -73,6 +73,21 @@ }, }, + mounted() { + this.$nextTick(() => { + const bbox = this.$refs.ylabel.getBBox(); + this.metricUsageXPosition = 0; + this.seriesXPosition = 0; + if (this.$refs.legendTitleSvg != null) { + this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width; + } + if (this.$refs.seriesTitleSvg != null) { + this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width; + } + this.yLabelWidth = bbox.width + 10; // Added some padding + this.yLabelHeight = bbox.height + 5; + }); + }, methods: { translateLegendGroup(index) { return `translate(0, ${12 * (index)})`; @@ -100,26 +115,10 @@ return null; }, }, - mounted() { - this.$nextTick(() => { - const bbox = this.$refs.ylabel.getBBox(); - this.metricUsageXPosition = 0; - this.seriesXPosition = 0; - if (this.$refs.legendTitleSvg != null) { - this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width; - } - if (this.$refs.seriesTitleSvg != null) { - this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width; - } - this.yLabelWidth = bbox.width + 10; // Added some padding - this.yLabelHeight = bbox.height + 5; - }); - }, }; </script> <template> - <g - class="axis-label-container"> + <g class="axis-label-container"> <line class="label-x-axis-line" stroke="#000000" @@ -127,8 +126,8 @@ x1="10" :y1="yPosition" :x2="graphWidth + 20" - :y2="yPosition"> - </line> + :y2="yPosition" + /> <line class="label-y-axis-line" stroke="#000000" @@ -136,39 +135,43 @@ x1="10" y1="0" :x2="10" - :y2="yPosition"> - </line> + :y2="yPosition" + /> <rect class="rect-axis-text" :transform="rectTransform" :width="yLabelWidth" - :height="yLabelHeight"> - </rect> + :height="yLabelHeight" + /> <text class="label-axis-text y-label-text" text-anchor="middle" :transform="textTransform" - ref="ylabel"> - {{yAxisLabel}} + ref="ylabel" + > + {{ yAxisLabel }} </text> <rect class="rect-axis-text" :x="xPosition + 60" :y="graphHeight - 80" width="35" - height="50"> - </rect> + height="50" + /> <text class="label-axis-text x-label-text" :x="xPosition + 60" :y="yPosition" - dy=".35em"> + dy=".35em" + > Time </text> - <g class="legend-group" + <g + class="legend-group" v-for="(series, index) in timeSeries" :key="index" - :transform="translateLegendGroup(index)"> + :transform="translateLegendGroup(index)" + > <line :stroke="series.lineColor" :stroke-width="measurements.legends.height" @@ -176,23 +179,25 @@ :x1="measurements.legends.offsetX" :x2="measurements.legends.offsetX + measurements.legends.width" :y1="graphHeight - measurements.legends.offsetY" - :y2="graphHeight - measurements.legends.offsetY"> - </line> + :y2="graphHeight - measurements.legends.offsetY" + /> <text v-if="timeSeries.length > 1" class="legend-metric-title" ref="legendTitleSvg" x="38" - :y="graphHeight - 30"> - {{createSeriesString(index, series)}} + :y="graphHeight - 30" + > + {{ createSeriesString(index, series) }} </text> <text v-else class="legend-metric-title" ref="legendTitleSvg" x="38" - :y="graphHeight - 30"> - {{legendTitle}} {{formatMetricUsage(series)}} + :y="graphHeight - 30" + > + {{ legendTitle }} {{ formatMetricUsage(series) }} </text> </g> </g> diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue index 5e6d409033a..c9721c4cb01 100644 --- a/app/assets/javascripts/monitoring/components/graph/path.vue +++ b/app/assets/javascripts/monitoring/components/graph/path.vue @@ -12,6 +12,7 @@ lineStyle: { type: String, required: false, + default: '', }, lineColor: { type: String, @@ -37,8 +38,8 @@ class="metric-area" :d="generatedAreaPath" :fill="areaColor" - transform="translate(-5, 20)"> - </path> + transform="translate(-5, 20)" + /> <path class="metric-line" :d="generatedLinePath" @@ -46,7 +47,7 @@ fill="none" stroke-width="1" :stroke-dasharray="strokeDashArray" - transform="translate(-5, 20)"> - </path> + transform="translate(-5, 20)" + /> </g> </template> diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue index 958f537d31b..079351a69af 100644 --- a/app/assets/javascripts/monitoring/components/graph_group.vue +++ b/app/assets/javascripts/monitoring/components/graph_group.vue @@ -1,21 +1,21 @@ <script> -export default { - props: { - name: { - type: String, - required: true, + export default { + props: { + name: { + type: String, + required: true, + }, }, - }, -}; + }; </script> <template> <div class="panel panel-default prometheus-panel"> <div class="panel-heading"> - <h4>{{name}}</h4> + <h4>{{ name }}</h4> </div> <div class="panel-body prometheus-graph-group"> - <slot /> + <slot></slot> </div> </div> </template> diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js index cbca14ede02..6cc67ba57ee 100644 --- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js +++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js @@ -29,15 +29,18 @@ const mixins = { time.setSeconds(this.timeSeries[0].values[0].time.getSeconds()); if (xPos >= 0) { + const seriesIndex = bisectDate(this.timeSeries[0].values, time, 1); + deploymentDataArray.push({ id: deployment.id, time, sha: deployment.sha, commitUrl: `${this.projectPath}/commit/${deployment.sha}`, tag: deployment.tag, - tagUrl: `${this.tagsPath}/${deployment.tag}`, + tagUrl: deployment.tag ? `${this.tagsPath}/${deployment.ref.name}` : null, ref: deployment.ref.name, xPos, + seriesIndex, showDeploymentFlag: false, }); } diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index 104432ef5de..c3b0ef7e9ca 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import Dashboard from './components/dashboard.vue'; -document.addEventListener('DOMContentLoaded', () => new Vue({ +export default () => new Vue({ el: '#prometheus-graphs', render: createElement => createElement(Dashboard), -})); +}); diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js index 48bdec1e030..f3c9acdd93e 100644 --- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js +++ b/app/assets/javascripts/monitoring/utils/date_time_formatters.js @@ -1,10 +1,20 @@ import { timeFormat as time } from 'd3-time-format'; -import { timeSecond, timeMinute, timeHour, timeDay, timeMonth, timeYear } from 'd3-time'; +import { timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear } from 'd3-time'; import { bisector } from 'd3-array'; -const d3 = { time, bisector, timeSecond, timeMinute, timeHour, timeDay, timeMonth, timeYear }; +const d3 = { + time, + bisector, + timeSecond, + timeMinute, + timeHour, + timeDay, + timeWeek, + timeMonth, + timeYear, +}; -export const dateFormat = d3.time('%b %-d, %Y'); +export const dateFormat = d3.time('%a, %b %-d'); export const timeFormat = d3.time('%-I:%M%p'); export const dateFormatWithName = d3.time('%a, %b %-d'); export const bisectDate = d3.bisector(d => d.time).left; diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index 5aad3908eb6..d3edcb724f1 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -1,5 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */ +import { __ } from '../locale'; +import axios from '../lib/utils/axios_utils'; +import flash from '../flash'; import Raphael from './raphael'; export default (function() { @@ -26,16 +29,13 @@ export default (function() { } BranchGraph.prototype.load = function() { - return $.ajax({ - url: this.options.url, - method: "get", - dataType: "json", - success: $.proxy(function(data) { + axios.get(this.options.url) + .then(({ data }) => { $(".loading", this.element).hide(); this.prepareData(data.days, data.commits); - return this.buildGraph(); - }, this) - }); + this.buildGraph(); + }) + .catch(() => __('Error fetching network graph.')); }; BranchGraph.prototype.prepareData = function(days, commits) { diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue index 82c51a1068c..3d09d24b6ab 100644 --- a/app/assets/javascripts/notebook/cells/markdown.vue +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -1,6 +1,7 @@ <script> /* global katex */ import marked from 'marked'; + import sanitize from 'sanitize-html'; import Prompt from './prompt.vue'; const renderer = new marked.Renderer(); @@ -82,7 +83,12 @@ }, computed: { markdown() { - return marked(this.cell.source.join('').replace(/\\/g, '\\\\')); + return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), { + allowedTags: false, + allowedAttributes: { + '*': ['class'], + }, + }); }, }, }; @@ -91,18 +97,21 @@ <template> <div class="cell text-cell"> <prompt /> - <div class="markdown" v-html="markdown"></div> + <div + class="markdown" + v-html="markdown"> + </div> </div> </template> <style> -.markdown .katex { - display: block; - text-align: center; -} + .markdown .katex { + display: block; + text-align: center; + } -.markdown .inline-katex .katex { - display: inline; - text-align: initial; -} + .markdown .inline-katex .katex { + display: inline; + text-align: initial; + } </style> diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue index 2110a9de7ed..0535ee7afa8 100644 --- a/app/assets/javascripts/notebook/cells/output/html.vue +++ b/app/assets/javascripts/notebook/cells/output/html.vue @@ -1,22 +1,35 @@ <script> -import Prompt from '../prompt.vue'; + import sanitize from 'sanitize-html'; + import Prompt from '../prompt.vue'; -export default { - props: { - rawCode: { - type: String, - required: true, + export default { + components: { + prompt: Prompt, }, - }, - components: { - prompt: Prompt, - }, -}; + props: { + rawCode: { + type: String, + required: true, + }, + }, + computed: { + sanitizedOutput() { + return sanitize(this.rawCode, { + allowedTags: sanitize.defaults.allowedTags.concat([ + 'img', 'svg', + ]), + allowedAttributes: { + img: ['src'], + }, + }); + }, + }, + }; </script> <template> <div class="output"> <prompt /> - <div v-html="rawCode"></div> + <div v-html="sanitizedOutput"></div> </div> </template> diff --git a/app/assets/javascripts/notebook/cells/output/image.vue b/app/assets/javascripts/notebook/cells/output/image.vue index fbb39ea6e2d..67d6c5ad12b 100644 --- a/app/assets/javascripts/notebook/cells/output/image.vue +++ b/app/assets/javascripts/notebook/cells/output/image.vue @@ -1,27 +1,26 @@ <script> -import Prompt from '../prompt.vue'; + import Prompt from '../prompt.vue'; -export default { - props: { - outputType: { - type: String, - required: true, + export default { + components: { + prompt: Prompt, }, - rawCode: { - type: String, - required: true, + props: { + outputType: { + type: String, + required: true, + }, + rawCode: { + type: String, + required: true, + }, }, - }, - components: { - prompt: Prompt, - }, -}; + }; </script> <template> <div class="output"> <prompt /> - <img - :src="'data:' + outputType + ';base64,' + rawCode" /> + <img :src="'data:' + outputType + ';base64,' + rawCode" /> </div> </template> diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue index 05af0bf1e8e..91b2269a83a 100644 --- a/app/assets/javascripts/notebook/cells/output/index.vue +++ b/app/assets/javascripts/notebook/cells/output/index.vue @@ -1,83 +1,87 @@ <script> -import CodeCell from '../code/index.vue'; -import Html from './html.vue'; -import Image from './image.vue'; + import CodeCell from '../code/index.vue'; + import Html from './html.vue'; + import Image from './image.vue'; -export default { - props: { - codeCssClass: { - type: String, - required: false, - default: '', + export default { + components: { + 'code-cell': CodeCell, + 'html-output': Html, + 'image-output': Image, }, - count: { - type: Number, - required: false, - default: 0, + props: { + codeCssClass: { + type: String, + required: false, + default: '', + }, + count: { + type: Number, + required: false, + default: 0, + }, + output: { + type: Object, + requred: true, + default: () => ({}), + }, }, - output: { - type: Object, - requred: true, - }, - }, - components: { - 'code-cell': CodeCell, - 'html-output': Html, - 'image-output': Image, - }, - data() { - return { - outputType: '', - }; - }, - computed: { - componentName() { - if (this.output.text) { - return 'code-cell'; - } else if (this.output.data['image/png']) { - this.outputType = 'image/png'; - - return 'image-output'; - } else if (this.output.data['text/html']) { - this.outputType = 'text/html'; + computed: { + componentName() { + if (this.output.text) { + return 'code-cell'; + } else if (this.output.data['image/png']) { + return 'image-output'; + } else if (this.output.data['text/html']) { + return 'html-output'; + } else if (this.output.data['image/svg+xml']) { + return 'html-output'; + } - return 'html-output'; - } else if (this.output.data['image/svg+xml']) { - this.outputType = 'image/svg+xml'; - - return 'html-output'; - } + return 'code-cell'; + }, + rawCode() { + if (this.output.text) { + return this.output.text.join(''); + } - this.outputType = 'text/plain'; - return 'code-cell'; - }, - rawCode() { - if (this.output.text) { - return this.output.text.join(''); - } + return this.dataForType(this.outputType); + }, + outputType() { + if (this.output.text) { + return ''; + } else if (this.output.data['image/png']) { + return 'image/png'; + } else if (this.output.data['text/html']) { + return 'text/html'; + } else if (this.output.data['image/svg+xml']) { + return 'image/svg+xml'; + } - return this.dataForType(this.outputType); + return 'text/plain'; + }, }, - }, - methods: { - dataForType(type) { - let data = this.output.data[type]; + methods: { + dataForType(type) { + let data = this.output.data[type]; - if (typeof data === 'object') { - data = data.join(''); - } + if (typeof data === 'object') { + data = data.join(''); + } - return data; + return data; + }, }, - }, -}; + }; </script> <template> - <component :is="componentName" + <component + :is="componentName" type="output" - :outputType="outputType" + :output-type="outputType" :count="count" :raw-code="rawCode" - :code-css-class="codeCssClass" /> + :code-css-class="codeCssClass" + /> </template> diff --git a/app/assets/javascripts/notebook/cells/prompt.vue b/app/assets/javascripts/notebook/cells/prompt.vue index 039fb99293d..fe1fc37e1dc 100644 --- a/app/assets/javascripts/notebook/cells/prompt.vue +++ b/app/assets/javascripts/notebook/cells/prompt.vue @@ -4,10 +4,17 @@ type: { type: String, required: false, + default: '', }, count: { type: Number, required: false, + default: 0, + }, + }, + computed: { + hasKeys() { + return this.type !== '' && this.count; }, }, }; @@ -15,16 +22,16 @@ <template> <div class="prompt"> - <span v-if="type && count"> + <span v-if="hasKeys"> {{ type }} [{{ count }}]: </span> </div> </template> <style scoped> -.prompt { - padding: 0 10px; - min-width: 7em; - font-family: monospace; -} + .prompt { + padding: 0 10px; + min-width: 7em; + font-family: monospace; + } </style> diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue index e88806431af..e2e3b08c77f 100644 --- a/app/assets/javascripts/notebook/index.vue +++ b/app/assets/javascripts/notebook/index.vue @@ -20,11 +20,6 @@ default: '', }, }, - methods: { - cellType(type) { - return `${type}-cell`; - }, - }, computed: { cells() { if (this.notebook.worksheets) { @@ -45,6 +40,11 @@ return Object.keys(this.notebook).length; }, }, + methods: { + cellType(type) { + return `${type}-cell`; + }, + }, }; </script> diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index a2b8e6f6495..8efb8ac5320 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -16,6 +16,7 @@ import Autosize from 'autosize'; import 'vendor/jquery.caret'; // required by jquery.atwho import 'vendor/jquery.atwho'; import AjaxCache from '~/lib/utils/ajax_cache'; +import axios from './lib/utils/axios_utils'; import { getLocationHash } from './lib/utils/url_utility'; import Flash from './flash'; import CommentTypeToggle from './comment_type_toggle'; @@ -23,7 +24,7 @@ import GLForm from './gl_form'; import loadAwardsHandler from './awards_handler'; import Autosave from './autosave'; import TaskList from './task_list'; -import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils'; +import { isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils'; import imageDiffHelper from './image_diff/helpers/index'; import { localTimeAgo } from './lib/utils/datetime_utility'; @@ -252,26 +253,20 @@ export default class Notes { return; } this.refreshing = true; - return $.ajax({ - url: this.notes_url, - headers: { 'X-Last-Fetched-At': this.last_fetched_at }, - dataType: 'json', - success: (function(_this) { - return function(data) { - var notes; - notes = data.notes; - _this.last_fetched_at = data.last_fetched_at; - _this.setPollingInterval(data.notes.length); - return $.each(notes, function(i, note) { - _this.renderNote(note); - }); - }; - })(this) - }).always((function(_this) { - return function() { - return _this.refreshing = false; - }; - })(this)); + axios.get(this.notes_url, { + headers: { + 'X-Last-Fetched-At': this.last_fetched_at, + }, + }).then(({ data }) => { + const notes = data.notes; + this.last_fetched_at = data.last_fetched_at; + this.setPollingInterval(data.notes.length); + $.each(notes, (i, note) => this.renderNote(note)); + + this.refreshing = false; + }).catch(() => { + this.refreshing = false; + }); } /** @@ -1404,7 +1399,7 @@ export default class Notes { * 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve * 3) Build temporary placeholder element (using `createPlaceholderNote`) * 4) Show placeholder note on UI - * 5) Perform network request to submit the note using `ajaxPost` + * 5) Perform network request to submit the note using `axios.post` * a) If request is successfully completed * 1. Remove placeholder element * 2. Show submitted Note element @@ -1486,8 +1481,10 @@ export default class Notes { /* eslint-disable promise/catch-or-return */ // Make request to submit comment on server - ajaxPost(formAction, formData) - .then((note) => { + axios.post(formAction, formData) + .then((res) => { + const note = res.data; + // Submission successful! remove placeholder $notesContainer.find(`#${noteUniqueId}`).remove(); @@ -1560,7 +1557,7 @@ export default class Notes { } $form.trigger('ajax:success', [note]); - }).fail(() => { + }).catch(() => { // Submission failed, remove placeholder note and show Flash error message $notesContainer.find(`#${noteUniqueId}`).remove(); @@ -1599,7 +1596,7 @@ export default class Notes { * * 1) Get Form metadata * 2) Update note element with new content - * 3) Perform network request to submit the updated note using `ajaxPost` + * 3) Perform network request to submit the updated note using `axios.post` * a) If request is successfully completed * 1. Show submitted Note element * b) If request failed @@ -1630,12 +1627,12 @@ export default class Notes { /* eslint-disable promise/catch-or-return */ // Make request to update comment on server - ajaxPost(formAction, formData) - .then((note) => { + axios.post(formAction, formData) + .then(({ data }) => { // Submission successful! render final note element - this.updateNote(note, $editingNote); + this.updateNote(data, $editingNote); }) - .fail(() => { + .catch(() => { // Submission failed, revert back to original note $noteBodyText.html(_.escape(cachedNoteBodyText)); $editingNote.removeClass('being-posted fade-in'); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index e594377bc40..3c8452ac808 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -15,7 +15,17 @@ import issuableStateMixin from '../mixins/issuable_state'; export default { - name: 'commentForm', + name: 'CommentForm', + components: { + issueWarning, + noteSignedOutWidget, + discussionLockedWidget, + markdownField, + userAvatarLink, + }, + mixins: [ + issuableStateMixin, + ], data() { return { note: '', @@ -27,21 +37,6 @@ isSubmitButtonDisabled: true, }; }, - components: { - issueWarning, - noteSignedOutWidget, - discussionLockedWidget, - markdownField, - userAvatarLink, - }, - watch: { - note(newNote) { - this.setIsSubmitButtonDisabled(newNote, this.isSubmitting); - }, - isSubmitting(newValue) { - this.setIsSubmitButtonDisabled(this.note, newValue); - }, - }, computed: { ...mapGetters([ 'getCurrentUserLastNote', @@ -65,7 +60,9 @@ if (this.note.length) { const actionText = this.isIssueOpen ? 'close' : 'reopen'; - return this.noteType === constants.COMMENT ? `Comment & ${actionText} issue` : `Start discussion & ${actionText} issue`; + return this.noteType === constants.COMMENT ? + `Comment & ${actionText} issue` : + `Start discussion & ${actionText} issue`; } return this.isIssueOpen ? 'Close issue' : 'Reopen issue'; @@ -97,6 +94,23 @@ return this.getNoteableData.create_note_path; }, }, + watch: { + note(newNote) { + this.setIsSubmitButtonDisabled(newNote, this.isSubmitting); + }, + isSubmitting(newValue) { + this.setIsSubmitButtonDisabled(this.note, newValue); + }, + }, + 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.initAutoSave(); + this.initTaskList(); + }, methods: { ...mapActions([ 'saveNote', @@ -159,7 +173,9 @@ .catch(() => { this.isSubmitting = false; this.discard(false); - const msg = 'Your comment could not be submitted! Please check your network connection and try again.'; + const msg = + `Your comment could not be submitted! +Please check your network connection and try again.`; Flash(msg, 'alert', this.$el); this.note = noteData.data.note.note; // Restore textarea content. this.removePlaceholderNotes(); @@ -207,7 +223,11 @@ }, initAutoSave() { if (this.isLoggedIn) { - this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getNoteableData.id], 'issue'); + this.autosave = new Autosave( + $(this.$refs.textarea), + ['Note', 'Issue', this.getNoteableData.id], + 'issue', + ); } }, initTaskList() { @@ -223,18 +243,6 @@ }); }, }, - mixins: [ - issuableStateMixin, - ], - 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.initAutoSave(); - this.initTaskList(); - }, }; </script> @@ -258,12 +266,12 @@ :img-src="author.avatar_url" :img-alt="author.name" :img-size="40" - /> + /> </div> <div class="timeline-content timeline-content-form"> <form ref="commentForm" - class="new-note js-quick-submit common-note-form gfm-form js-main-target-form" + class="new-note common-note-form gfm-form js-main-target-form" > <div class="error-alert"></div> @@ -283,7 +291,8 @@ <textarea id="note-body" name="note[note]" - class="note-textarea js-vue-comment-form js-gfm-input js-autosize markdown-area js-vue-textarea" + class="note-textarea js-vue-comment-form +js-gfm-input js-autosize markdown-area js-vue-textarea" data-supports-quick-actions="true" aria-label="Description" v-model="note" @@ -292,17 +301,20 @@ :disabled="isSubmitting" placeholder="Write a comment or drag your files here..." @keydown.up="editCurrentUserLastNote()" - @keydown.meta.enter="handleSave()"> + @keydown.meta.enter="handleSave()" + @keydown.ctrl.enter="handleSave()"> </textarea> </markdown-field> <div class="note-form-actions"> - <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> + <div + class="pull-left btn-group +append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> <button @click.prevent="handleSave()" :disabled="isSubmitButtonDisabled" class="btn btn-create comment-btn js-comment-button js-comment-submit-button" type="submit"> - {{commentButtonTitle}} + {{ commentButtonTitle }} </button> <button :disabled="isSubmitButtonDisabled" @@ -344,7 +356,7 @@ <i aria-hidden="true" class="fa fa-check icon"> - </i> + </i> <div class="description"> <strong>Start discussion</strong> <p> @@ -362,7 +374,7 @@ :class="actionButtonClassNames" :disabled="isSubmitting" class="btn btn-comment btn-comment-and-close js-action-button"> - {{issueActionButtonTitle}} + {{ issueActionButtonTitle }} </button> <button type="button" diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue index e6f7ee56ff3..fc0722042cc 100644 --- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue +++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue @@ -3,12 +3,12 @@ import Issuable from '~/vue_shared/mixins/issuable'; export default { - mixins: [ - Issuable, - ], components: { Icon, }, + mixins: [ + Issuable, + ], }; </script> @@ -18,9 +18,11 @@ <icon name="lock" :size="16" - class="icon"> - </icon> - <span>This {{ issuableDisplayName }} is locked. Only <b>project members</b> can comment.</span> - </span> + class="icon" + /> + <span> + This {{ issuableDisplayName }} is locked. Only <b>project members</b> can comment. + </span> + </span> </div> </template> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 7fb45ed4d4b..46ffb60aa60 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -9,7 +9,13 @@ import tooltip from '~/vue_shared/directives/tooltip'; export default { - name: 'noteActions', + name: 'NoteActions', + directives: { + tooltip, + }, + components: { + loadingIcon, + }, props: { authorId: { type: Number, @@ -41,12 +47,6 @@ required: true, }, }, - directives: { - tooltip, - }, - components: { - loadingIcon, - }, computed: { ...mapGetters([ 'getUserDataByProp', @@ -64,6 +64,13 @@ return this.getUserDataByProp('id'); }, }, + created() { + this.emojiSmiling = emojiSmiling; + this.emojiSmile = emojiSmile; + this.emojiSmiley = emojiSmiley; + this.editSvg = editSvg; + this.ellipsisSvg = ellipsisSvg; + }, methods: { onEdit() { this.$emit('handleEdit'); @@ -72,13 +79,6 @@ this.$emit('handleDelete'); }, }, - created() { - this.emojiSmiling = emojiSmiling; - this.emojiSmile = emojiSmile; - this.emojiSmiley = emojiSmiley; - this.editSvg = editSvg; - this.ellipsisSvg = ellipsisSvg; - }, }; </script> @@ -86,7 +86,9 @@ <div class="note-actions"> <span v-if="accessLevel" - class="note-role user-access-role">{{accessLevel}}</span> + class="note-role user-access-role"> + {{ accessLevel }} + </span> <div v-if="canAddAwardEmoji" class="note-actions-item"> @@ -98,20 +100,21 @@ data-placement="bottom" data-container="body" href="#" - title="Add reaction"> - <loading-icon :inline="true" /> - <span - v-html="emojiSmiling" - class="link-highlight award-control-icon-neutral"> - </span> - <span - v-html="emojiSmiley" - class="link-highlight award-control-icon-positive"> - </span> - <span - v-html="emojiSmile" - class="link-highlight award-control-icon-super-positive"> - </span> + title="Add reaction" + > + <loading-icon :inline="true" /> + <span + v-html="emojiSmiling" + class="link-highlight award-control-icon-neutral"> + </span> + <span + v-html="emojiSmiley" + class="link-highlight award-control-icon-positive"> + </span> + <span + v-html="emojiSmile" + class="link-highlight award-control-icon-super-positive"> + </span> </a> </div> <div @@ -125,9 +128,10 @@ class="note-action-button js-note-edit btn btn-transparent" data-container="body" data-placement="bottom"> - <span - v-html="editSvg" - class="link-highlight"></span> + <span + v-html="editSvg" + class="link-highlight"> + </span> </button> </div> <div @@ -141,9 +145,10 @@ data-toggle="dropdown" data-container="body" data-placement="bottom"> - <span - class="icon" - v-html="ellipsisSvg"></span> + <span + class="icon" + v-html="ellipsisSvg"> + </span> </button> <ul class="dropdown-menu more-actions-dropdown dropdown-open-left"> <li v-if="canReportAsAbuse"> diff --git a/app/assets/javascripts/notes/components/note_attachment.vue b/app/assets/javascripts/notes/components/note_attachment.vue index cd9571a4002..618b807b9cc 100644 --- a/app/assets/javascripts/notes/components/note_attachment.vue +++ b/app/assets/javascripts/notes/components/note_attachment.vue @@ -1,6 +1,6 @@ <script> export default { - name: 'noteAttachment', + name: 'NoteAttachment', props: { attachment: { type: Object, @@ -19,7 +19,8 @@ rel="noopener noreferrer"> <img :src="attachment.url" - class="note-image-attach" /> + class="note-image-attach" + /> </a> <div class="attachment"> <a @@ -29,8 +30,9 @@ rel="noopener noreferrer"> <i class="fa fa-paperclip" - aria-hidden="true"></i> - {{attachment.filename}} + aria-hidden="true"> + </i> + {{ attachment.filename }} </a> </div> </div> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index c3a340139e7..caa9701e03f 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -8,6 +8,9 @@ import tooltip from '../../vue_shared/directives/tooltip'; export default { + directives: { + tooltip, + }, props: { awards: { type: Array, @@ -26,9 +29,6 @@ required: true, }, }, - directives: { - tooltip, - }, computed: { ...mapGetters([ 'getUserData', @@ -73,6 +73,11 @@ return this.getUserData.id; }, }, + created() { + this.emojiSmiling = emojiSmiling; + this.emojiSmile = emojiSmile; + this.emojiSmiley = emojiSmiley; + }, methods: { ...mapActions([ 'toggleAwardRequest', @@ -168,11 +173,6 @@ .catch(() => Flash('Something went wrong on our end.')); }, }, - created() { - this.emojiSmiling = emojiSmiling; - this.emojiSmile = emojiSmile; - this.emojiSmiley = emojiSmiley; - }, }; </script> @@ -191,7 +191,7 @@ type="button"> <span v-html="getAwardHTML(awardName)"></span> <span class="award-control-text js-counter"> - {{awardList.length}} + {{ awardList.length }} </span> </button> <div diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index ac4e1ffe53a..2d7cd30115d 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -7,6 +7,15 @@ import autosave from '../mixins/autosave'; export default { + components: { + noteEditedText, + noteAwardsList, + noteAttachment, + noteForm, + }, + mixins: [ + autosave, + ], props: { note: { type: Object, @@ -22,40 +31,11 @@ default: false, }, }, - mixins: [ - autosave, - ], - components: { - noteEditedText, - noteAwardsList, - noteAttachment, - noteForm, - }, computed: { noteBody() { return this.note.note; }, }, - methods: { - renderGFM() { - $(this.$refs['note-body']).renderGFM(); - }, - initTaskList() { - if (this.canEdit) { - this.taskList = new TaskList({ - dataType: 'note', - fieldName: 'note', - selector: '.notes', - }); - } - }, - handleFormUpdate(note, parentElement, callback) { - this.$emit('handleFormUpdate', note, parentElement, callback); - }, - formCancelHandler(shouldConfirm, isDirty) { - this.$emit('cancelFormEdition', shouldConfirm, isDirty); - }, - }, mounted() { this.renderGFM(); this.initTaskList(); @@ -76,6 +56,26 @@ } } }, + methods: { + renderGFM() { + $(this.$refs['note-body']).renderGFM(); + }, + initTaskList() { + if (this.canEdit) { + this.taskList = new TaskList({ + dataType: 'note', + fieldName: 'note', + selector: '.notes', + }); + } + }, + handleFormUpdate(note, parentElement, callback) { + this.$emit('handleFormUpdate', note, parentElement, callback); + }, + formCancelHandler(shouldConfirm, isDirty) { + this.$emit('cancelFormEdition', shouldConfirm, isDirty); + }, + }, }; </script> @@ -95,7 +95,7 @@ :is-editing="isEditing" :note-body="noteBody" :note-id="note.id" - /> + /> <textarea v-if="canEdit" v-model="note.note" @@ -106,17 +106,17 @@ :edited-at="note.last_edited_at" :edited-by="note.last_edited_by" action-text="Edited" - /> + /> <note-awards-list v-if="note.award_emoji.length" :note-id="note.id" :note-author-id="note.author.id" :awards="note.award_emoji" :toggle-award-path="note.toggle_award_path" - /> + /> <note-attachment v-if="note.attachment" :attachment="note.attachment" - /> + /> </div> </template> diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue index 49e09f0ecc5..ae2e52554d2 100644 --- a/app/assets/javascripts/notes/components/note_edited_text.vue +++ b/app/assets/javascripts/notes/components/note_edited_text.vue @@ -2,7 +2,10 @@ import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; export default { - name: 'editedNoteText', + name: 'EditedNoteText', + components: { + timeAgoTooltip, + }, props: { actionText: { type: String, @@ -15,6 +18,7 @@ editedBy: { type: Object, required: false, + default: () => ({}), }, className: { type: String, @@ -22,25 +26,22 @@ default: 'edited-text', }, }, - components: { - timeAgoTooltip, - }, }; </script> <template> <div :class="className"> - {{actionText}} + {{ actionText }} <time-ago-tooltip :time="editedAt" tooltip-placement="bottom" - /> + /> <template v-if="editedBy"> by <a :href="editedBy.path" class="js-vue-author author_link"> - {{editedBy.name}} + {{ editedBy.name }} </a> </template> </div> diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 4d527cb6643..d382a9bb642 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -6,7 +6,14 @@ import issuableStateMixin from '../mixins/issuable_state'; export default { - name: 'issueNoteForm', + name: 'IssueNoteForm', + components: { + issueWarning, + markdownField, + }, + mixins: [ + issuableStateMixin, + ], props: { noteBody: { type: String, @@ -16,6 +23,7 @@ noteId: { type: Number, required: false, + default: 0, }, saveButtonTitle: { type: String, @@ -39,10 +47,6 @@ isSubmitting: false, }; }, - components: { - issueWarning, - markdownField, - }, computed: { ...mapGetters([ 'getDiscussionLastNote', @@ -70,6 +74,18 @@ return !this.note.length || this.isSubmitting; }, }, + watch: { + noteBody() { + if (this.note === this.noteBody) { + this.note = this.noteBody; + } else { + this.conflictWhileEditing = true; + } + }, + }, + mounted() { + this.$refs.textarea.focus(); + }, methods: { handleUpdate() { this.isSubmitting = true; @@ -94,26 +110,13 @@ this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note); }, }, - mixins: [ - issuableStateMixin, - ], - mounted() { - this.$refs.textarea.focus(); - }, - watch: { - noteBody() { - if (this.note === this.noteBody) { - this.note = this.noteBody; - } else { - this.conflictWhileEditing = true; - } - }, - }, }; </script> <template> - <div ref="editNoteForm" class="note-edit-form current-note-edit-form"> + <div + ref="editNoteForm" + class="note-edit-form current-note-edit-form"> <div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger"> @@ -121,12 +124,13 @@ <a :href="noteHash" target="_blank" - rel="noopener noreferrer">updated comment</a> - to ensure information is not lost. + rel="noopener noreferrer"> + updated comment + </a> + to ensure information is not lost. </div> <div class="flash-container timeline-content"></div> - <form - class="edit-note common-note-form js-quick-submit gfm-form"> + <form class="edit-note common-note-form js-quick-submit gfm-form"> <issue-warning v-if="hasWarning(getNoteableData)" @@ -142,7 +146,8 @@ <textarea id="note_note" name="note[note]" - class="note-textarea js-gfm-input js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" + class="note-textarea js-gfm-input +js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" :data-supports-quick-actions="!isEditing" aria-label="Description" v-model="note" @@ -150,6 +155,7 @@ slot="textarea" placeholder="Write a comment or drag your files here..." @keydown.meta.enter="handleUpdate()" + @keydown.ctrl.enter="handleUpdate()" @keydown.up="editMyLastNote()" @keydown.esc="cancelHandler(true)"> </textarea> @@ -160,7 +166,7 @@ @click="handleUpdate()" :disabled="isDisabled" class="js-vue-issue-save btn btn-save"> - {{saveButtonTitle}} + {{ saveButtonTitle }} </button> <button @click="cancelHandler()" diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index 63aa3d777d0..5b255d4a710 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -3,6 +3,9 @@ import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; export default { + components: { + timeAgoTooltip, + }, props: { author: { type: Object, @@ -37,9 +40,6 @@ isExpanded: true, }; }, - components: { - timeAgoTooltip, - }, computed: { toggleChevronClass() { return this.isExpanded ? 'fa-chevron-up' : 'fa-chevron-down'; @@ -66,17 +66,15 @@ <template> <div class="note-header-info"> <a :href="author.path"> - <span class="note-header-author-name"> - {{author.name}} - </span> + <span class="note-header-author-name">{{ author.name }}</span> <span class="note-headline-light"> - @{{author.username}} + @{{ author.username }} </span> </a> <span class="note-headline-light"> <span class="note-headline-meta"> <template v-if="actionText"> - {{actionText}} + {{ actionText }} </template> <span v-if="actionTextHtml" @@ -90,12 +88,13 @@ <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" - /> + /> </a> <i class="fa fa-spinner fa-spin editing-spinner" aria-label="Comment is being updated" - aria-hidden="true"> + aria-hidden="true" + > </i> </span> </span> @@ -106,12 +105,12 @@ @click="handleToggle" class="note-action-button discussion-toggle-button js-vue-toggle-button" type="button"> - <i - :class="toggleChevronClass" - class="fa" - aria-hidden="true"> - </i> - Toggle discussion + <i + :class="toggleChevronClass" + class="fa" + aria-hidden="true"> + </i> + Toggle discussion </button> </div> </div> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 11e8f805635..98a06c5fc71 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -13,17 +13,6 @@ import autosave from '../mixins/autosave'; export default { - props: { - note: { - type: Object, - required: true, - }, - }, - data() { - return { - isReplying: false, - }; - }, components: { noteableNote, userAvatarLink, @@ -37,6 +26,17 @@ mixins: [ autosave, ], + props: { + note: { + type: Object, + required: true, + }, + }, + data() { + return { + isReplying: false, + }; + }, computed: { ...mapGetters([ 'getNoteableData', @@ -72,6 +72,20 @@ return null; }, }, + mounted() { + if (this.isReplying) { + this.initAutoSave(); + } + }, + updated() { + if (this.isReplying) { + if (!this.autosave) { + this.initAutoSave(); + } else { + this.setAutoSave(); + } + } + }, methods: { ...mapActions([ 'saveNote', @@ -130,7 +144,8 @@ this.removePlaceholderNotes(); this.isReplying = true; this.$nextTick(() => { - const msg = 'Your comment could not be submitted! Please check your network connection and try again.'; + const msg = `Your comment could not be submitted! +Please check your network connection and try again.`; Flash(msg, 'alert', this.$el); this.$refs.noteForm.note = noteText; callback(err); @@ -138,20 +153,6 @@ }); }, }, - mounted() { - if (this.isReplying) { - this.initAutoSave(); - } - }, - updated() { - if (this.isReplying) { - if (!this.autosave) { - this.initAutoSave(); - } else { - this.setAutoSave(); - } - } - }, }; </script> @@ -164,7 +165,7 @@ :img-src="author.avatar_url" :img-alt="author.name" :img-size="40" - /> + /> </div> <div class="timeline-content"> <div class="discussion"> @@ -184,42 +185,43 @@ :edited-by="lastUpdatedBy" action-text="Last updated" 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> + </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> </div> </div> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 9186d6ff64a..30e7ccc8229 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -9,6 +9,12 @@ import eventHub from '../event_hub'; export default { + components: { + userAvatarLink, + noteHeader, + noteActions, + noteBody, + }, props: { note: { type: Object, @@ -22,12 +28,6 @@ isRequesting: false, }; }, - components: { - userAvatarLink, - noteHeader, - noteActions, - noteBody, - }, computed: { ...mapGetters([ 'targetNoteHash', @@ -51,6 +51,16 @@ return `note_${this.note.id}`; }, }, + + created() { + eventHub.$on('enterEditMode', ({ noteId }) => { + if (noteId === this.note.id) { + this.isEditing = true; + this.scrollToNoteIfNeeded($(this.$el)); + } + }); + }, + methods: { ...mapActions([ 'deleteNote', @@ -126,14 +136,6 @@ this.$refs.noteBody.$refs.noteForm.note = noteText; }, }, - created() { - eventHub.$on('enterEditMode', ({ noteId }) => { - if (noteId === this.note.id) { - this.isEditing = true; - this.scrollToNoteIfNeeded($(this.$el)); - } - }); - }, }; </script> @@ -150,7 +152,7 @@ :img-src="author.avatar_url" :img-alt="author.name" :img-size="40" - /> + /> </div> <div class="timeline-content"> <div class="note-header"> @@ -159,7 +161,7 @@ :created-at="note.created_at" :note-id="note.id" action-text="commented" - /> + /> <note-actions :author-id="author.id" :note-id="note.id" @@ -170,7 +172,7 @@ :report-abuse-path="note.report_abuse_path" @handleEdit="editHandler" @handleDelete="deleteHandler" - /> + /> </div> <note-body :note="note" @@ -179,7 +181,7 @@ @handleFormUpdate="formUpdateHandler" @cancelFormEdition="formCancelHandler" ref="noteBody" - /> + /> </div> </div> </li> diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index c4cae4b3b6f..92db4830704 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -13,7 +13,16 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { - name: 'notesApp', + name: 'NotesApp', + components: { + noteableNote, + noteableDiscussion, + systemNote, + commentForm, + loadingIcon, + placeholderNote, + placeholderSystemNote, + }, props: { noteableData: { type: Object, @@ -26,7 +35,7 @@ userData: { type: Object, required: false, - default: {}, + default: () => ({}), }, }, store, @@ -35,21 +44,30 @@ isLoading: true, }; }, - components: { - noteableNote, - noteableDiscussion, - systemNote, - commentForm, - loadingIcon, - placeholderNote, - placeholderSystemNote, - }, computed: { ...mapGetters([ 'notes', 'getNotesDataByProp', ]), }, + created() { + this.setNotesData(this.notesData); + this.setNoteableData(this.noteableData); + this.setUserData(this.userData); + }, + mounted() { + this.fetchNotes(); + + const parentElement = this.$el.parentElement; + + if (parentElement && + parentElement.classList.contains('js-vue-notes-event')) { + parentElement.addEventListener('toggleAward', (event) => { + const { awardName, noteId } = event.detail; + this.actionToggleAward({ awardName, noteId }); + }); + } + }, methods: { ...mapActions({ actionFetchNotes: 'fetchNotes', @@ -105,24 +123,6 @@ } }, }, - created() { - this.setNotesData(this.notesData); - this.setNoteableData(this.noteableData); - this.setUserData(this.userData); - }, - mounted() { - this.fetchNotes(); - - const parentElement = this.$el.parentElement; - - if (parentElement && - parentElement.classList.contains('js-vue-notes-event')) { - parentElement.addEventListener('toggleAward', (event) => { - const { awardName, noteId } = event.detail; - this.actionToggleAward({ awardName, noteId }); - }); - } - }, }; </script> @@ -144,7 +144,7 @@ :is="getComponentName(note)" :note="getComponentData(note)" :key="note.id" - /> + /> </ul> <comment-form /> diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js index 4534360d577..4e0afe13590 100644 --- a/app/assets/javascripts/notifications_form.js +++ b/app/assets/javascripts/notifications_form.js @@ -1,3 +1,7 @@ +import { __ } from './locale'; +import axios from './lib/utils/axios_utils'; +import flash from './flash'; + export default class NotificationsForm { constructor() { this.toggleCheckbox = this.toggleCheckbox.bind(this); @@ -27,24 +31,20 @@ export default class NotificationsForm { saveEvent($checkbox, $parent) { const form = $parent.parents('form:first'); - return $.ajax({ - url: form.attr('action'), - method: form.attr('method'), - dataType: 'json', - data: form.serialize(), - beforeSend: () => { - this.showCheckboxLoadingSpinner($parent); - }, - }).done((data) => { - $checkbox.enable(); - if (data.saved) { - $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done'); - setTimeout(() => { - $parent.removeClass('is-loading') - .find('.custom-notification-event-loading') - .toggleClass('fa-spin fa-spinner fa-check is-done'); - }, 2000); - } - }); + this.showCheckboxLoadingSpinner($parent); + + axios[form.attr('method')](form.attr('action'), form.serialize()) + .then(({ data }) => { + $checkbox.enable(); + if (data.saved) { + $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done'); + setTimeout(() => { + $parent.removeClass('is-loading') + .find('.custom-notification-event-loading') + .toggleClass('fa-spin fa-spinner fa-check is-done'); + }, 2000); + } + }) + .catch(() => flash(__('There was an error saving your notification settings.'))); } } diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index 6552a88b606..fd3105b1960 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -1,4 +1,5 @@ import { getParameterByName } from '~/lib/utils/common_utils'; +import axios from './lib/utils/axios_utils'; import { removeParams } from './lib/utils/url_utility'; const ENDLESS_SCROLL_BOTTOM_PX = 400; @@ -22,24 +23,22 @@ export default { getOld() { this.loading.show(); - $.ajax({ - type: 'GET', - url: this.url, - data: `limit=${this.limit}&offset=${this.offset}`, - dataType: 'json', - error: () => this.loading.hide(), - success: (data) => { - this.append(data.count, this.prepareData(data.html)); - this.callback(); - - // keep loading until we've filled the viewport height - if (!this.disable && !this.isScrollable()) { - this.getOld(); - } else { - this.loading.hide(); - } + axios.get(this.url, { + params: { + limit: this.limit, + offset: this.offset, }, - }); + }).then(({ data }) => { + this.append(data.count, this.prepareData(data.html)); + this.callback(); + + // keep loading until we've filled the viewport height + if (!this.disable && !this.isScrollable()) { + this.getOld(); + } else { + this.loading.hide(); + } + }).catch(() => this.loading.hide()); }, append(count, html) { diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js index d2d3a257c0d..d87e6304a24 100644 --- a/app/assets/javascripts/abuse_reports.js +++ b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js @@ -1,4 +1,4 @@ -import { truncate } from './lib/utils/text_utility'; +import { truncate } from '../../../lib/utils/text_utility'; const MAX_MESSAGE_LENGTH = 500; const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; diff --git a/app/assets/javascripts/pages/admin/abuse_reports/index.js b/app/assets/javascripts/pages/admin/abuse_reports/index.js new file mode 100644 index 00000000000..c0b6e8d4095 --- /dev/null +++ b/app/assets/javascripts/pages/admin/abuse_reports/index.js @@ -0,0 +1,3 @@ +import AbuseReports from './abuse_reports'; + +export default () => new AbuseReports(); diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/pages/admin/admin.js index c1f7fa2aced..135c15c346b 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/pages/admin/admin.js @@ -1,4 +1,4 @@ -import { refreshCurrentPage } from './lib/utils/url_utility'; +import { refreshCurrentPage } from '../../lib/utils/url_utility'; function showBlacklistType() { if ($('input[name="blacklist_type"]:checked').val() === 'file') { diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js index ff88083a4b4..885acfac6d0 100644 --- a/app/assets/javascripts/broadcast_message.js +++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js @@ -1,3 +1,8 @@ +import _ from 'underscore'; +import axios from '~/lib/utils/axios_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; + export default function initBroadcastMessagesForm() { $('input#broadcast_message_color').on('input', function onMessageColorInput() { const previewColor = $(this).val(); @@ -16,13 +21,15 @@ export default function initBroadcastMessagesForm() { if (message === '') { $('.js-broadcast-message-preview').text('Your message here'); } else { - $.ajax({ - url: previewPath, - type: 'POST', - data: { - broadcast_message: { message }, + axios.post(previewPath, { + broadcast_message: { + message, }, - }); + }) + .then(({ data }) => { + $('.js-broadcast-message-preview').html(data.message); + }) + .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 new file mode 100644 index 00000000000..b548c48282a --- /dev/null +++ b/app/assets/javascripts/pages/admin/broadcast_messages/index.js @@ -0,0 +1,3 @@ +import initBroadcastMessagesForm from './broadcast_message'; + +export default () => initBroadcastMessagesForm(); diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/admin/cohorts/index.js new file mode 100644 index 00000000000..42ef9d38ef7 --- /dev/null +++ b/app/assets/javascripts/pages/admin/cohorts/index.js @@ -0,0 +1,3 @@ +import initUsagePing from './usage_ping'; + +export default () => initUsagePing(); diff --git a/app/assets/javascripts/pages/admin/cohorts/usage_ping.js b/app/assets/javascripts/pages/admin/cohorts/usage_ping.js new file mode 100644 index 00000000000..914a9661c27 --- /dev/null +++ b/app/assets/javascripts/pages/admin/cohorts/usage_ping.js @@ -0,0 +1,13 @@ +import axios from '../../../lib/utils/axios_utils'; +import { __ } from '../../../locale'; +import flash from '../../../flash'; + +export default function UsagePing() { + const el = document.querySelector('.usage-data'); + + axios.get(el.dataset.endpoint, { + responseType: 'text', + }).then(({ data }) => { + el.innerHTML = data; + }).catch(() => flash(__('Error fetching usage ping data.'))); +} 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 new file mode 100644 index 00000000000..6e66ef69fe1 --- /dev/null +++ b/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js @@ -0,0 +1,3 @@ +import UserCallout from '../../../../user_callout'; + +export default () => new UserCallout(); diff --git a/app/assets/javascripts/pages/admin/groups/edit/index.js b/app/assets/javascripts/pages/admin/groups/edit/index.js new file mode 100644 index 00000000000..ff9ef8d2449 --- /dev/null +++ b/app/assets/javascripts/pages/admin/groups/edit/index.js @@ -0,0 +1,3 @@ +import groupAvatar from '../../../../group_avatar'; + +export default () => groupAvatar(); diff --git a/app/assets/javascripts/pages/admin/groups/new/index.js b/app/assets/javascripts/pages/admin/groups/new/index.js new file mode 100644 index 00000000000..fb5c46e4729 --- /dev/null +++ b/app/assets/javascripts/pages/admin/groups/new/index.js @@ -0,0 +1,9 @@ +import BindInOut from '../../../../behaviors/bind_in_out'; +import Group from '../../../../group'; +import groupAvatar from '../../../../group_avatar'; + +export default () => { + 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 new file mode 100644 index 00000000000..5defea104d4 --- /dev/null +++ b/app/assets/javascripts/pages/admin/groups/show/index.js @@ -0,0 +1,3 @@ +import UsersSelect from '../../../../users_select'; + +export default () => new UsersSelect(); diff --git a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js new file mode 100644 index 00000000000..030328a1363 --- /dev/null +++ b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js @@ -0,0 +1,3 @@ +import DueDateSelectors from '../../../due_date_select'; + +export default () => new DueDateSelectors(); diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js new file mode 100644 index 00000000000..8b843037d85 --- /dev/null +++ b/app/assets/javascripts/pages/admin/index.js @@ -0,0 +1,3 @@ +import initAdmin from './admin'; + +export default () => 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 new file mode 100644 index 00000000000..555725cbe12 --- /dev/null +++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue @@ -0,0 +1,47 @@ +<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 { redirectTo } from '~/lib/utils/url_utility'; + + export default { + components: { + modal, + }, + props: { + url: { + type: String, + required: true, + }, + }, + computed: { + text() { + return s__('AdminArea|You’re about to stop all jobs. This will halt all current jobs that are running.'); + }, + }, + methods: { + onSubmit() { + return axios.post(this.url) + .then((response) => { + // follow the rediect to refresh the page + redirectTo(response.request.responseURL); + }) + .catch((error) => { + Flash(s__('AdminArea|Stopping jobs failed')); + throw error; + }); + }, + }, + }; +</script> + +<template> + <modal + id="stop-jobs-modal" + :title="s__('AdminArea|Stop all jobs?')" + :text="text" + kind="danger" + :primary-button-label="s__('AdminArea|Stop jobs')" + @submit="onSubmit" /> +</template> diff --git a/app/assets/javascripts/pages/admin/jobs/index/index.js b/app/assets/javascripts/pages/admin/jobs/index/index.js new file mode 100644 index 00000000000..0e004bd9174 --- /dev/null +++ b/app/assets/javascripts/pages/admin/jobs/index/index.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; + +import Translate from '~/vue_shared/translate'; + +import stopJobsModal from './components/stop_jobs_modal.vue'; + +Vue.use(Translate); + +export default () => { + 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, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/pages/admin/labels/edit/index.js b/app/assets/javascripts/pages/admin/labels/edit/index.js new file mode 100644 index 00000000000..d7ec6e47f67 --- /dev/null +++ b/app/assets/javascripts/pages/admin/labels/edit/index.js @@ -0,0 +1,3 @@ +import Labels from '../../../../labels'; + +export default () => new Labels(); diff --git a/app/assets/javascripts/pages/admin/labels/new/index.js b/app/assets/javascripts/pages/admin/labels/new/index.js new file mode 100644 index 00000000000..d7ec6e47f67 --- /dev/null +++ b/app/assets/javascripts/pages/admin/labels/new/index.js @@ -0,0 +1,3 @@ +import Labels from '../../../../labels'; + +export default () => new Labels(); diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js new file mode 100644 index 00000000000..71e0ddcd7b6 --- /dev/null +++ b/app/assets/javascripts/pages/admin/projects/index.js @@ -0,0 +1,9 @@ +import ProjectsList from '../../../projects_list'; +import NamespaceSelect from '../../../namespace_select'; + +export default () => { + new ProjectsList(); // eslint-disable-line no-new + + document.querySelectorAll('.js-namespace-select') + .forEach(dropdown => new NamespaceSelect({ dropdown })); +}; diff --git a/app/assets/javascripts/ci_lint_editor.js b/app/assets/javascripts/pages/ci/lints/ci_lint_editor.js index b9469e5b7cb..b9469e5b7cb 100644 --- a/app/assets/javascripts/ci_lint_editor.js +++ b/app/assets/javascripts/pages/ci/lints/ci_lint_editor.js diff --git a/app/assets/javascripts/pages/ci/lints/index.js b/app/assets/javascripts/pages/ci/lints/index.js new file mode 100644 index 00000000000..5cc66546109 --- /dev/null +++ b/app/assets/javascripts/pages/ci/lints/index.js @@ -0,0 +1,3 @@ +import CILintEditor from './ci_lint_editor'; + +export default () => new CILintEditor(); diff --git a/app/assets/javascripts/pages/constants.js b/app/assets/javascripts/pages/constants.js new file mode 100644 index 00000000000..328b6541636 --- /dev/null +++ b/app/assets/javascripts/pages/constants.js @@ -0,0 +1,6 @@ +/* eslint-disable import/prefer-default-export */ + +export const FILTERED_SEARCH = { + MERGE_REQUESTS: 'merge_requests', + ISSUES: 'issues', +}; diff --git a/app/assets/javascripts/pages/dashboard/activity/index.js b/app/assets/javascripts/pages/dashboard/activity/index.js new file mode 100644 index 00000000000..95faf1f1e98 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/activity/index.js @@ -0,0 +1,3 @@ +import Activities from '~/activities'; + +export default () => new Activities(); diff --git a/app/assets/javascripts/pages/dashboard/groups/index/index.js b/app/assets/javascripts/pages/dashboard/groups/index/index.js new file mode 100644 index 00000000000..8a2aae706c0 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/groups/index/index.js @@ -0,0 +1,5 @@ +import initGroupsList from '../../../../groups'; + +export default () => { + initGroupsList(); +}; diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js new file mode 100644 index 00000000000..b7353669e65 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/issues/index.js @@ -0,0 +1,7 @@ +import projectSelect from '~/project_select'; +import initLegacyFilters from '~/init_legacy_filters'; + +export default () => { + projectSelect(); + initLegacyFilters(); +}; diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js new file mode 100644 index 00000000000..b7353669e65 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js @@ -0,0 +1,7 @@ +import projectSelect from '~/project_select'; +import initLegacyFilters from '~/init_legacy_filters'; + +export default () => { + projectSelect(); + initLegacyFilters(); +}; diff --git a/app/assets/javascripts/pages/dashboard/milestones/index/index.js b/app/assets/javascripts/pages/dashboard/milestones/index/index.js new file mode 100644 index 00000000000..38ddebe30d9 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/milestones/index/index.js @@ -0,0 +1,3 @@ +import projectSelect from '~/project_select'; + +document.addEventListener('DOMContentLoaded', projectSelect); diff --git a/app/assets/javascripts/pages/dashboard/milestones/show/index.js b/app/assets/javascripts/pages/dashboard/milestones/show/index.js new file mode 100644 index 00000000000..2e7a08a369c --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/milestones/show/index.js @@ -0,0 +1,7 @@ +import Milestone from '~/milestone'; +import Sidebar from '~/right_sidebar'; + +export default () => { + new Milestone(); // eslint-disable-line no-new + new Sidebar(); // 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 new file mode 100644 index 00000000000..c88cbf1a6ba --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/projects/index.js @@ -0,0 +1,3 @@ +import ProjectsList from '~/projects_list'; + +export default () => new ProjectsList(); diff --git a/app/assets/javascripts/pages/dashboard/todos/index/index.js b/app/assets/javascripts/pages/dashboard/todos/index/index.js new file mode 100644 index 00000000000..77c23685943 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/todos/index/index.js @@ -0,0 +1,3 @@ +import Todos from './todos'; + +export default () => new Todos(); diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index 748caecf153..b3f6a72fdcb 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -1,7 +1,10 @@ /* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */ -import { visitUrl } from './lib/utils/url_utility'; -import UsersSelect from './users_select'; -import { isMetaClick } from './lib/utils/common_utils'; +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'; export default class Todos { constructor() { @@ -59,18 +62,12 @@ export default class Todos { const target = e.target; target.setAttribute('disabled', true); target.classList.add('disabled'); - $.ajax({ - type: 'POST', - url: target.dataset.href, - dataType: 'json', - data: { - '_method': target.dataset.method, - }, - success: (data) => { + + axios[target.dataset.method](target.dataset.href) + .then(({ data }) => { this.updateRowState(target); - return this.updateBadges(data); - }, - }); + this.updateBadges(data); + }).catch(() => flash(__('Error updating todo status.'))); } updateRowState(target) { @@ -98,19 +95,15 @@ export default class Todos { e.preventDefault(); const target = e.currentTarget; - const requestData = { '_method': target.dataset.method, ids: this.todo_ids }; target.setAttribute('disabled', true); target.classList.add('disabled'); - $.ajax({ - type: 'POST', - url: target.dataset.href, - dataType: 'json', - data: requestData, - success: (data) => { - this.updateAllState(target, data); - return this.updateBadges(data); - }, - }); + + axios[target.dataset.method](target.dataset.href, { + ids: this.todo_ids, + }).then(({ data }) => { + this.updateAllState(target, data); + this.updateBadges(data); + }).catch(() => flash(__('Error updating status for all todos.'))); } updateAllState(target, data) { diff --git a/app/assets/javascripts/pages/explore/groups/index.js b/app/assets/javascripts/pages/explore/groups/index.js new file mode 100644 index 00000000000..e59c38b8bc4 --- /dev/null +++ b/app/assets/javascripts/pages/explore/groups/index.js @@ -0,0 +1,16 @@ +import GroupsList from '~/groups_list'; +import Landing from '~/landing'; +import initGroupsList from '../../../groups'; + +export default function () { + new GroupsList(); // eslint-disable-line no-new + initGroupsList(); + const landingElement = document.querySelector('.js-explore-groups-landing'); + if (!landingElement) return; + const exploreGroupsLanding = new Landing( + landingElement, + landingElement.querySelector('.dismiss-button'), + '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 new file mode 100644 index 00000000000..c88cbf1a6ba --- /dev/null +++ b/app/assets/javascripts/pages/explore/projects/index.js @@ -0,0 +1,3 @@ +import ProjectsList from '~/projects_list'; + +export default () => new ProjectsList(); diff --git a/app/assets/javascripts/pages/groups/activity/index.js b/app/assets/javascripts/pages/groups/activity/index.js new file mode 100644 index 00000000000..95faf1f1e98 --- /dev/null +++ b/app/assets/javascripts/pages/groups/activity/index.js @@ -0,0 +1,3 @@ +import Activities from '~/activities'; + +export default () => new Activities(); diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js new file mode 100644 index 00000000000..48e8c9550bf --- /dev/null +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -0,0 +1,3 @@ +import groupAvatar from '~/group_avatar'; + +export default groupAvatar; diff --git a/app/assets/javascripts/pages/groups/group_members/index/index.js b/app/assets/javascripts/pages/groups/group_members/index/index.js new file mode 100644 index 00000000000..29319b97ae2 --- /dev/null +++ b/app/assets/javascripts/pages/groups/group_members/index/index.js @@ -0,0 +1,11 @@ +/* eslint-disable no-new */ + +import memberExpirationDate from '~/member_expiration_date'; +import Members from '~/members'; +import UsersSelect from '~/users_select'; + +export default () => { + 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 new file mode 100644 index 00000000000..78db543a64d --- /dev/null +++ b/app/assets/javascripts/pages/groups/issues/index.js @@ -0,0 +1,8 @@ +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); + projectSelect(); +}; diff --git a/app/assets/javascripts/pages/groups/labels/edit/index.js b/app/assets/javascripts/pages/groups/labels/edit/index.js new file mode 100644 index 00000000000..72c5e4744ac --- /dev/null +++ b/app/assets/javascripts/pages/groups/labels/edit/index.js @@ -0,0 +1,3 @@ +import Labels from '~/labels'; + +export default () => new Labels(); diff --git a/app/assets/javascripts/pages/groups/labels/index/index.js b/app/assets/javascripts/pages/groups/labels/index/index.js new file mode 100644 index 00000000000..018345fa112 --- /dev/null +++ b/app/assets/javascripts/pages/groups/labels/index/index.js @@ -0,0 +1,3 @@ +import initLabels from '~/init_labels'; + +export default initLabels; diff --git a/app/assets/javascripts/pages/groups/labels/new/index.js b/app/assets/javascripts/pages/groups/labels/new/index.js new file mode 100644 index 00000000000..72c5e4744ac --- /dev/null +++ b/app/assets/javascripts/pages/groups/labels/new/index.js @@ -0,0 +1,3 @@ +import Labels from '~/labels'; + +export default () => new Labels(); diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js new file mode 100644 index 00000000000..9b3af4537e7 --- /dev/null +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -0,0 +1,8 @@ +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); + projectSelect(); +}; diff --git a/app/assets/javascripts/pages/groups/milestones/edit/index.js b/app/assets/javascripts/pages/groups/milestones/edit/index.js new file mode 100644 index 00000000000..5c99c90e24d --- /dev/null +++ b/app/assets/javascripts/pages/groups/milestones/edit/index.js @@ -0,0 +1,3 @@ +import initForm from '../../../../shared/milestones/form'; + +export default () => initForm(false); diff --git a/app/assets/javascripts/pages/groups/milestones/new/index.js b/app/assets/javascripts/pages/groups/milestones/new/index.js new file mode 100644 index 00000000000..5c99c90e24d --- /dev/null +++ b/app/assets/javascripts/pages/groups/milestones/new/index.js @@ -0,0 +1,3 @@ +import initForm from '../../../../shared/milestones/form'; + +export default () => initForm(false); diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js new file mode 100644 index 00000000000..c9a18353f2e --- /dev/null +++ b/app/assets/javascripts/pages/groups/milestones/show/index.js @@ -0,0 +1,3 @@ +import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; + +export default initMilestonesShow; diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js new file mode 100644 index 00000000000..7850b90d3d2 --- /dev/null +++ b/app/assets/javascripts/pages/groups/new/index.js @@ -0,0 +1,9 @@ +import BindInOut from '~/behaviors/bind_in_out'; +import Group from '~/group'; +import groupAvatar from '~/group_avatar'; + +export default () => { + 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 new file mode 100644 index 00000000000..f26c7360fbe --- /dev/null +++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js @@ -0,0 +1,11 @@ +import SecretValues from '~/behaviors/secret_values'; + +export default () => { + const secretVariableTable = document.querySelector('.js-secret-variable-table'); + if (secretVariableTable) { + const secretVariableTableValues = new SecretValues({ + container: secretVariableTable, + }); + secretVariableTableValues.init(); + } +}; diff --git a/app/assets/javascripts/pages/groups/show/index.js b/app/assets/javascripts/pages/groups/show/index.js new file mode 100644 index 00000000000..5c763986da3 --- /dev/null +++ b/app/assets/javascripts/pages/groups/show/index.js @@ -0,0 +1,22 @@ +/* eslint-disable no-new */ + +import NewGroupChild from '~/groups/new_group_child'; +import notificationsDropdown from '~/notifications_dropdown'; +import NotificationsForm from '~/notifications_form'; +import ProjectsList from '~/projects_list'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import initGroupsList from '../../../groups'; + +document.addEventListener('DOMContentLoaded', () => { + const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup'); + new ShortcutsNavigation(); + new NotificationsForm(); + notificationsDropdown(); + new ProjectsList(); + + if (newGroupChildWrapper) { + new NewGroupChild(newGroupChildWrapper); + } + + initGroupsList(); +}); diff --git a/app/assets/javascripts/pages/help/index.js b/app/assets/javascripts/pages/help/index.js new file mode 100644 index 00000000000..4cf8afc4b7e --- /dev/null +++ b/app/assets/javascripts/pages/help/index.js @@ -0,0 +1,3 @@ +import VersionCheckImage from '../../version_check_image'; + +export default () => VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); 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 new file mode 100644 index 00000000000..5defea104d4 --- /dev/null +++ b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js @@ -0,0 +1,3 @@ +import UsersSelect from '../../../../users_select'; + +export default () => new UsersSelect(); diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue new file mode 100644 index 00000000000..c43e0a0490f --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue @@ -0,0 +1,110 @@ +<script> + import axios from '~/lib/utils/axios_utils'; + + import Flash from '~/flash'; + import modal from '~/vue_shared/components/modal.vue'; + import { n__, s__, sprintf } from '~/locale'; + import { redirectTo } from '~/lib/utils/url_utility'; + import eventHub from '../event_hub'; + + export default { + components: { + modal, + }, + props: { + issueCount: { + type: Number, + required: true, + }, + mergeRequestCount: { + type: Number, + required: true, + }, + milestoneId: { + type: Number, + required: true, + }, + milestoneTitle: { + type: String, + required: true, + }, + milestoneUrl: { + type: String, + required: true, + }, + }, + computed: { + text() { + const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', { milestoneTitle: this.milestoneTitle }); + + if (this.issueCount === 0 && this.mergeRequestCount === 0) { + return sprintf( + s__(`Milestones| +You’re about to permanently delete the milestone %{milestoneTitle} from this project. +%{milestoneTitle} is not currently used in any issues or merge requests.`), + { + milestoneTitle, + }, + false, + ); + } + + return sprintf( + s__(`Milestones| +You’re about to permanently delete the milestone %{milestoneTitle} from this project and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. +Once deleted, it cannot be undone or recovered.`), + { + milestoneTitle, + issuesWithCount: n__('%d issue', '%d issues', this.issueCount), + mergeRequestsWithCount: n__('%d merge request', '%d merge requests', this.mergeRequestCount), + }, + false, + ); + }, + title() { + return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), { milestoneTitle: this.milestoneTitle }); + }, + }, + methods: { + onSubmit() { + eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl); + + return axios.delete(this.milestoneUrl) + .then((response) => { + eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: true }); + + // follow the rediect to milestones overview page + redirectTo(response.request.responseURL); + }) + .catch((error) => { + eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: false }); + + if (error.response && error.response.status === 404) { + Flash(sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), { milestoneTitle: this.milestoneTitle })); + } else { + Flash(sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), { milestoneTitle: this.milestoneTitle })); + } + throw error; + }); + }, + }, + }; +</script> + +<template> + <modal + id="delete-milestone-modal" + :title="title" + :text="text" + kind="danger" + :primary-button-label="s__('Milestones|Delete milestone')" + @submit="onSubmit"> + + <template + slot="body" + slot-scope="props"> + <p v-html="props.text"></p> + </template> + + </modal> +</template> diff --git a/app/assets/javascripts/pages/milestones/shared/event_hub.js b/app/assets/javascripts/pages/milestones/shared/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/pages/milestones/shared/index.js b/app/assets/javascripts/pages/milestones/shared/index.js new file mode 100644 index 00000000000..327e2cf569c --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/index.js @@ -0,0 +1,88 @@ +import Vue from 'vue'; + +import Translate from '~/vue_shared/translate'; + +import deleteMilestoneModal from './components/delete_milestone_modal.vue'; +import eventHub from './event_hub'; + +export default () => { + Vue.use(Translate); + + const onRequestFinished = ({ milestoneUrl, successful }) => { + const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`); + + if (!successful) { + button.removeAttribute('disabled'); + } + + button.querySelector('.js-loading-icon').classList.add('hidden'); + }; + + const onRequestStarted = (milestoneUrl) => { + const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`); + button.setAttribute('disabled', ''); + button.querySelector('.js-loading-icon').classList.remove('hidden'); + eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished); + }; + + const onDeleteButtonClick = (event) => { + const button = event.currentTarget; + const modalProps = { + milestoneId: parseInt(button.dataset.milestoneId, 10), + milestoneTitle: button.dataset.milestoneTitle, + milestoneUrl: button.dataset.milestoneUrl, + issueCount: parseInt(button.dataset.milestoneIssueCount, 10), + mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10), + }; + eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted); + eventHub.$emit('deleteMilestoneModal.props', modalProps); + }; + + const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button'); + for (let i = 0; i < deleteMilestoneButtons.length; i += 1) { + const button = deleteMilestoneButtons[i]; + button.addEventListener('click', onDeleteButtonClick); + } + + eventHub.$once('deleteMilestoneModal.mounted', () => { + for (let i = 0; i < deleteMilestoneButtons.length; i += 1) { + const button = deleteMilestoneButtons[i]; + button.removeAttribute('disabled'); + } + }); + + return new Vue({ + el: '#delete-milestone-modal', + components: { + deleteMilestoneModal, + }, + data() { + return { + modalProps: { + milestoneId: -1, + milestoneTitle: '', + milestoneUrl: '', + issueCount: -1, + mergeRequestCount: -1, + }, + }; + }, + mounted() { + eventHub.$on('deleteMilestoneModal.props', this.setModalProps); + eventHub.$emit('deleteMilestoneModal.mounted'); + }, + beforeDestroy() { + eventHub.$off('deleteMilestoneModal.props', this.setModalProps); + }, + methods: { + setModalProps(modalProps) { + this.modalProps = modalProps; + }, + }, + render(createElement) { + return createElement(deleteMilestoneModal, { + props: this.modalProps, + }); + }, + }); +}; diff --git a/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js b/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js new file mode 100644 index 00000000000..7aa5be0d5b9 --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js @@ -0,0 +1,9 @@ +/* eslint-disable no-new */ + +import Milestone from '~/milestone'; +import Sidebar from '~/right_sidebar'; + +export default () => { + new Milestone(); + new Sidebar(); +}; diff --git a/app/assets/javascripts/pages/omniauth_callbacks/index.js b/app/assets/javascripts/pages/omniauth_callbacks/index.js new file mode 100644 index 00000000000..54f4e56359a --- /dev/null +++ b/app/assets/javascripts/pages/omniauth_callbacks/index.js @@ -0,0 +1,5 @@ +import initU2F from '../../shared/sessions/u2f'; + +export default () => { + initU2F(); +}; diff --git a/app/assets/javascripts/pages/profiles/index/index.js b/app/assets/javascripts/pages/profiles/index/index.js new file mode 100644 index 00000000000..90eed38777a --- /dev/null +++ b/app/assets/javascripts/pages/profiles/index/index.js @@ -0,0 +1,7 @@ +import NotificationsForm from '../../../notifications_form'; +import notificationsDropdown from '../../../notifications_dropdown'; + +export default () => { + 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 new file mode 100644 index 00000000000..030328a1363 --- /dev/null +++ b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js @@ -0,0 +1,3 @@ +import DueDateSelectors from '../../../due_date_select'; + +export default () => new DueDateSelectors(); diff --git a/app/assets/javascripts/pages/projects/activity/index.js b/app/assets/javascripts/pages/projects/activity/index.js new file mode 100644 index 00000000000..7af95127fd5 --- /dev/null +++ b/app/assets/javascripts/pages/projects/activity/index.js @@ -0,0 +1,7 @@ +import Activities from '~/activities'; +import ShortcutsNavigation from '~/shortcuts_navigation'; + +export default function () { + 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 new file mode 100644 index 00000000000..02456071086 --- /dev/null +++ b/app/assets/javascripts/pages/projects/artifacts/browse/index.js @@ -0,0 +1,7 @@ +import BuildArtifacts from '~/build_artifacts'; +import ShortcutsNavigation from '~/shortcuts_navigation'; + +export default function () { + 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 new file mode 100644 index 00000000000..4cd67ac76e3 --- /dev/null +++ b/app/assets/javascripts/pages/projects/artifacts/file/index.js @@ -0,0 +1,7 @@ +import BlobViewer from '~/blob/viewer/index'; +import ShortcutsNavigation from '~/shortcuts_navigation'; + +export default function () { + 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 new file mode 100644 index 00000000000..480357a309c --- /dev/null +++ b/app/assets/javascripts/pages/projects/blame/show/index.js @@ -0,0 +1,3 @@ +import initBlob from '~/pages/projects/init_blob'; + +export default initBlob; diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js new file mode 100644 index 00000000000..a3eeb1cefb6 --- /dev/null +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -0,0 +1,7 @@ +import BlobViewer from '~/blob/viewer/index'; +import initBlob from '~/pages/projects/init_blob'; + +export default () => { + 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 new file mode 100644 index 00000000000..3aeeedbb45d --- /dev/null +++ b/app/assets/javascripts/pages/projects/boards/index.js @@ -0,0 +1,7 @@ +import UsersSelect from '~/users_select'; +import ShortcutsNavigation from '~/shortcuts_navigation'; + +document.addEventListener('DOMContentLoaded', () => { + new UsersSelect(); // eslint-disable-line no-new + new ShortcutsNavigation(); // eslint-disable-line no-new +}); diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js new file mode 100644 index 00000000000..cee0f19bf2a --- /dev/null +++ b/app/assets/javascripts/pages/projects/branches/index/index.js @@ -0,0 +1,7 @@ +import AjaxLoadingSpinner from '~/ajax_loading_spinner'; +import DeleteModal from '~/branches/branches_delete_modal'; + +export default () => { + 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 new file mode 100644 index 00000000000..ae5e033e97e --- /dev/null +++ b/app/assets/javascripts/pages/projects/branches/new/index.js @@ -0,0 +1,3 @@ +import NewBranchForm from '~/new_branch_form'; + +export default () => new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)); diff --git a/app/assets/javascripts/pages/projects/clusters/index/index.js b/app/assets/javascripts/pages/projects/clusters/index/index.js new file mode 100644 index 00000000000..d531ab81dc7 --- /dev/null +++ b/app/assets/javascripts/pages/projects/clusters/index/index.js @@ -0,0 +1,5 @@ +import ClustersIndex from '~/clusters/clusters_index'; + +export default () => { + 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 new file mode 100644 index 00000000000..0458c02a66f --- /dev/null +++ b/app/assets/javascripts/pages/projects/clusters/show/index.js @@ -0,0 +1,5 @@ +import ClustersBundle from '~/clusters/clusters_bundle'; + +export default () => { + 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 new file mode 100644 index 00000000000..523ad567021 --- /dev/null +++ b/app/assets/javascripts/pages/projects/commit/pipelines/index.js @@ -0,0 +1,8 @@ +import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; + +export default () => { + new MiniPipelineGraph({ + container: '.js-commit-pipeline-graph', + }).bindEvents(); + $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); +}; diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js new file mode 100644 index 00000000000..5ac38e6f278 --- /dev/null +++ b/app/assets/javascripts/pages/projects/commit/show/index.js @@ -0,0 +1,22 @@ +/* eslint-disable no-new */ +import Diff from '~/diff'; +import ZenMode from '~/zen_mode'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; +import initNotes from '~/init_notes'; +import initChangesDropdown from '~/init_changes_dropdown'; +import { fetchCommitMergeRequests } from '~/commit_merge_requests'; + +export default () => { + new Diff(); + new ZenMode(); + new ShortcutsNavigation(); + new MiniPipelineGraph({ + container: '.js-commit-pipeline-graph', + }).bindEvents(); + initNotes(); + const stickyBarPaddingTop = 16; + initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop); + $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); + fetchCommitMergeRequests(); +}; diff --git a/app/assets/javascripts/pages/projects/commits/show/index.js b/app/assets/javascripts/pages/projects/commits/show/index.js new file mode 100644 index 00000000000..90b5882a24f --- /dev/null +++ b/app/assets/javascripts/pages/projects/commits/show/index.js @@ -0,0 +1,9 @@ +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); + 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 new file mode 100644 index 00000000000..890062eeee6 --- /dev/null +++ b/app/assets/javascripts/pages/projects/compare/index.js @@ -0,0 +1,5 @@ +import initCompareAutocomplete from '~/compare_autocomplete'; + +export default () => { + initCompareAutocomplete(); +}; diff --git a/app/assets/javascripts/pages/projects/compare/show/index.js b/app/assets/javascripts/pages/projects/compare/show/index.js new file mode 100644 index 00000000000..6b8d4503568 --- /dev/null +++ b/app/assets/javascripts/pages/projects/compare/show/index.js @@ -0,0 +1,8 @@ +import Diff from '~/diff'; +import initChangesDropdown from '~/init_changes_dropdown'; + +export default () => { + 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/constants.js b/app/assets/javascripts/pages/projects/constants.js new file mode 100644 index 00000000000..9efbf7cd36e --- /dev/null +++ b/app/assets/javascripts/pages/projects/constants.js @@ -0,0 +1,6 @@ +/* eslint-disable import/prefer-default-export */ + +export const ISSUABLE_INDEX = { + MERGE_REQUEST: 'merge_request_', + ISSUE: 'issue_', +}; diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js new file mode 100644 index 00000000000..9edf36d66b1 --- /dev/null +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -0,0 +1,14 @@ +import initSettingsPanels from '~/settings_panels'; +import setupProjectEdit from '~/project_edit'; +import ProjectNew from '../shared/project_new'; +import projectAvatar from '../shared/project_avatar'; +import initProjectPermissionsSettings from '../shared/permissions'; + +export default () => { + new ProjectNew(); // eslint-disable-line no-new + setupProjectEdit(); + // Initialize expandable settings panels + initSettingsPanels(); + projectAvatar(); + initProjectPermissionsSettings(); +}; diff --git a/app/assets/javascripts/pages/projects/environments/metrics/index.js b/app/assets/javascripts/pages/projects/environments/metrics/index.js new file mode 100644 index 00000000000..f4760cb2720 --- /dev/null +++ b/app/assets/javascripts/pages/projects/environments/metrics/index.js @@ -0,0 +1,3 @@ +import monitoringBundle from '~/monitoring/monitoring_bundle'; + +export default 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 new file mode 100644 index 00000000000..42bde0ff779 --- /dev/null +++ b/app/assets/javascripts/pages/projects/find_file/show/index.js @@ -0,0 +1,12 @@ +import ProjectFindFile from '~/project_find_file'; +import ShortcutsFindFile from '~/shortcuts_find_file'; + +export default () => { + const findElement = document.querySelector('.js-file-finder'); + const projectFindFile = new ProjectFindFile($('.file-finder-holder'), { + url: findElement.dataset.fileFindUrl, + treeUrl: findElement.dataset.findTreeUrl, + 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 new file mode 100644 index 00000000000..7825eb01949 --- /dev/null +++ b/app/assets/javascripts/pages/projects/forks/new/index.js @@ -0,0 +1,5 @@ +import ProjectFork from '~/project_fork'; + +export default () => { + new ProjectFork(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/imports/show/index.js b/app/assets/javascripts/pages/projects/imports/show/index.js new file mode 100644 index 00000000000..378f7b3f38b --- /dev/null +++ b/app/assets/javascripts/pages/projects/imports/show/index.js @@ -0,0 +1,5 @@ +import ProjectImport from '~/project_import'; + +export default () => { + 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 new file mode 100644 index 00000000000..9b1d52692a3 --- /dev/null +++ b/app/assets/javascripts/pages/projects/index.js @@ -0,0 +1,7 @@ +import Project from './project'; +import ShortcutsNavigation from '../../shortcuts_navigation'; + +export default () => { + 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 new file mode 100644 index 00000000000..26f0ad46114 --- /dev/null +++ b/app/assets/javascripts/pages/projects/init_blob.js @@ -0,0 +1,33 @@ +import LineHighlighter from '~/line_highlighter'; +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'; + +export default () => { + new LineHighlighter(); // eslint-disable-line no-new + + new BlobLinePermalinkUpdater( // eslint-disable-line no-new + document.querySelector('#blob-content-holder'), + '.diff-line-num[data-line-number]', + document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'), + ); + + const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); + const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); + + new ShortcutsNavigation(); // eslint-disable-line no-new + + new ShortcutsBlob({ // eslint-disable-line no-new + skipResetBindings: true, + fileBlobPermalinkUrl, + }); + + new BlobForkSuggestion({ // eslint-disable-line no-new + openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'), + forkButtons: document.querySelectorAll('.js-fork-suggestion-button'), + cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'), + suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'), + actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'), + }).init(); +}; diff --git a/app/assets/javascripts/pages/projects/init_form.js b/app/assets/javascripts/pages/projects/init_form.js new file mode 100644 index 00000000000..0b6c5c1d30b --- /dev/null +++ b/app/assets/javascripts/pages/projects/init_form.js @@ -0,0 +1,7 @@ +import ZenMode from '~/zen_mode'; +import GLForm from '~/gl_form'; + +export default function ($formEl) { + new ZenMode(); // eslint-disable-line no-new + new GLForm($formEl, true); // eslint-disable-line no-new +} diff --git a/app/assets/javascripts/pages/projects/issues/edit/index.js b/app/assets/javascripts/pages/projects/issues/edit/index.js new file mode 100644 index 00000000000..7f27f379d8c --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/edit/index.js @@ -0,0 +1,5 @@ +import initForm from '../form'; + +export default () => { + initForm(); +}; diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js new file mode 100644 index 00000000000..5c7daf84738 --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/form.js @@ -0,0 +1,16 @@ +/* eslint-disable no-new */ +import GLForm from '~/gl_form'; +import IssuableForm from '~/issuable_form'; +import LabelsSelect from '~/labels_select'; +import MilestoneSelect from '~/milestone_select'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import IssuableTemplateSelectors from '~/templates/issuable_template_selectors'; + +export default () => { + new ShortcutsNavigation(); + new GLForm($('.issue-form'), true); + new IssuableForm($('.issue-form')); + new LabelsSelect(); + new MilestoneSelect(); + new IssuableTemplateSelectors(); +}; diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js new file mode 100644 index 00000000000..39c043edc38 --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/index/index.js @@ -0,0 +1,16 @@ +/* eslint-disable no-new */ + +import IssuableIndex from '~/issuable_index'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import UsersSelect from '~/users_select'; +import initFilteredSearch from '~/pages/search/init_filtered_search'; +import { FILTERED_SEARCH } from '~/pages/constants'; +import { ISSUABLE_INDEX } from '~/pages/projects/constants'; + +document.addEventListener('DOMContentLoaded', () => { + initFilteredSearch(FILTERED_SEARCH.ISSUES); + new IssuableIndex(ISSUABLE_INDEX.ISSUE); + + new ShortcutsNavigation(); + new UsersSelect(); +}); diff --git a/app/assets/javascripts/pages/projects/issues/new/index.js b/app/assets/javascripts/pages/projects/issues/new/index.js new file mode 100644 index 00000000000..7f27f379d8c --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/new/index.js @@ -0,0 +1,5 @@ +import initForm from '../form'; + +export default () => { + initForm(); +}; diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js new file mode 100644 index 00000000000..da312c1f1b7 --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/show/index.js @@ -0,0 +1,13 @@ +/* eslint-disable no-new */ + +import initIssuableSidebar from '~/init_issuable_sidebar'; +import Issue from '~/issue'; +import ShortcutsIssuable from '~/shortcuts_issuable'; +import ZenMode from '~/zen_mode'; + +document.addEventListener('DOMContentLoaded', () => { + new Issue(); + new ShortcutsIssuable(); + new ZenMode(); + initIssuableSidebar(); +}); diff --git a/app/assets/javascripts/pages/projects/labels/edit/index.js b/app/assets/javascripts/pages/projects/labels/edit/index.js new file mode 100644 index 00000000000..72c5e4744ac --- /dev/null +++ b/app/assets/javascripts/pages/projects/labels/edit/index.js @@ -0,0 +1,3 @@ +import Labels from '~/labels'; + +export default () => new Labels(); diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js new file mode 100644 index 00000000000..018345fa112 --- /dev/null +++ b/app/assets/javascripts/pages/projects/labels/index/index.js @@ -0,0 +1,3 @@ +import initLabels from '~/init_labels'; + +export default initLabels; diff --git a/app/assets/javascripts/pages/projects/labels/new/index.js b/app/assets/javascripts/pages/projects/labels/new/index.js new file mode 100644 index 00000000000..72c5e4744ac --- /dev/null +++ b/app/assets/javascripts/pages/projects/labels/new/index.js @@ -0,0 +1,3 @@ +import Labels from '~/labels'; + +export default () => new Labels(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js new file mode 100644 index 00000000000..734d01ae6f2 --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js @@ -0,0 +1,3 @@ +import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request'; + +export default 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 new file mode 100644 index 00000000000..ccd0b54c5ed --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js @@ -0,0 +1,18 @@ +import Compare from '~/compare'; +import MergeRequest from '~/merge_request'; + +export default () => { + const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); + if (mrNewCompareNode) { + new Compare({ // eslint-disable-line no-new + targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl, + sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl, + targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl, + }); + } else { + const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit'); + new MergeRequest({ // eslint-disable-line no-new + action: mrNewSubmitNode.dataset.mrSubmitAction, + }); + } +}; diff --git a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js new file mode 100644 index 00000000000..734d01ae6f2 --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js @@ -0,0 +1,3 @@ +import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request'; + +export default 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 new file mode 100644 index 00000000000..adadbf28e49 --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js @@ -0,0 +1,13 @@ +import IssuableIndex from '~/issuable_index'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import UsersSelect from '~/users_select'; +import initFilteredSearch from '~/pages/search/init_filtered_search'; +import { FILTERED_SEARCH } from '~/pages/constants'; +import { ISSUABLE_INDEX } from '~/pages/projects/constants'; + +document.addEventListener('DOMContentLoaded', () => { + initFilteredSearch(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.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js new file mode 100644 index 00000000000..8bfac606aab --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js @@ -0,0 +1,19 @@ +/* eslint-disable no-new */ + +import Diff from '~/diff'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import GLForm from '~/gl_form'; +import IssuableForm from '~/issuable_form'; +import LabelsSelect from '~/labels_select'; +import MilestoneSelect from '~/milestone_select'; +import IssuableTemplateSelectors from '~/templates/issuable_template_selectors'; + +export default () => { + new Diff(); + new ShortcutsNavigation(); + new GLForm($('.merge-request-form'), true); + new IssuableForm($('.merge-request-form')); + new LabelsSelect(); + new MilestoneSelect(); + new IssuableTemplateSelectors(); +}; diff --git a/app/assets/javascripts/pages/projects/milestones/edit/index.js b/app/assets/javascripts/pages/projects/milestones/edit/index.js new file mode 100644 index 00000000000..10e3979a36e --- /dev/null +++ b/app/assets/javascripts/pages/projects/milestones/edit/index.js @@ -0,0 +1,3 @@ +import initForm from '../../../../shared/milestones/form'; + +export default () => initForm(); diff --git a/app/assets/javascripts/pages/projects/milestones/index/index.js b/app/assets/javascripts/pages/projects/milestones/index/index.js new file mode 100644 index 00000000000..8fb4d83d8a3 --- /dev/null +++ b/app/assets/javascripts/pages/projects/milestones/index/index.js @@ -0,0 +1,3 @@ +import milestones from '~/pages/milestones/shared'; + +export default milestones; diff --git a/app/assets/javascripts/pages/projects/milestones/new/index.js b/app/assets/javascripts/pages/projects/milestones/new/index.js new file mode 100644 index 00000000000..10e3979a36e --- /dev/null +++ b/app/assets/javascripts/pages/projects/milestones/new/index.js @@ -0,0 +1,3 @@ +import initForm from '../../../../shared/milestones/form'; + +export default () => initForm(); diff --git a/app/assets/javascripts/pages/projects/milestones/show/index.js b/app/assets/javascripts/pages/projects/milestones/show/index.js new file mode 100644 index 00000000000..35b5c9c2ced --- /dev/null +++ b/app/assets/javascripts/pages/projects/milestones/show/index.js @@ -0,0 +1,7 @@ +import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; +import milestones from '~/pages/milestones/shared'; + +export default () => { + initMilestonesShow(); + milestones(); +}; diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js new file mode 100644 index 00000000000..71c49deb9d0 --- /dev/null +++ b/app/assets/javascripts/pages/projects/new/index.js @@ -0,0 +1,9 @@ +import ProjectNew from '../shared/project_new'; +import initProjectVisibilitySelector from '../../../project_visibility'; +import initProjectNew from '../../../projects/project_new'; + +export default () => { + new ProjectNew(); // eslint-disable-line no-new + initProjectVisibilitySelector(); + initProjectNew.bindEvents(); +}; diff --git a/app/assets/javascripts/pages/projects/pipelines/builds/index.js b/app/assets/javascripts/pages/projects/pipelines/builds/index.js new file mode 100644 index 00000000000..060a78b427e --- /dev/null +++ b/app/assets/javascripts/pages/projects/pipelines/builds/index.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 new file mode 100644 index 00000000000..c54cc62bf05 --- /dev/null +++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js @@ -0,0 +1,5 @@ +import NewBranchForm from '../../../../new_branch_form'; + +export default () => { + new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/pages/projects/project.js index d4f26b81f30..863dac0d20e 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -1,8 +1,11 @@ /* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ import Cookies from 'js-cookie'; -import { visitUrl } from './lib/utils/url_utility'; -import projectSelect from './project_select'; +import { __ } from '~/locale'; +import { visitUrl } from '~/lib/utils/url_utility'; +import axios from '~/lib/utils/axios_utils'; +import flash from '~/flash'; +import projectSelect from '../../project_select'; export default class Project { constructor() { @@ -67,17 +70,15 @@ export default class Project { $dropdown = $(this); selected = $dropdown.data('selected'); return $dropdown.glDropdown({ - data: function(term, callback) { - return $.ajax({ - url: $dropdown.data('refs-url'), - data: { + data(term, callback) { + axios.get($dropdown.data('refs-url'), { + params: { ref: $dropdown.data('ref'), search: term, }, - dataType: 'json', - }).done(function(refs) { - return callback(refs); - }); + }) + .then(({ data }) => callback(data)) + .catch(() => flash(__('An error occurred while getting projects'))); }, selectable: true, filterable: true, diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js new file mode 100644 index 00000000000..f4643e7dba0 --- /dev/null +++ b/app/assets/javascripts/pages/projects/project_members/index.js @@ -0,0 +1,12 @@ +import memberExpirationDate from '../../../member_expiration_date'; +import UsersSelect from '../../../users_select'; +import groupsSelect from '../../../groups_select'; +import Members from '../../../members'; + +export default () => { + 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/releases/edit/index.js b/app/assets/javascripts/pages/projects/releases/edit/index.js new file mode 100644 index 00000000000..3d997cdfff0 --- /dev/null +++ b/app/assets/javascripts/pages/projects/releases/edit/index.js @@ -0,0 +1,3 @@ +import initForm from '~/pages/projects/init_form'; + +export default initForm($('.release-form')); 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 new file mode 100644 index 00000000000..18dc1dc03a5 --- /dev/null +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -0,0 +1,22 @@ +import initSettingsPanels from '~/settings_panels'; +import SecretValues from '~/behaviors/secret_values'; + +export default function () { + // Initialize expandable settings panels + initSettingsPanels(); + const runnerToken = document.querySelector('.js-secret-runner-token'); + if (runnerToken) { + const runnerTokenSecretValue = new SecretValues({ + container: runnerToken, + }); + runnerTokenSecretValue.init(); + } + + const secretVariableTable = document.querySelector('.js-secret-variable-table'); + if (secretVariableTable) { + const secretVariableTableValues = new SecretValues({ + container: secretVariableTable, + }); + secretVariableTableValues.init(); + } +} diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js new file mode 100644 index 00000000000..83b5467fbc0 --- /dev/null +++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js @@ -0,0 +1,3 @@ +import initSettingsPanels from '~/settings_panels'; + +export default initSettingsPanels; diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue new file mode 100644 index 00000000000..9b13b2a524f --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue @@ -0,0 +1,111 @@ +<script> + import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; + + export default { + components: { + projectFeatureToggle, + }, + + model: { + prop: 'value', + event: 'change', + }, + + props: { + name: { + type: String, + required: false, + default: '', + }, + options: { + type: Array, + required: false, + default: () => [], + }, + value: { + type: Number, + required: false, + default: 0, + }, + disabledInput: { + type: Boolean, + required: false, + default: false, + }, + }, + + computed: { + featureEnabled() { + return this.value !== 0; + }, + + displayOptions() { + if (this.featureEnabled) { + return this.options; + } + return [ + [0, 'Enable feature to choose access level'], + ]; + }, + + displaySelectInput() { + return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2; + }, + }, + + methods: { + toggleFeature(featureEnabled) { + if (featureEnabled === false || this.options.length < 1) { + this.$emit('change', 0); + } else { + const [firstOptionValue] = this.options[this.options.length - 1]; + this.$emit('change', firstOptionValue); + } + }, + + selectOption(e) { + this.$emit('change', Number(e.target.value)); + }, + }, + }; +</script> + +<template> + <div + class="project-feature-controls" + :data-for="name" + > + <input + v-if="name" + type="hidden" + :name="name" + :value="value" + /> + <project-feature-toggle + :value="featureEnabled" + @change="toggleFeature" + :disabled-input="disabledInput" + /> + <div class="select-wrapper"> + <select + class="form-control project-repo-select select-control" + @change="selectOption" + :disabled="displaySelectInput" + > + <option + v-for="[optionValue, optionName] in displayOptions" + :key="optionValue" + :value="optionValue" + :selected="optionValue === value" + > + {{ optionName }} + </option> + </select> + <i + aria-hidden="true" + class="fa fa-chevron-down" + > + </i> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue new file mode 100644 index 00000000000..25a88f846eb --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue @@ -0,0 +1,51 @@ +<script> + export default { + props: { + label: { + type: String, + required: false, + default: null, + }, + helpPath: { + type: String, + required: false, + default: null, + }, + helpText: { + type: String, + required: false, + default: null, + }, + }, + }; +</script> + +<template> + <div class="project-feature-row"> + <label + v-if="label" + class="label-light" + > + {{ label }} + <a + v-if="helpPath" + :href="helpPath" + target="_blank" + > + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-question-circle" + > + </i> + </a> + </label> + <span + v-if="helpText" + class="help-block" + > + {{ helpText }} + </span> + <slot></slot> + </div> +</template> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue new file mode 100644 index 00000000000..755a34b7348 --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -0,0 +1,328 @@ +<script> + import projectFeatureSetting from './project_feature_setting.vue'; + import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; + import projectSettingRow from './project_setting_row.vue'; + import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; + import { toggleHiddenClassBySelector } from '../external'; + + export default { + components: { + projectFeatureSetting, + projectFeatureToggle, + projectSettingRow, + }, + + props: { + currentSettings: { + type: Object, + required: true, + }, + canChangeVisibilityLevel: { + type: Boolean, + required: false, + default: false, + }, + allowedVisibilityOptions: { + type: Array, + required: false, + default: () => [0, 10, 20], + }, + lfsAvailable: { + type: Boolean, + required: false, + default: false, + }, + registryAvailable: { + type: Boolean, + required: false, + default: false, + }, + visibilityHelpPath: { + type: String, + required: false, + default: '', + }, + lfsHelpPath: { + type: String, + required: false, + default: '', + }, + registryHelpPath: { + type: String, + required: false, + default: '', + }, + }, + + data() { + const defaults = { + visibilityOptions, + visibilityLevel: visibilityOptions.PUBLIC, + issuesAccessLevel: 20, + repositoryAccessLevel: 20, + mergeRequestsAccessLevel: 20, + buildsAccessLevel: 20, + wikiAccessLevel: 20, + snippetsAccessLevel: 20, + containerRegistryEnabled: true, + lfsEnabled: true, + requestAccessEnabled: true, + highlightChangesClass: false, + }; + + return { ...defaults, ...this.currentSettings }; + }, + + computed: { + featureAccessLevelOptions() { + const options = [ + [10, 'Only Project Members'], + ]; + if (this.visibilityLevel !== visibilityOptions.PRIVATE) { + options.push([20, 'Everyone With Access']); + } + return options; + }, + + repoFeatureAccessLevelOptions() { + return this.featureAccessLevelOptions.filter( + ([value]) => value <= this.repositoryAccessLevel, + ); + }, + + repositoryEnabled() { + return this.repositoryAccessLevel > 0; + }, + + visibilityLevelDescription() { + return visibilityLevelDescriptions[this.visibilityLevel]; + }, + }, + + watch: { + visibilityLevel(value, oldValue) { + if (value === visibilityOptions.PRIVATE) { + // when private, features are restricted to "only team members" + this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel); + this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel); + this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel); + this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); + this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); + this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); + this.highlightChanges(); + } else if (oldValue === visibilityOptions.PRIVATE) { + // if changing away from private, make enabled features more permissive + if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20; + if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20; + if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20; + if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; + if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; + if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; + this.highlightChanges(); + } + }, + + repositoryAccessLevel(value, oldValue) { + if (value < oldValue) { + // sub-features cannot have more premissive access level + this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value); + this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value); + + if (value === 0) { + this.containerRegistryEnabled = false; + this.lfsEnabled = false; + } + } else if (oldValue === 0) { + this.mergeRequestsAccessLevel = value; + this.buildsAccessLevel = value; + this.containerRegistryEnabled = true; + this.lfsEnabled = true; + } + }, + + issuesAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.issues-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false); + }, + + mergeRequestsAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false); + }, + + buildsAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.builds-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false); + }, + }, + + methods: { + highlightChanges() { + this.highlightChangesClass = true; + this.$nextTick(() => { + this.highlightChangesClass = false; + }); + }, + + visibilityAllowed(option) { + return this.allowedVisibilityOptions.includes(option); + }, + }, + }; +</script> + +<template> + <div> + <div class="project-visibility-setting"> + <project-setting-row + label="Project visibility" + :help-path="visibilityHelpPath" + > + <div class="project-feature-controls"> + <div class="select-wrapper"> + <select + name="project[visibility_level]" + v-model="visibilityLevel" + class="form-control select-control" + :disabled="!canChangeVisibilityLevel" + > + <option + :value="visibilityOptions.PRIVATE" + :disabled="!visibilityAllowed(visibilityOptions.PRIVATE)" + > + Private + </option> + <option + :value="visibilityOptions.INTERNAL" + :disabled="!visibilityAllowed(visibilityOptions.INTERNAL)" + > + Internal + </option> + <option + :value="visibilityOptions.PUBLIC" + :disabled="!visibilityAllowed(visibilityOptions.PUBLIC)" + > + Public + </option> + </select> + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-chevron-down" + > + </i> + </div> + </div> + <span class="help-block">{{ visibilityLevelDescription }}</span> + <label + v-if="visibilityLevel !== visibilityOptions.PRIVATE" + class="request-access" + > + <input + type="hidden" + name="project[request_access_enabled]" + :value="requestAccessEnabled" + /> + <input + type="checkbox" + v-model="requestAccessEnabled" + /> + Allow users to request access + </label> + </project-setting-row> + </div> + <div + class="project-feature-settings" + :class="{ 'highlight-changes': highlightChangesClass }" + > + <project-setting-row + label="Issues" + help-text="Lightweight issue tracking system for this project" + > + <project-feature-setting + name="project[project_feature_attributes][issues_access_level]" + :options="featureAccessLevelOptions" + v-model="issuesAccessLevel" + /> + </project-setting-row> + <project-setting-row + label="Repository" + help-text="View and edit files in this project" + > + <project-feature-setting + name="project[project_feature_attributes][repository_access_level]" + :options="featureAccessLevelOptions" + v-model="repositoryAccessLevel" + /> + </project-setting-row> + <div class="project-feature-setting-group"> + <project-setting-row + label="Merge requests" + help-text="Submit changes to be merged upstream" + > + <project-feature-setting + name="project[project_feature_attributes][merge_requests_access_level]" + :options="repoFeatureAccessLevelOptions" + v-model="mergeRequestsAccessLevel" + :disabled-input="!repositoryEnabled" + /> + </project-setting-row> + <project-setting-row + label="Pipelines" + help-text="Build, test, and deploy your changes" + > + <project-feature-setting + name="project[project_feature_attributes][builds_access_level]" + :options="repoFeatureAccessLevelOptions" + v-model="buildsAccessLevel" + :disabled-input="!repositoryEnabled" + /> + </project-setting-row> + <project-setting-row + v-if="registryAvailable" + label="Container registry" + :help-path="registryHelpPath" + help-text="Every project can have its own space to store its Docker images" + > + <project-feature-toggle + name="project[container_registry_enabled]" + v-model="containerRegistryEnabled" + :disabled-input="!repositoryEnabled" + /> + </project-setting-row> + <project-setting-row + v-if="lfsAvailable" + label="Git Large File Storage" + :help-path="lfsHelpPath" + help-text="Manages large files such as audio, video, and graphics files" + > + <project-feature-toggle + name="project[lfs_enabled]" + v-model="lfsEnabled" + :disabled-input="!repositoryEnabled" + /> + </project-setting-row> + </div> + <project-setting-row + label="Wiki" + help-text="Pages for project documentation" + > + <project-feature-setting + name="project[project_feature_attributes][wiki_access_level]" + :options="featureAccessLevelOptions" + v-model="wikiAccessLevel" + /> + </project-setting-row> + <project-setting-row + label="Snippets" + help-text="Share code pastes with others out of Git repository" + > + <project-feature-setting + name="project[project_feature_attributes][snippets_access_level]" + :options="featureAccessLevelOptions" + v-model="snippetsAccessLevel" + /> + </project-setting-row> + </div> + </div> +</template> diff --git a/app/assets/javascripts/projects/permissions/constants.js b/app/assets/javascripts/pages/projects/shared/permissions/constants.js index ce47562f259..ce47562f259 100644 --- a/app/assets/javascripts/projects/permissions/constants.js +++ b/app/assets/javascripts/pages/projects/shared/permissions/constants.js diff --git a/app/assets/javascripts/projects/permissions/external.js b/app/assets/javascripts/pages/projects/shared/permissions/external.js index 460af4a2111..460af4a2111 100644 --- a/app/assets/javascripts/projects/permissions/external.js +++ b/app/assets/javascripts/pages/projects/shared/permissions/external.js diff --git a/app/assets/javascripts/projects/permissions/index.js b/app/assets/javascripts/pages/projects/shared/permissions/index.js index dbde8dda634..dbde8dda634 100644 --- a/app/assets/javascripts/projects/permissions/index.js +++ b/app/assets/javascripts/pages/projects/shared/permissions/index.js diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/pages/projects/shared/project_avatar.js index 56627aa155c..56627aa155c 100644 --- a/app/assets/javascripts/project_avatar.js +++ b/app/assets/javascripts/pages/projects/shared/project_avatar.js diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/pages/projects/shared/project_new.js index ca548d011b6..86faba0b910 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/pages/projects/shared/project_new.js @@ -1,6 +1,6 @@ /* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/ -import VisibilitySelect from './visibility_select'; +import VisibilitySelect from '../../../visibility_select'; function highlightChanges($elm) { $elm.addClass('highlight-changes'); diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js new file mode 100644 index 00000000000..9b87f249f09 --- /dev/null +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -0,0 +1,27 @@ +import ShortcutsNavigation from '~/shortcuts_navigation'; +import NotificationsForm from '~/notifications_form'; +import UserCallout from '~/user_callout'; +import TreeView from '~/tree'; +import BlobViewer from '~/blob/viewer/index'; +import Activities from '~/activities'; +import { ajaxGet } from '~/lib/utils/common_utils'; +import Star from '../../../star'; +import notificationsDropdown from '../../../notifications_dropdown'; + +document.addEventListener('DOMContentLoaded', () => { + new Star(); // eslint-disable-line no-new + notificationsDropdown(); + new ShortcutsNavigation(); // eslint-disable-line no-new + new NotificationsForm(); // eslint-disable-line no-new + new UserCallout({ // eslint-disable-line no-new + setCalloutPerProject: true, + className: 'js-autodevops-banner', + }); + + if ($('#tree-slider').length) new TreeView(); // eslint-disable-line no-new + if ($('.blob-viewer').length) new BlobViewer(); // eslint-disable-line no-new + if ($('.project-show-activity').length) new Activities(); // eslint-disable-line no-new + $('#tree-slider').waitForImages(() => { + ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); + }); +}); diff --git a/app/assets/javascripts/pages/projects/snippets/edit/index.js b/app/assets/javascripts/pages/projects/snippets/edit/index.js new file mode 100644 index 00000000000..9edb16dc73b --- /dev/null +++ b/app/assets/javascripts/pages/projects/snippets/edit/index.js @@ -0,0 +1,3 @@ +import initForm from '~/pages/projects/init_form'; + +export default 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 new file mode 100644 index 00000000000..9edb16dc73b --- /dev/null +++ b/app/assets/javascripts/pages/projects/snippets/new/index.js @@ -0,0 +1,3 @@ +import initForm from '~/pages/projects/init_form'; + +export default 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 new file mode 100644 index 00000000000..a3cf75c385b --- /dev/null +++ b/app/assets/javascripts/pages/projects/snippets/show/index.js @@ -0,0 +1,11 @@ +import initNotes from '~/init_notes'; +import ZenMode from '~/zen_mode'; +import LineHighlighter from '../../../../line_highlighter'; +import BlobViewer from '../../../../blob/viewer'; + +export default function () { + 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 new file mode 100644 index 00000000000..dacc2875c8c --- /dev/null +++ b/app/assets/javascripts/pages/projects/tags/new/index.js @@ -0,0 +1,9 @@ +import RefSelectDropdown from '../../../../ref_select_dropdown'; +import ZenMode from '../../../../zen_mode'; +import GLForm from '../../../../gl_form'; + +export default () => { + 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 new file mode 100644 index 00000000000..c4b3356e478 --- /dev/null +++ b/app/assets/javascripts/pages/projects/tree/show/index.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; +import TreeView from '../../../../tree'; +import ShortcutsNavigation from '../../../../shortcuts_navigation'; +import BlobViewer from '../../../../blob/viewer'; +import NewCommitForm from '../../../../new_commit_form'; +import { ajaxGet } from '../../../../lib/utils/common_utils'; + +export default () => { + new ShortcutsNavigation(); // eslint-disable-line no-new + new TreeView(); // eslint-disable-line no-new + new BlobViewer(); // eslint-disable-line no-new + new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new + $('#tree-slider').waitForImages(() => + ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath)); + + const commitPipelineStatusEl = document.getElementById('commit-pipeline-status'); + const statusLink = document.querySelector('.commit-actions .ci-status-link'); + if (statusLink != null) { + statusLink.remove(); + // eslint-disable-next-line no-new + new Vue({ + el: commitPipelineStatusEl, + components: { + commitPipelineStatus, + }, + render(createElement) { + return createElement('commit-pipeline-status', { + props: { + endpoint: commitPipelineStatusEl.dataset.endpoint, + }, + }); + }, + }); + } +}; + diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js new file mode 100644 index 00000000000..eb14c7a0e78 --- /dev/null +++ b/app/assets/javascripts/pages/projects/wikis/index.js @@ -0,0 +1,11 @@ +import Wikis from './wikis'; +import ShortcutsWiki from '../../../shortcuts_wiki'; +import ZenMode from '../../../zen_mode'; +import GLForm from '../../../gl_form'; + +export default () => { + 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/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js index 7a865587444..34a12ef76a1 100644 --- a/app/assets/javascripts/wikis.js +++ b/app/assets/javascripts/pages/projects/wikis/wikis.js @@ -1,5 +1,5 @@ -import bp from './breakpoints'; -import { slugify } from './lib/utils/text_utility'; +import bp from '../../../breakpoints'; +import { slugify } from '../../../lib/utils/text_utility'; export default class Wikis { constructor() { diff --git a/app/assets/javascripts/pages/search/init_filtered_search.js b/app/assets/javascripts/pages/search/init_filtered_search.js new file mode 100644 index 00000000000..44853636aea --- /dev/null +++ b/app/assets/javascripts/pages/search/init_filtered_search.js @@ -0,0 +1,7 @@ +export default (page) => { + const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); + if (filteredSearchEnabled) { + const filteredSearchManager = new gl.FilteredSearchManager(page); + filteredSearchManager.setup(); + } +}; diff --git a/app/assets/javascripts/pages/search/show/index.js b/app/assets/javascripts/pages/search/show/index.js new file mode 100644 index 00000000000..4264c5c9dbe --- /dev/null +++ b/app/assets/javascripts/pages/search/show/index.js @@ -0,0 +1,3 @@ +import Search from './search'; + +export default () => new Search(); diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/pages/search/show/search.js index 363322af47a..dc621bc87c0 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/pages/search/show/search.js @@ -1,5 +1,5 @@ -import Flash from './flash'; -import Api from './api'; +import Flash from '~/flash'; +import Api from '~/api'; export default class Search { constructor() { @@ -15,6 +15,7 @@ export default class Search { $groupDropdown.glDropdown({ selectable: true, filterable: true, + filterRemote: true, fieldName: 'group_id', search: { fields: ['full_name'], @@ -43,6 +44,7 @@ export default class Search { $projectDropdown.glDropdown({ selectable: true, filterable: true, + filterRemote: true, fieldName: 'project_id', search: { fields: ['name'], diff --git a/app/assets/javascripts/pages/sessions/index.js b/app/assets/javascripts/pages/sessions/index.js new file mode 100644 index 00000000000..54f4e56359a --- /dev/null +++ b/app/assets/javascripts/pages/sessions/index.js @@ -0,0 +1,5 @@ +import initU2F from '../../shared/sessions/u2f'; + +export default () => { + initU2F(); +}; diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js new file mode 100644 index 00000000000..a0aa0499776 --- /dev/null +++ b/app/assets/javascripts/pages/sessions/new/index.js @@ -0,0 +1,11 @@ +import UsernameValidator from './username_validator'; +import SigninTabsMemoizer from './signin_tabs_memoizer'; +import OAuthRememberMe from './oauth_remember_me'; + +document.addEventListener('DOMContentLoaded', () => { + new UsernameValidator(); // eslint-disable-line no-new + new SigninTabsMemoizer(); // eslint-disable-line no-new + new OAuthRememberMe({ // eslint-disable-line no-new + container: $('.omniauth-container'), + }).bindEvents(); +}); diff --git a/app/assets/javascripts/oauth_remember_me.js b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js index ffc2dd6bbca..ffc2dd6bbca 100644 --- a/app/assets/javascripts/oauth_remember_me.js +++ b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js diff --git a/app/assets/javascripts/signin_tabs_memoizer.js b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js index 20255398047..08f0afdcce3 100644 --- a/app/assets/javascripts/signin_tabs_memoizer.js +++ b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js @@ -1,6 +1,4 @@ -/* eslint no-param-reassign: ["error", { "props": false }]*/ -/* eslint no-new: "off" */ -import AccessorUtilities from './lib/utils/accessor'; +import AccessorUtilities from '~/lib/utils/accessor'; /** * Memorize the last selected tab after reloading a page. @@ -11,6 +9,10 @@ export default class SigninTabsMemoizer { this.currentTabKey = currentTabKey; this.tabSelector = tabSelector; this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); + // sets selected tab if given as hash tag + if (window.location.hash) { + this.saveData(window.location.hash); + } this.bootstrap(); } diff --git a/app/assets/javascripts/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js index bb34d5d2008..745543c22da 100644 --- a/app/assets/javascripts/username_validator.js +++ b/app/assets/javascripts/pages/sessions/new/username_validator.js @@ -1,6 +1,9 @@ /* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */ import _ from 'underscore'; +import axios from '~/lib/utils/axios_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; const debounceTimeoutDuration = 1000; const invalidInputClass = 'gl-field-error-outline'; @@ -77,12 +80,9 @@ export default class UsernameValidator { this.state.pending = true; this.state.available = false; this.renderState(); - return $.ajax({ - type: 'GET', - url: `${gon.relative_url_root}/users/${username}/exists`, - dataType: 'json', - success: (res) => this.setAvailabilityState(res.exists) - }); + axios.get(`${gon.relative_url_root}/users/${username}/exists`) + .then(({ data }) => this.setAvailabilityState(data.exists)) + .catch(() => flash(__('An error occurred while validating username'))); } } diff --git a/app/assets/javascripts/pages/snippets/edit/index.js b/app/assets/javascripts/pages/snippets/edit/index.js new file mode 100644 index 00000000000..9c664b5f1ff --- /dev/null +++ b/app/assets/javascripts/pages/snippets/edit/index.js @@ -0,0 +1,3 @@ +import form from '../form'; + +export default form; diff --git a/app/assets/javascripts/pages/snippets/form.js b/app/assets/javascripts/pages/snippets/form.js new file mode 100644 index 00000000000..f996d3cd74e --- /dev/null +++ b/app/assets/javascripts/pages/snippets/form.js @@ -0,0 +1,7 @@ +import GLForm from '~/gl_form'; +import ZenMode from '~/zen_mode'; + +export default () => { + new GLForm($('.snippet-form'), false); // eslint-disable-line no-new + new ZenMode(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/snippets/new/index.js b/app/assets/javascripts/pages/snippets/new/index.js new file mode 100644 index 00000000000..9c664b5f1ff --- /dev/null +++ b/app/assets/javascripts/pages/snippets/new/index.js @@ -0,0 +1,3 @@ +import form from '../form'; + +export default form; diff --git a/app/assets/javascripts/pages/snippets/show/index.js b/app/assets/javascripts/pages/snippets/show/index.js new file mode 100644 index 00000000000..04c9562bfbb --- /dev/null +++ b/app/assets/javascripts/pages/snippets/show/index.js @@ -0,0 +1,12 @@ +/* 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(); + initNotes(); + new ZenMode(); +}; diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue index c8a2f778ee8..00f32d9de78 100644 --- a/app/assets/javascripts/pdf/index.vue +++ b/app/assets/javascripts/pdf/index.vue @@ -5,6 +5,7 @@ import page from './page/index.vue'; export default { + components: { page }, props: { pdf: { type: [String, Uint8Array], @@ -17,8 +18,6 @@ pages: [], }; }, - components: { page }, - watch: { pdf: 'load' }, computed: { document() { return typeof this.pdf === 'string' ? this.pdf : { data: this.pdf }; @@ -27,6 +26,11 @@ return this.pdf && this.pdf.length > 0; }, }, + watch: { pdf: 'load' }, + mounted() { + pdfjsLib.PDFJS.workerSrc = workerSrc; + if (this.hasPDF) this.load(); + }, methods: { load() { this.pages = []; @@ -47,20 +51,20 @@ return Promise.all(pagePromises); }, }, - mounted() { - pdfjsLib.PDFJS.workerSrc = workerSrc; - if (this.hasPDF) this.load(); - }, }; </script> <template> - <div class="pdf-viewer" v-if="hasPDF"> - <page v-for="(page, index) in pages" + <div + class="pdf-viewer" + v-if="hasPDF"> + <page + v-for="(page, index) in pages" :key="index" :v-if="!loading" :page="page" - :number="index + 1" /> + :number="index + 1" + /> </div> </template> diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue index be38f7cc129..fcba819beba 100644 --- a/app/assets/javascripts/pdf/page/index.vue +++ b/app/assets/javascripts/pdf/page/index.vue @@ -45,24 +45,26 @@ <canvas class="pdf-page" ref="canvas" - :data-page="number" /> + :data-page="number" + > + </canvas> </template> <style> -.pdf-page { - margin: 8px auto 0 auto; - border-top: 1px #ddd solid; - border-bottom: 1px #ddd solid; - width: 100%; -} + .pdf-page { + margin: 8px auto 0 auto; + border-top: 1px #ddd solid; + border-bottom: 1px #ddd solid; + width: 100%; + } -.pdf-page:first-child { - margin-top: 0px; - border-top: 0px; -} + .pdf-page:first-child { + margin-top: 0px; + border-top: 0px; + } -.pdf-page:last-child { - margin-bottom: 0px; - border-bottom: 0px; -} + .pdf-page:last-child { + margin-bottom: 0px; + border-bottom: 0px; + } </style> diff --git a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue index b5d85299cf8..2d18fa2044b 100644 --- a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue @@ -32,6 +32,20 @@ return !!(this.customInputEnabled || !this.intervalIsPreset); }, }, + watch: { + cronInterval() { + // updates field validation state when model changes, as + // glFieldError only updates on input. + this.$nextTick(() => { + gl.pipelineScheduleFieldErrors.updateFormValidityState(); + }); + }, + }, + created() { + if (this.intervalIsPreset) { + this.enableCustomInput = false; + } + }, methods: { toggleCustomInput(shouldEnable) { this.customInputEnabled = shouldEnable; @@ -43,20 +57,6 @@ } }, }, - created() { - if (this.intervalIsPreset) { - this.enableCustomInput = false; - } - }, - watch: { - cronInterval() { - // updates field validation state when model changes, as - // glFieldError only updates on input. - this.$nextTick(() => { - gl.pipelineScheduleFieldErrors.updateFormValidityState(); - }); - }, - }, }; </script> @@ -78,7 +78,12 @@ </label> <span class="cron-syntax-link-wrap"> - (<a :href="cronSyntaxUrl" target="_blank">{{ __('Cron syntax') }}</a>) + (<a + :href="cronSyntaxUrl" + target="_blank" + > + {{ __('Cron syntax') }} + </a>) </span> </div> @@ -93,7 +98,10 @@ @click="toggleCustomInput(false)" /> - <label class="label-light" for="every-day"> + <label + class="label-light" + for="every-day" + > {{ __('Every day (at 4:00am)') }} </label> </div> @@ -109,7 +117,10 @@ @click="toggleCustomInput(false)" /> - <label class="label-light" for="every-week"> + <label + class="label-light" + for="every-week" + > {{ __('Every week (Sundays at 4:00am)') }} </label> </div> @@ -125,7 +136,10 @@ @click="toggleCustomInput(false)" /> - <label class="label-light" for="every-month"> + <label + class="label-light" + for="every-month" + > {{ __('Every month (on the 1st at 4:00am)') }} </label> </div> diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue index 6e0bc2d697a..aa04a0ac47a 100644 --- a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue +++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue @@ -16,15 +16,15 @@ calloutDismissed: Cookies.get(cookieKey) === 'true', }; }, + created() { + this.illustrationSvg = illustrationSvg; + }, methods: { dismissCallout() { this.calloutDismissed = true; Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 }); }, }, - created() { - this.illustrationSvg = illustrationSvg; - }, }; </script> <template> @@ -41,17 +41,25 @@ class="fa fa-times"> </i> </button> - <div class="svg-container" v-html="illustrationSvg"></div> + <div + class="svg-container" + v-html="illustrationSvg"> + </div> <div class="user-callout-copy"> <h4>{{ __('Scheduling Pipelines') }}</h4> <p> - {{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }} + {{ __(`The pipelines schedule runs pipelines in the future, +repeatedly, for specific branches or tags. +Those scheduled pipelines will inherit limited project access based on their associated user.`) }} </p> <p> {{ __('Learn more in the') }} <a :href="docsUrl" target="_blank" - rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period --> + rel="nofollow" + > + {{ s__('Learn more in the|pipeline schedules documentation') }}</a>. + <!-- oneline to prevent extra space before period --> </p> </div> </div> diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js index f1cf6e92ef5..0b1a81bae13 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js +++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js @@ -4,7 +4,7 @@ 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 { setupPipelineVariableList } from './setup_pipeline_variable_list'; +import setupNativeFormVariableList from '../ci_variable_list/native_form_variable_list'; Vue.use(Translate); @@ -42,5 +42,8 @@ document.addEventListener('DOMContentLoaded', () => { gl.targetBranchDropdown = new TargetBranchDropdown(); gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement); - setupPipelineVariableList($('.js-pipeline-variable-list')); + setupNativeFormVariableList({ + container: $('.js-ci-variable-list-section'), + formField: 'schedule', + }); }); diff --git a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js deleted file mode 100644 index 9e0e5cacb11..00000000000 --- a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js +++ /dev/null @@ -1,73 +0,0 @@ -import { convertPermissionToBoolean } from '../lib/utils/common_utils'; - -function insertRow($row) { - const $rowClone = $row.clone(); - $rowClone.removeAttr('data-is-persisted'); - $rowClone.find('input, textarea').val(''); - $row.after($rowClone); -} - -function removeRow($row) { - const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted')); - - if (isPersisted) { - $row.hide(); - $row - .find('.js-destroy-input') - .val(1); - } else { - $row.remove(); - } -} - -function checkIfRowTouched($row) { - return $row.find('.js-user-input').toArray().some(el => $(el).val().length > 0); -} - -function setupPipelineVariableList(parent = document) { - const $parent = $(parent); - - $parent.on('click', '.js-row-remove-button', (e) => { - const $row = $(e.currentTarget).closest('.js-row'); - removeRow($row); - - e.preventDefault(); - }); - - // Remove any empty rows except the last r - $parent.on('blur', '.js-user-input', (e) => { - const $row = $(e.currentTarget).closest('.js-row'); - - const isTouched = checkIfRowTouched($row); - if ($row.is(':not(:last-child)') && !isTouched) { - removeRow($row); - } - }); - - // Always make sure there is an empty last row - $parent.on('input', '.js-user-input', () => { - const $lastRow = $parent.find('.js-row').last(); - - const isTouched = checkIfRowTouched($lastRow); - if (isTouched) { - insertRow($lastRow); - } - }); - - // Clear out the empty last row so it - // doesn't get submitted and throw validation errors - $parent.closest('form').on('submit', () => { - const $lastRow = $parent.find('.js-row').last(); - - const isTouched = checkIfRowTouched($lastRow); - if (!isTouched) { - $lastRow.find('input, textarea').attr('name', ''); - } - }); -} - -export { - setupPipelineVariableList, - insertRow, - removeRow, -}; diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue index 16cc0761fc1..77553ca67cc 100644 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -1,67 +1,67 @@ <script> -/* eslint-disable no-new, no-alert */ + /* eslint-disable no-alert */ -import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import icon from '../../vue_shared/components/icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; -export default { - props: { - endpoint: { - type: String, - required: true, + export default { + directives: { + tooltip, }, - title: { - type: String, - required: true, + components: { + loadingIcon, + icon, }, - icon: { - type: String, - required: true, + props: { + endpoint: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + icon: { + type: String, + required: true, + }, + cssClass: { + type: String, + required: true, + }, + confirmActionMessage: { + type: String, + required: false, + default: '', + }, }, - cssClass: { - type: String, - required: true, + data() { + return { + isLoading: false, + }; }, - confirmActionMessage: { - type: String, - required: false, + computed: { + buttonClass() { + return `btn ${this.cssClass}`; + }, }, - }, - directives: { - tooltip, - }, - components: { - loadingIcon, - }, - data() { - return { - isLoading: false, - }; - }, - computed: { - iconClass() { - return `fa fa-${this.icon}`; - }, - buttonClass() { - return `btn ${this.cssClass}`; - }, - }, - methods: { - onClick() { - if (this.confirmActionMessage && confirm(this.confirmActionMessage)) { - this.makeRequest(); - } else if (!this.confirmActionMessage) { - this.makeRequest(); - } - }, - makeRequest() { - this.isLoading = true; + methods: { + onClick() { + if (this.confirmActionMessage !== '' && confirm(this.confirmActionMessage)) { + this.makeRequest(); + } else if (this.confirmActionMessage === '') { + this.makeRequest(); + } + }, + makeRequest() { + this.isLoading = true; - eventHub.$emit('postAction', this.endpoint); + eventHub.$emit('postAction', this.endpoint); + }, }, - }, -}; + }; </script> <template> @@ -75,10 +75,9 @@ export default { data-container="body" data-placement="top" :disabled="isLoading"> - <i - :class="iconClass" - aria-hidden="true"> - </i> + <icon + :name="icon" + /> <loading-icon v-if="isLoading" /> </button> </template> diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue index 78322f30685..dfaa2574091 100644 --- a/app/assets/javascripts/pipelines/components/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/empty_state.vue @@ -26,13 +26,15 @@ {{ s__("Pipelines|Build with confidence") }} </h4> <p> - {{ s__("Pipelines|Continous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment.") }} + {{ s__(`Pipelines|Continous Integration can help +catch bugs by running your tests automatically, +while Continuous Deployment can help you deliver code to your product environment.`) }} </p> <div class="text-center"> <a :href="helpPagePath" class="btn btn-info" - > + > {{ s__("Pipelines|Get started with Pipelines") }} </a> </div> diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 19d8e1f49cf..d7effb27bff 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -7,6 +7,14 @@ * TODO: Remove UJS from here and use an async request instead. */ export default { + components: { + icon, + }, + + directives: { + tooltip, + }, + props: { tooltipText: { type: String, @@ -29,14 +37,6 @@ }, }, - components: { - icon, - }, - - directives: { - tooltip, - }, - computed: { cssClass() { const actionIconDash = dasherize(this.actionIcon); @@ -53,7 +53,8 @@ :href="link" class="ci-action-icon-container ci-action-icon-wrapper" :class="cssClass" - data-container="body"> - <icon :name="actionIcon"/> + data-container="body" + > + <icon :name="actionIcon" /> </a> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue index 1c0944d45fc..7c4fd65e36f 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue @@ -7,6 +7,13 @@ * TODO: Remove UJS from here and use an async request instead. */ export default { + components: { + icon, + }, + + directives: { + tooltip, + }, props: { tooltipText: { type: String, @@ -28,14 +35,6 @@ required: true, }, }, - - components: { - icon, - }, - - directives: { - tooltip, - }, }; </script> <template> @@ -47,7 +46,8 @@ rel="nofollow" class="ci-action-icon-wrapper js-ci-status-icon" data-container="body" - aria-label="Job's action"> - <icon :name="actionIcon"/> + aria-label="Job's action" + > + <icon :name="actionIcon" /> </a> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index 7006d05e7b2..b86e95f0b4a 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -27,13 +27,6 @@ * } */ export default { - props: { - job: { - type: Object, - required: true, - }, - }, - directives: { tooltip, }, @@ -43,12 +36,23 @@ jobNameComponent, }, + props: { + job: { + type: Object, + required: true, + }, + }, + computed: { tooltipText() { return `${this.job.name} - ${this.job.status.label}`; }, }, + mounted() { + this.stopDropdownClickPropagation(); + }, + methods: { /** * When the user right clicks or cmd/ctrl + click in the job name @@ -59,16 +63,13 @@ * target the click event of this component. */ stopDropdownClickPropagation() { - $(this.$el.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) + $(this.$el + .querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) .on('click', (e) => { e.stopPropagation(); }); }, }, - - mounted() { - this.stopDropdownClickPropagation(); - }, }; </script> <template> @@ -83,22 +84,25 @@ <job-name-component :name="job.name" - :status="job.status" /> + :status="job.status" + /> <span class="dropdown-counter-badge"> - {{job.size}} + {{ job.size }} </span> </button> <ul class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown"> <li class="scrollable-menu"> <ul> - <li v-for="item in job.jobs"> + <li + v-for="(item, i) in job.jobs" + :key="i"> <job-component :job="item" :is-dropdown="true" css-class-job-name="mini-pipeline-graph-dropdown-item" - /> + /> </li> </ul> </li> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 66bc1d1979c..a1f58580318 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,9 +1,13 @@ <script> import loadingIcon from '~/vue_shared/components/loading_icon.vue'; - import '~/flash'; import stageColumnComponent from './stage_column_component.vue'; export default { + components: { + stageColumnComponent, + loadingIcon, + }, + props: { isLoading: { type: Boolean, @@ -15,11 +19,6 @@ }, }, - components: { - stageColumnComponent, - loadingIcon, - }, - computed: { graph() { return this.pipeline.details && this.pipeline.details.stages; @@ -58,7 +57,7 @@ <loading-icon v-if="isLoading" size="3" - /> + /> </div> <ul @@ -70,7 +69,8 @@ :jobs="stage.groups" :key="stage.name" :stage-connector-class="stageConnectorClass(index, stage)" - :is-first-column="isFirstColumn(index)"/> + :is-first-column="isFirstColumn(index)" + /> </ul> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index b01c799643c..9b136573135 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -29,6 +29,15 @@ */ export default { + components: { + actionComponent, + dropdownActionComponent, + jobNameComponent, + }, + + directives: { + tooltip, + }, props: { job: { type: Object, @@ -48,16 +57,6 @@ }, }, - components: { - actionComponent, - dropdownActionComponent, - jobNameComponent, - }, - - directives: { - tooltip, - }, - computed: { status() { return this.job && this.job.status ? this.job.status : {}; @@ -102,12 +101,12 @@ :class="cssClassJobName" data-container="body" class="js-pipeline-graph-job-link" - > + > <job-name-component :name="job.name" :status="job.status" - /> + /> </a> <div @@ -117,12 +116,12 @@ :title="tooltipText" :class="cssClassJobName" data-container="body" - > + > <job-name-component :name="job.name" :status="job.status" - /> + /> </div> <action-component @@ -131,7 +130,7 @@ :link="status.action.path" :action-icon="status.action.icon" :action-method="status.action.method" - /> + /> <dropdown-action-component v-if="hasAction && isDropdown" @@ -139,6 +138,6 @@ :link="status.action.path" :action-icon="status.action.icon" :action-method="status.action.method" - /> + /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue index f46d21bd6d7..14f4964a406 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue @@ -8,6 +8,9 @@ * - Dropdown badge components */ export default { + components: { + ciIcon, + }, props: { name: { type: String, @@ -19,19 +22,14 @@ required: true, }, }, - - components: { - ciIcon, - }, }; </script> <template> <span class="ci-job-name-component"> - <ci-icon - :status="status" /> + <ci-icon :status="status" /> <span class="ci-status-text"> - {{name}} + {{ name }} </span> </span> </template> 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 9b1bbb0906f..e027f08ff5c 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,58 +1,58 @@ <script> -import jobComponent from './job_component.vue'; -import dropdownJobComponent from './dropdown_job_component.vue'; + import jobComponent from './job_component.vue'; + import dropdownJobComponent from './dropdown_job_component.vue'; -export default { - props: { - title: { - type: String, - required: true, + export default { + components: { + jobComponent, + dropdownJobComponent, }, - jobs: { - type: Array, - required: true, - }, - - isFirstColumn: { - type: Boolean, - required: false, - default: false, - }, + props: { + title: { + type: String, + required: true, + }, - stageConnectorClass: { - type: String, - required: false, - default: '', - }, - }, + jobs: { + type: Array, + required: true, + }, - components: { - jobComponent, - dropdownJobComponent, - }, + isFirstColumn: { + type: Boolean, + required: false, + default: false, + }, - methods: { - firstJob(list) { - return list[0]; + stageConnectorClass: { + type: String, + required: false, + default: '', + }, }, - jobId(job) { - return `ci-badge-${job.name}`; - }, + methods: { + firstJob(list) { + return list[0]; + }, + + jobId(job) { + return `ci-badge-${job.name}`; + }, - buildConnnectorClass(index) { - return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; + buildConnnectorClass(index) { + return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; + }, }, - }, -}; + }; </script> <template> <li class="stage-column" :class="stageConnectorClass"> <div class="stage-name"> - {{title}} + {{ title }} </div> <div class="builds-container"> <ul> @@ -61,7 +61,8 @@ export default { :key="job.id" class="build" :class="buildConnnectorClass(index)" - :id="jobId(job)"> + :id="jobId(job)" + > <div class="curve"></div> @@ -69,12 +70,12 @@ export default { v-if="job.size === 1" :job="job" css-class-job-name="build-content" - /> + /> <dropdown-job-component v-if="job.size > 1" :job="job" - /> + /> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index 2a1ecac3707..e08c2092680 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -1,82 +1,81 @@ <script> -import ciHeader from '../../vue_shared/components/header_ci_component.vue'; -import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import ciHeader from '../../vue_shared/components/header_ci_component.vue'; + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -export default { - name: 'PipelineHeaderSection', - props: { - pipeline: { - type: Object, - required: true, + export default { + name: 'PipelineHeaderSection', + components: { + ciHeader, + loadingIcon, }, - isLoading: { - type: Boolean, - required: true, + props: { + pipeline: { + type: Object, + required: true, + }, + isLoading: { + type: Boolean, + required: true, + }, }, - }, - components: { - ciHeader, - loadingIcon, - }, - - data() { - return { - actions: this.getActions(), - }; - }, - - computed: { - status() { - return this.pipeline.details && this.pipeline.details.status; + data() { + return { + actions: this.getActions(), + }; }, - shouldRenderContent() { - return !this.isLoading && Object.keys(this.pipeline).length; + + computed: { + status() { + return this.pipeline.details && this.pipeline.details.status; + }, + shouldRenderContent() { + return !this.isLoading && Object.keys(this.pipeline).length; + }, }, - }, - methods: { - postAction(action) { - const index = this.actions.indexOf(action); + watch: { + pipeline() { + this.actions = this.getActions(); + }, + }, - this.$set(this.actions[index], 'isLoading', true); + methods: { + postAction(action) { + const index = this.actions.indexOf(action); - eventHub.$emit('headerPostAction', action); - }, + this.$set(this.actions[index], 'isLoading', true); - getActions() { - const actions = []; + eventHub.$emit('headerPostAction', action); + }, - if (this.pipeline.retry_path) { - actions.push({ - label: 'Retry', - path: this.pipeline.retry_path, - cssClass: 'js-retry-button btn btn-inverted-secondary', - type: 'button', - isLoading: false, - }); - } + getActions() { + const actions = []; - if (this.pipeline.cancel_path) { - actions.push({ - label: 'Cancel running', - path: this.pipeline.cancel_path, - cssClass: 'js-btn-cancel-pipeline btn btn-danger', - type: 'button', - isLoading: false, - }); - } + if (this.pipeline.retry_path) { + actions.push({ + label: 'Retry', + path: this.pipeline.retry_path, + cssClass: 'js-retry-button btn btn-inverted-secondary', + type: 'button', + isLoading: false, + }); + } - return actions; - }, - }, + if (this.pipeline.cancel_path) { + actions.push({ + label: 'Cancel running', + path: this.pipeline.cancel_path, + cssClass: 'js-btn-cancel-pipeline btn btn-danger', + type: 'button', + isLoading: false, + }); + } - watch: { - pipeline() { - this.actions = this.getActions(); + return actions; + }, }, - }, -}; + }; </script> <template> <div class="pipeline-header-container"> @@ -89,9 +88,11 @@ export default { :user="pipeline.user" :actions="actions" @actionClicked="postAction" - /> + /> <loading-icon v-if="isLoading" - size="2"/> + size="2" + class="prepend-top-default append-bottom-default" + /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue index 632fc167f2b..f31a91c3403 100644 --- a/app/assets/javascripts/pipelines/components/nav_controls.vue +++ b/app/assets/javascripts/pipelines/components/nav_controls.vue @@ -17,6 +17,11 @@ export default { required: true, }, + resetCachePath: { + type: String, + required: true, + }, + ciLintPath: { type: String, required: true, @@ -46,6 +51,14 @@ export default { </a> <a + data-method="post" + rel="nofollow" + :href="resetCachePath" + class="btn btn-default"> + Clear runner caches + </a> + + <a :href="ciLintPath" class="btn btn-default"> CI Lint diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 9da0aac50a1..ceb4d9ca604 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -4,6 +4,13 @@ import popover from '../../vue_shared/directives/popover'; export default { + components: { + userAvatarLink, + }, + directives: { + tooltip, + popover, + }, props: { pipeline: { type: Object, @@ -14,13 +21,6 @@ required: true, }, }, - components: { - userAvatarLink, - }, - directives: { - tooltip, - popover, - }, computed: { user() { return this.pipeline.user; @@ -30,8 +30,16 @@ html: true, trigger: 'focus', placement: 'top', - title: '<div class="autodevops-title">This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b></div>', - content: `<a class="autodevops-link" href="${this.autoDevopsHelpPath}" target="_blank" rel="noopener noreferrer nofollow">Learn more about Auto DevOps</a>`, + title: `<div class="autodevops-title"> + This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b> + </div>`, + content: `<a + class="autodevops-link" + href="${this.autoDevopsHelpPath}" + target="_blank" + rel="noopener noreferrer nofollow"> + Learn more about Auto DevOps + </a>`, }; }, }, @@ -42,7 +50,7 @@ <a :href="pipeline.path" class="js-pipeline-url-link"> - <span class="pipeline-id">#{{pipeline.id}}</span> + <span class="pipeline-id">#{{ pipeline.id }}</span> </a> <span>by</span> <user-avatar-link diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index fe1f3b4246a..90930d5ff44 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -13,6 +13,15 @@ import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; export default { + components: { + tablePagination, + navigationTabs, + navigationControls, + }, + mixins: [ + pipelinesMixin, + CIPaginationMixin, + ], props: { store: { type: Object, @@ -28,15 +37,6 @@ default: 'root', }, }, - components: { - tablePagination, - navigationTabs, - navigationControls, - }, - mixins: [ - pipelinesMixin, - CIPaginationMixin, - ], data() { const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; @@ -50,6 +50,7 @@ canCreatePipeline: pipelinesData.canCreatePipeline, hasCi: pipelinesData.hasCi, ciLintPath: pipelinesData.ciLintPath, + resetCachePath: pipelinesData.resetCachePath, state: this.store.state, scope: getParameterByName('scope') || 'all', page: getParameterByName('page') || '1', @@ -196,7 +197,8 @@ <div class="pipelines-container"> <div class="top-area scrolling-tabs-container inner-page-scroll-tabs" - v-if="!shouldRenderEmptyState"> + v-if="!shouldRenderEmptyState" + > <div class="fade-left"> <i class="fa fa-angle-left" @@ -214,15 +216,16 @@ :tabs="tabs" @onChangeTab="onChangeTab" scope="pipelines" - /> + /> <navigation-controls :new-pipeline-path="newPipelinePath" :has-ci-enabled="hasCiEnabled" :help-page-path="helpPagePath" + :reset-cache-path="resetCachePath" :ci-lint-path="ciLintPath" :can-create-pipeline="canCreatePipelineParsed " - /> + /> </div> <div class="content-list pipelines"> @@ -232,22 +235,23 @@ size="3" v-if="isLoading" class="prepend-top-20" - /> + /> <empty-state v-if="shouldRenderEmptyState" :help-page-path="helpPagePath" :empty-state-svg-path="emptyStateSvgPath" - /> + /> <error-state v-if="shouldRenderErrorState" :error-state-svg-path="errorStateSvgPath" - /> + /> <div class="blank-state-row" - v-if="shouldRenderNoPipelinesMessage"> + v-if="shouldRenderNoPipelinesMessage" + > <div class="blank-state-center"> <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> </div> @@ -255,21 +259,22 @@ <div class="table-holder" - v-if="shouldRenderTable"> + v-if="shouldRenderTable" + > <pipelines-table-component :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" :auto-devops-help-path="autoDevopsPath" :view-type="viewType" - /> + /> </div> <table-pagination v-if="shouldRenderPagination" :change="onChangePage" :page-info="state.pageInfo" - /> + /> </div> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index f3c0aca17ba..3297af7bde4 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -1,25 +1,25 @@ <script> - import playIconSvg from 'icons/_icon_play.svg'; import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import icon from '../../vue_shared/components/icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; export default { - props: { - actions: { - type: Array, - required: true, - }, - }, directives: { tooltip, }, components: { loadingIcon, + icon, + }, + props: { + actions: { + type: Array, + required: true, + }, }, data() { return { - playIconSvg, isLoading: false, }; }, @@ -50,8 +50,12 @@ data-toggle="dropdown" data-placement="top" aria-label="Manual job" - :disabled="isLoading"> - <span v-html="playIconSvg"></span> + :disabled="isLoading" + > + <icon + name="play" + class="icon-play" + /> <i class="fa fa-caret-down" aria-hidden="true"> @@ -60,14 +64,18 @@ </button> <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for="action in actions"> + <li + v-for="(action, i) in actions" + :key="i" + > <button type="button" class="js-pipeline-action-link no-btn btn" @click="onClickAction(action.path)" :class="{ disabled: isActionDisabled(action) }" - :disabled="isActionDisabled(action)"> - {{action.name}} + :disabled="isActionDisabled(action)" + > + {{ action.name }} </button> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue index 751a20991af..1b9e0f917a4 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -1,45 +1,52 @@ <script> import tooltip from '../../vue_shared/directives/tooltip'; + import icon from '../../vue_shared/components/icon.vue'; export default { + directives: { + tooltip, + }, + components: { + icon, + }, props: { artifacts: { type: Array, required: true, }, }, - directives: { - tooltip, - }, }; </script> <template> <div class="btn-group" - role="group"> + role="group" + > <button v-tooltip class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download" title="Artifacts" data-placement="top" data-toggle="dropdown" - aria-label="Artifacts"> - <i - class="fa fa-download" - aria-hidden="true"> - </i> + aria-label="Artifacts" + > + <icon name="download" /> <i class="fa fa-caret-down" - aria-hidden="true"> + aria-hidden="true" + > </i> </button> <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for="artifact in artifacts"> + <li + v-for="(artifact, i) in artifacts" + :key="i"> <a rel="nofollow" download - :href="artifact.path"> - Download {{artifact.name}} artifacts + :href="artifact.path" + > + Download {{ artifact.name }} artifacts </a> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 16a705cbaff..6681b89e629 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -7,6 +7,9 @@ * Given an array of objects, renders a table. */ export default { + components: { + pipelinesTableRowComponent, + }, props: { pipelines: { type: Array, @@ -26,34 +29,36 @@ required: true, }, }, - components: { - pipelinesTableRowComponent, - }, }; </script> <template> <div class="ci-table"> <div class="gl-responsive-table-row table-row-header" - role="row"> + role="row" + > <div class="table-section section-10 js-pipeline-status pipeline-status" - role="rowheader"> + role="rowheader" + > Status </div> <div class="table-section section-15 js-pipeline-info pipeline-info" - role="rowheader"> + role="rowheader" + > Pipeline </div> <div - class="table-section section-25 js-pipeline-commit pipeline-commit" - role="rowheader"> + class="table-section section-20 js-pipeline-commit pipeline-commit" + role="rowheader" + > Commit </div> <div - class="table-section section-15 js-pipeline-stages pipeline-stages" - role="rowheader"> + class="table-section section-20 js-pipeline-stages pipeline-stages" + role="rowheader" + > Stages </div> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 33fbce993b2..d0e4cf7ff40 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -1,227 +1,228 @@ <script> -/* eslint-disable no-param-reassign */ -import asyncButtonComponent from './async_button.vue'; -import pipelinesActionsComponent from './pipelines_actions.vue'; -import pipelinesArtifactsComponent from './pipelines_artifacts.vue'; -import ciBadge from '../../vue_shared/components/ci_badge_link.vue'; -import pipelineStage from './stage.vue'; -import pipelineUrl from './pipeline_url.vue'; -import pipelinesTimeago from './time_ago.vue'; -import commitComponent from '../../vue_shared/components/commit.vue'; + /* eslint-disable no-param-reassign */ + import asyncButtonComponent from './async_button.vue'; + import pipelinesActionsComponent from './pipelines_actions.vue'; + import pipelinesArtifactsComponent from './pipelines_artifacts.vue'; + import ciBadge from '../../vue_shared/components/ci_badge_link.vue'; + import pipelineStage from './stage.vue'; + import pipelineUrl from './pipeline_url.vue'; + import pipelinesTimeago from './time_ago.vue'; + import commitComponent from '../../vue_shared/components/commit.vue'; -/** - * Pipeline table row. - * - * Given the received object renders a table row in the pipelines' table. - */ -export default { - props: { - pipeline: { - type: Object, - required: true, + /** + * Pipeline table row. + * + * Given the received object renders a table row in the pipelines' table. + */ + export default { + components: { + asyncButtonComponent, + pipelinesActionsComponent, + pipelinesArtifactsComponent, + commitComponent, + pipelineStage, + pipelineUrl, + ciBadge, + pipelinesTimeago, }, - updateGraphDropdown: { - type: Boolean, - required: false, - default: false, + props: { + pipeline: { + type: Object, + required: true, + }, + updateGraphDropdown: { + type: Boolean, + required: false, + default: false, + }, + autoDevopsHelpPath: { + type: String, + required: true, + }, + viewType: { + type: String, + required: true, + }, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, - viewType: { - type: String, - required: true, - }, - }, - components: { - asyncButtonComponent, - pipelinesActionsComponent, - pipelinesArtifactsComponent, - commitComponent, - pipelineStage, - pipelineUrl, - ciBadge, - pipelinesTimeago, - }, - computed: { - /** - * If provided, returns the commit tag. - * Needed to render the commit component column. - * - * This field needs a lot of verification, because of different possible cases: - * - * 1. person who is an author of a commit might be a GitLab user - * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar - * 3. If GitLab user does not have avatar he/she might have a Gravatar - * 4. If committer is not a GitLab User he/she can have a Gravatar - * 5. We do not have consistent API object in this case - * 6. We should improve API and the code - * - * @returns {Object|Undefined} - */ - commitAuthor() { - let commitAuthorInformation; + computed: { + /** + * If provided, returns the commit tag. + * Needed to render the commit component column. + * + * This field needs a lot of verification, because of different possible cases: + * + * 1. person who is an author of a commit might be a GitLab user + * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar + * 3. If GitLab user does not have avatar he/she might have a Gravatar + * 4. If committer is not a GitLab User he/she can have a Gravatar + * 5. We do not have consistent API object in this case + * 6. We should improve API and the code + * + * @returns {Object|Undefined} + */ + commitAuthor() { + let commitAuthorInformation; - if (!this.pipeline || !this.pipeline.commit) { - return null; - } + if (!this.pipeline || !this.pipeline.commit) { + return null; + } - // 1. person who is an author of a commit might be a GitLab user - if (this.pipeline.commit.author) { - // 2. if person who is an author of a commit is a GitLab user - // he/she can have a GitLab avatar - if (this.pipeline.commit.author.avatar_url) { - commitAuthorInformation = this.pipeline.commit.author; + // 1. person who is an author of a commit might be a GitLab user + if (this.pipeline.commit.author) { + // 2. if person who is an author of a commit is a GitLab user + // he/she can have a GitLab avatar + if (this.pipeline.commit.author.avatar_url) { + commitAuthorInformation = this.pipeline.commit.author; - // 3. If GitLab user does not have avatar he/she might have a Gravatar - } else if (this.pipeline.commit.author_gravatar_url) { - commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, { + // 3. If GitLab user does not have avatar he/she might have a Gravatar + } else if (this.pipeline.commit.author_gravatar_url) { + commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, { + avatar_url: this.pipeline.commit.author_gravatar_url, + }); + } + // 4. If committer is not a GitLab User he/she can have a Gravatar + } else { + commitAuthorInformation = { avatar_url: this.pipeline.commit.author_gravatar_url, - }); + path: `mailto:${this.pipeline.commit.author_email}`, + username: this.pipeline.commit.author_name, + }; } - // 4. If committer is not a GitLab User he/she can have a Gravatar - } else { - commitAuthorInformation = { - avatar_url: this.pipeline.commit.author_gravatar_url, - path: `mailto:${this.pipeline.commit.author_email}`, - username: this.pipeline.commit.author_name, - }; - } - return commitAuthorInformation; - }, + return commitAuthorInformation; + }, - /** - * If provided, returns the commit tag. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ - commitTag() { - if (this.pipeline.ref && - this.pipeline.ref.tag) { - return this.pipeline.ref.tag; - } - return undefined; - }, + /** + * If provided, returns the commit tag. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.pipeline.ref && + this.pipeline.ref.tag) { + return this.pipeline.ref.tag; + } + return undefined; + }, - /** - * If provided, returns the commit ref. - * Needed to render the commit component column. - * - * Matches `path` prop sent in the API to `ref_url` prop needed - * in the commit component. - * - * @returns {Object|Undefined} - */ - commitRef() { - if (this.pipeline.ref) { - return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { - if (prop === 'path') { - accumulator.ref_url = this.pipeline.ref[prop]; - } else { - accumulator[prop] = this.pipeline.ref[prop]; - } - return accumulator; - }, {}); - } + /** + * If provided, returns the commit ref. + * Needed to render the commit component column. + * + * Matches `path` prop sent in the API to `ref_url` prop needed + * in the commit component. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.pipeline.ref) { + return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { + if (prop === 'path') { + accumulator.ref_url = this.pipeline.ref[prop]; + } else { + accumulator[prop] = this.pipeline.ref[prop]; + } + return accumulator; + }, {}); + } - return undefined; - }, + return undefined; + }, - /** - * If provided, returns the commit url. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ - commitUrl() { - if (this.pipeline.commit && - this.pipeline.commit.commit_path) { - return this.pipeline.commit.commit_path; - } - return undefined; - }, + /** + * If provided, returns the commit url. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.pipeline.commit && + this.pipeline.commit.commit_path) { + return this.pipeline.commit.commit_path; + } + return undefined; + }, - /** - * If provided, returns the commit short sha. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ - commitShortSha() { - if (this.pipeline.commit && - this.pipeline.commit.short_id) { - return this.pipeline.commit.short_id; - } - return undefined; - }, + /** + * If provided, returns the commit short sha. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.pipeline.commit && + this.pipeline.commit.short_id) { + return this.pipeline.commit.short_id; + } + return undefined; + }, - /** - * If provided, returns the commit title. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ - commitTitle() { - if (this.pipeline.commit && - this.pipeline.commit.title) { - return this.pipeline.commit.title; - } - return undefined; - }, + /** + * If provided, returns the commit title. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.pipeline.commit && + this.pipeline.commit.title) { + return this.pipeline.commit.title; + } + return undefined; + }, - /** - * Timeago components expects a number - * - * @return {type} description - */ - pipelineDuration() { - if (this.pipeline.details && this.pipeline.details.duration) { - return this.pipeline.details.duration; - } + /** + * Timeago components expects a number + * + * @return {type} description + */ + pipelineDuration() { + if (this.pipeline.details && this.pipeline.details.duration) { + return this.pipeline.details.duration; + } - return 0; - }, + return 0; + }, - /** - * Timeago component expects a String. - * - * @return {String} - */ - pipelineFinishedAt() { - if (this.pipeline.details && this.pipeline.details.finished_at) { - return this.pipeline.details.finished_at; - } + /** + * Timeago component expects a String. + * + * @return {String} + */ + pipelineFinishedAt() { + if (this.pipeline.details && this.pipeline.details.finished_at) { + return this.pipeline.details.finished_at; + } - return ''; - }, + return ''; + }, - pipelineStatus() { - if (this.pipeline.details && this.pipeline.details.status) { - return this.pipeline.details.status; - } - return {}; - }, + pipelineStatus() { + if (this.pipeline.details && this.pipeline.details.status) { + return this.pipeline.details.status; + } + return {}; + }, - displayPipelineActions() { - return this.pipeline.flags.retryable || - this.pipeline.flags.cancelable || - this.pipeline.details.manual_actions.length || - this.pipeline.details.artifacts.length; - }, + displayPipelineActions() { + return this.pipeline.flags.retryable || + this.pipeline.flags.cancelable || + this.pipeline.details.manual_actions.length || + this.pipeline.details.artifacts.length; + }, - isChildView() { - return this.viewType === 'child'; + isChildView() { + return this.viewType === 'child'; + }, }, - }, -}; + }; </script> <template> <div class="commit gl-responsive-table-row"> <div class="table-section section-10 commit-link"> - <div class="table-mobile-header" + <div + class="table-mobile-header" role="rowheader"> Status </div> @@ -229,16 +230,16 @@ export default { <ci-badge :status="pipelineStatus" :show-text="!isChildView" - /> + /> </div> </div> <pipeline-url :pipeline="pipeline" :auto-devops-help-path="autoDevopsHelpPath" - /> + /> - <div class="table-section section-25"> + <div class="table-section section-20"> <div class="table-mobile-header" role="rowheader"> @@ -253,32 +254,35 @@ export default { :title="commitTitle" :author="commitAuthor" :show-branch="!isChildView" - /> + /> </div> </div> - <div class="table-section section-wrap section-15 stage-cell"> + <div class="table-section section-wrap section-20 stage-cell"> <div class="table-mobile-header" role="rowheader"> Stages </div> <div class="table-mobile-content"> - <div class="stage-container dropdown js-mini-pipeline-graph" - v-if="pipeline.details.stages.length > 0" - v-for="stage in pipeline.details.stages"> - <pipeline-stage - :stage="stage" - :update-dropdown="updateGraphDropdown" + <template v-if="pipeline.details.stages.length > 0"> + <div + class="stage-container dropdown js-mini-pipeline-graph" + v-for="(stage, index) in pipeline.details.stages" + :key="index"> + <pipeline-stage + :stage="stage" + :update-dropdown="updateGraphDropdown" /> - </div> + </div> + </template> </div> </div> <pipelines-timeago :duration="pipelineDuration" :finished-time="pipelineFinishedAt" - /> + /> <div v-if="displayPipelineActions" @@ -287,13 +291,13 @@ export default { <pipelines-actions-component v-if="pipeline.details.manual_actions.length" :actions="pipeline.details.manual_actions" - /> + /> <pipelines-artifacts-component v-if="pipeline.details.artifacts.length" class="hidden-xs hidden-sm" :artifacts="pipeline.details.artifacts" - /> + /> <async-button-component v-if="pipeline.flags.retryable" @@ -301,16 +305,16 @@ export default { css-class="js-pipelines-retry-button btn-default btn-retry" title="Retry" icon="repeat" - /> + /> <async-button-component v-if="pipeline.flags.cancelable" :endpoint="pipeline.cancel_path" css-class="js-pipelines-cancel-button btn-remove" title="Cancel" - icon="remove" + icon="close" confirm-action-message="Are you sure you want to cancel this pipeline?" - /> + /> </div> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index ac9d9c901ca..58806aa114a 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -1,133 +1,135 @@ <script> -/** - * Renders each stage of the pipeline mini graph. - * - * Given the provided endpoint will make a request to - * fetch the dropdown data when the stage is clicked. - * - * Request is made inside this component to make it reusable between: - * 1. Pipelines main table - * 2. Pipelines table in commit and Merge request views - * 3. Merge request widget - * 4. Commit widget - */ - -import Flash from '../../flash'; -import icon from '../../vue_shared/components/icon.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; - -export default { - props: { - stage: { - type: Object, - required: true, + /** + * Renders each stage of the pipeline mini graph. + * + * Given the provided endpoint will make a request to + * fetch the dropdown data when the stage is clicked. + * + * Request is made inside this component to make it reusable between: + * 1. Pipelines main table + * 2. Pipelines table in commit and Merge request views + * 3. Merge request widget + * 4. Commit widget + */ + + import Flash from '../../flash'; + import icon from '../../vue_shared/components/icon.vue'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; + + export default { + components: { + loadingIcon, + icon, }, - updateDropdown: { - type: Boolean, - required: false, - default: false, + directives: { + tooltip, }, - }, - - directives: { - tooltip, - }, - - data() { - return { - isLoading: false, - dropdownContent: '', - }; - }, - - components: { - loadingIcon, - icon, - }, - - updated() { - if (this.dropdownContent.length > 0) { - this.stopDropdownClickPropagation(); - } - }, - - watch: { - updateDropdown() { - if (this.updateDropdown && - this.isDropdownOpen() && - !this.isLoading) { - this.fetchJobs(); - } - }, - }, - methods: { - onClickStage() { - if (!this.isDropdownOpen()) { - this.isLoading = true; - this.fetchJobs(); - } + props: { + stage: { + type: Object, + required: true, + }, + + updateDropdown: { + type: Boolean, + required: false, + default: false, + }, }, - fetchJobs() { - this.$http.get(this.stage.dropdown_path) - .then(response => response.json()) - .then((data) => { - this.dropdownContent = data.html; - this.isLoading = false; - }) - .catch(() => { - this.closeDropdown(); - this.isLoading = false; - - const flash = new Flash('Something went wrong on our end.'); - return flash; - }); + data() { + return { + isLoading: false, + dropdownContent: '', + }; }, - /** - * When the user right clicks or cmd/ctrl + click in the job name - * the dropdown should not be closed and the link should open in another tab, - * so we stop propagation of the click event inside the dropdown. - * - * Since this component is rendered multiple times per page we need to guarantee we only - * target the click event of this component. - */ - stopDropdownClickPropagation() { - $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')) - .on('click', (e) => { - e.stopPropagation(); - }); - }, + computed: { + dropdownClass() { + return this.dropdownContent.length > 0 ? + 'js-builds-dropdown-container' : + 'js-builds-dropdown-loading'; + }, - closeDropdown() { - if (this.isDropdownOpen()) { - $(this.$refs.dropdown).dropdown('toggle'); - } - }, + triggerButtonClass() { + return `ci-status-icon-${this.stage.status.group}`; + }, - isDropdownOpen() { - return this.$el.classList.contains('open'); + borderlessIcon() { + return `${this.stage.status.icon}_borderless`; + }, }, - }, - computed: { - dropdownClass() { - return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading'; + watch: { + updateDropdown() { + if (this.updateDropdown && + this.isDropdownOpen() && + !this.isLoading) { + this.fetchJobs(); + } + }, }, - triggerButtonClass() { - return `ci-status-icon-${this.stage.status.group}`; + updated() { + if (this.dropdownContent.length > 0) { + this.stopDropdownClickPropagation(); + } }, - borderlessIcon() { - return `${this.stage.status.icon}_borderless`; + methods: { + onClickStage() { + if (!this.isDropdownOpen()) { + this.isLoading = true; + this.fetchJobs(); + } + }, + + fetchJobs() { + this.$http.get(this.stage.dropdown_path) + .then(response => response.json()) + .then((data) => { + this.dropdownContent = data.html; + this.isLoading = false; + }) + .catch(() => { + this.closeDropdown(); + this.isLoading = false; + + const flash = new Flash('Something went wrong on our end.'); + return flash; + }); + }, + + /** + * When the user right clicks or cmd/ctrl + click in the job name + * the dropdown should not be closed and the link should open in another tab, + * so we stop propagation of the click event inside the dropdown. + * + * Since this component is rendered multiple times per page we need to guarantee we only + * target the click event of this component. + */ + stopDropdownClickPropagation() { + $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')) + .on('click', (e) => { + e.stopPropagation(); + }); + }, + + closeDropdown() { + if (this.isDropdownOpen()) { + $(this.$refs.dropdown).dropdown('toggle'); + } + }, + + isDropdownOpen() { + return this.$el.classList.contains('open'); + }, }, - }, -}; + }; </script> <template> @@ -143,36 +145,41 @@ export default { type="button" id="stageDropdown" aria-haspopup="true" - aria-expanded="false"> + aria-expanded="false" + > <span aria-hidden="true" - :aria-label="stage.title"> - <icon - :name="borderlessIcon"/> + :aria-label="stage.title" + > + <icon :name="borderlessIcon" /> </span> <i class="fa fa-caret-down" - aria-hidden="true"> + aria-hidden="true" + > </i> </button> <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container" - aria-labelledby="stageDropdown"> + aria-labelledby="stageDropdown" + > <li :class="dropdownClass" - class="js-builds-dropdown-list scrollable-menu"> + class="js-builds-dropdown-list scrollable-menu" + > <loading-icon v-if="isLoading"/> <ul v-else - v-html="dropdownContent"> + v-html="dropdownContent" + > </ul> </li> </ul> </div> -</script> +</template> diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue index 037684b4e72..cd54d26c9d3 100644 --- a/app/assets/javascripts/pipelines/components/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/time_ago.vue @@ -5,6 +5,12 @@ import timeagoMixin from '../../vue_shared/mixins/timeago'; export default { + directives: { + tooltip, + }, + mixins: [ + timeagoMixin, + ], props: { finishedTime: { type: String, @@ -15,12 +21,6 @@ required: true, }, }, - mixins: [ - timeagoMixin, - ], - directives: { - tooltip, - }, data() { return { iconTimerSvg, @@ -60,26 +60,29 @@ <div class="table-section section-15 pipelines-time-ago"> <div class="table-mobile-header" - role="rowheader"> + role="rowheader" + > Duration </div> <div class="table-mobile-content"> <p class="duration" - v-if="hasDuration"> - <span - v-html="iconTimerSvg"> + v-if="hasDuration" + > + <span v-html="iconTimerSvg"> </span> - {{durationFormated}} + {{ durationFormated }} </p> <p class="finished-at hidden-xs hidden-sm" - v-if="hasFinishedTime"> + v-if="hasFinishedTime" + > <i class="fa fa-calendar" - aria-hidden="true"> + aria-hidden="true" + > </i> <time @@ -87,9 +90,9 @@ data-placement="top" data-container="body" :title="tooltipTitle(finishedTime)"> - {{timeFormated(finishedTime)}} + {{ timeFormated(finishedTime) }} </time> </p> </div> </div> -</script> +</template> diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 206023d4ddb..d88d280cb3f 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -15,14 +15,14 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line new Vue({ el: '#js-pipeline-graph-vue', + components: { + pipelineGraph, + }, data() { return { mediator, }; }, - components: { - pipelineGraph, - }, render(createElement) { return createElement('pipeline-graph', { props: { @@ -36,14 +36,14 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line new Vue({ el: '#js-pipeline-header-vue', + components: { + pipelineHeader, + }, data() { return { mediator, }; }, - components: { - pipelineHeader, - }, created() { eventHub.$on('headerPostAction', this.postAction); }, diff --git a/app/assets/javascripts/pipelines/pipelines_bundle.js b/app/assets/javascripts/pipelines/pipelines_bundle.js index 3e4b6eeb5bf..ab5596e70f0 100644 --- a/app/assets/javascripts/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/pipelines/pipelines_bundle.js @@ -7,6 +7,9 @@ Vue.use(Translate); document.addEventListener('DOMContentLoaded', () => new Vue({ el: '#pipelines-list-vue', + components: { + pipelinesComponent, + }, data() { const store = new PipelinesStore(); @@ -14,9 +17,6 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ store, }; }, - components: { - pipelinesComponent, - }, render(createElement) { return createElement('pipelines-component', { props: { diff --git a/app/assets/javascripts/pipelines/pipelines_charts.js b/app/assets/javascripts/pipelines/pipelines_charts.js index 001faf4be33..821aa7e229f 100644 --- a/app/assets/javascripts/pipelines/pipelines_charts.js +++ b/app/assets/javascripts/pipelines/pipelines_charts.js @@ -6,16 +6,16 @@ document.addEventListener('DOMContentLoaded', () => { const data = { labels: chartScope.labels, datasets: [{ - fillColor: '#7f8fa4', - strokeColor: '#7f8fa4', - pointColor: '#7f8fa4', + fillColor: '#707070', + strokeColor: '#707070', + pointColor: '#707070', pointStrokeColor: '#EEE', data: chartScope.totalValues, }, { - fillColor: '#44aa22', - strokeColor: '#44aa22', - pointColor: '#44aa22', + fillColor: '#1aaa55', + strokeColor: '#1aaa55', + pointColor: '#1aaa55', pointStrokeColor: '#fff', data: chartScope.successValues, }, diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index ffaafb3ee9e..464bfb351e7 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -6,195 +6,193 @@ // (including the explanation of quick actions), and showing a warning when // more than `x` users are referenced. // -(function () { - var lastTextareaPreviewed; - var lastTextareaHeight = null; - var markdownPreview; - var previewButtonSelector; - var writeButtonSelector; - - window.MarkdownPreview = (function () { - function MarkdownPreview() {} - - // Minimum number of users referenced before triggering a warning - MarkdownPreview.prototype.referenceThreshold = 10; - MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.'; - - MarkdownPreview.prototype.ajaxCache = {}; - - MarkdownPreview.prototype.showPreview = function ($form) { - var mdText; - var preview = $form.find('.js-md-preview'); - var url = preview.data('url'); - if (preview.hasClass('md-preview-loading')) { - return; - } - mdText = $form.find('textarea.markdown-area').val(); - if (mdText.trim().length === 0) { - preview.text(this.emptyMessage); - this.hideReferencedUsers($form); +import axios from '~/lib/utils/axios_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; + +var lastTextareaPreviewed; +var lastTextareaHeight = null; +var markdownPreview; +var previewButtonSelector; +var writeButtonSelector; + +function MarkdownPreview() {} + +// Minimum number of users referenced before triggering a warning +MarkdownPreview.prototype.referenceThreshold = 10; +MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.'; + +MarkdownPreview.prototype.ajaxCache = {}; + +MarkdownPreview.prototype.showPreview = function ($form) { + var mdText; + var preview = $form.find('.js-md-preview'); + var url = preview.data('url'); + if (preview.hasClass('md-preview-loading')) { + return; + } + mdText = $form.find('textarea.markdown-area').val(); + + if (mdText.trim().length === 0) { + preview.text(this.emptyMessage); + this.hideReferencedUsers($form); + } else { + preview.addClass('md-preview-loading').text('Loading...'); + this.fetchMarkdownPreview(mdText, url, (function (response) { + var body; + if (response.body.length > 0) { + body = response.body; } else { - preview.addClass('md-preview-loading').text('Loading...'); - this.fetchMarkdownPreview(mdText, url, (function (response) { - var body; - if (response.body.length > 0) { - body = response.body; - } else { - body = this.emptyMessage; - } - - preview.removeClass('md-preview-loading').html(body); - preview.renderGFM(); - this.renderReferencedUsers(response.references.users, $form); - - if (response.references.commands) { - this.renderReferencedCommands(response.references.commands, $form); - } - }).bind(this)); + body = this.emptyMessage; } - }; - MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { - if (!url) { - return; - } - if (text === this.ajaxCache.text) { - success(this.ajaxCache.response); - return; - } - $.ajax({ - type: 'POST', - url: url, - data: { - text: text - }, - dataType: 'json', - success: (function (response) { - this.ajaxCache = { - text: text, - response: response - }; - success(response); - }).bind(this) - }); - }; + preview.removeClass('md-preview-loading').html(body); + preview.renderGFM(); + this.renderReferencedUsers(response.references.users, $form); - MarkdownPreview.prototype.hideReferencedUsers = function ($form) { - $form.find('.referenced-users').hide(); - }; - - MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) { - var referencedUsers; - referencedUsers = $form.find('.referenced-users'); - if (referencedUsers.length) { - if (users.length >= this.referenceThreshold) { - referencedUsers.show(); - referencedUsers.find('.js-referenced-users-count').text(users.length); - } else { - referencedUsers.hide(); - } + if (response.references.commands) { + this.renderReferencedCommands(response.references.commands, $form); } + }).bind(this)); + } +}; + +MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { + if (!url) { + return; + } + if (text === this.ajaxCache.text) { + success(this.ajaxCache.response); + return; + } + axios.post(url, { + text, + }) + .then(({ data }) => { + this.ajaxCache = { + text: text, + response: data, }; - - MarkdownPreview.prototype.hideReferencedCommands = function ($form) { - $form.find('.referenced-commands').hide(); - }; - - MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) { - var referencedCommands; - referencedCommands = $form.find('.referenced-commands'); - if (commands.length > 0) { - referencedCommands.html(commands); - referencedCommands.show(); - } else { - referencedCommands.html(''); - referencedCommands.hide(); - } - }; - - return MarkdownPreview; - }()); - - markdownPreview = new window.MarkdownPreview(); - previewButtonSelector = '.js-md-preview-button'; - writeButtonSelector = '.js-md-write-button'; - lastTextareaPreviewed = null; - const markdownToolbar = $('.md-header-toolbar'); - - $.fn.setupMarkdownPreview = function () { - var $form = $(this); - $form.find('textarea.markdown-area').on('input', function () { - markdownPreview.hideReferencedUsers($form); - }); - }; - - $(document).on('markdown-preview:show', function (e, $form) { - if (!$form) { - return; - } - - lastTextareaPreviewed = $form.find('textarea.markdown-area'); - lastTextareaHeight = lastTextareaPreviewed.height(); - - // toggle tabs - $form.find(writeButtonSelector).parent().removeClass('active'); - $form.find(previewButtonSelector).parent().addClass('active'); - - // toggle content - $form.find('.md-write-holder').hide(); - $form.find('.md-preview-holder').show(); - markdownToolbar.removeClass('active'); - markdownPreview.showPreview($form); - }); - - $(document).on('markdown-preview:hide', function (e, $form) { - if (!$form) { - return; - } - lastTextareaPreviewed = null; - - if (lastTextareaHeight) { - $form.find('textarea.markdown-area').height(lastTextareaHeight); - } - - // toggle tabs - $form.find(writeButtonSelector).parent().addClass('active'); - $form.find(previewButtonSelector).parent().removeClass('active'); - - // toggle content - $form.find('.md-write-holder').show(); - $form.find('textarea.markdown-area').focus(); - $form.find('.md-preview-holder').hide(); - markdownToolbar.addClass('active'); - - markdownPreview.hideReferencedCommands($form); - }); - - $(document).on('markdown-preview:toggle', function (e, keyboardEvent) { - var $target; - $target = $(keyboardEvent.target); - if ($target.is('textarea.markdown-area')) { - $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]); - keyboardEvent.preventDefault(); - } else if (lastTextareaPreviewed) { - $target = lastTextareaPreviewed; - $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]); - keyboardEvent.preventDefault(); + success(data); + }) + .catch(() => flash(__('An error occurred while fetching markdown preview'))); +}; + +MarkdownPreview.prototype.hideReferencedUsers = function ($form) { + $form.find('.referenced-users').hide(); +}; + +MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) { + var referencedUsers; + referencedUsers = $form.find('.referenced-users'); + if (referencedUsers.length) { + if (users.length >= this.referenceThreshold) { + referencedUsers.show(); + referencedUsers.find('.js-referenced-users-count').text(users.length); + } else { + referencedUsers.hide(); } + } +}; + +MarkdownPreview.prototype.hideReferencedCommands = function ($form) { + $form.find('.referenced-commands').hide(); +}; + +MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) { + var referencedCommands; + referencedCommands = $form.find('.referenced-commands'); + if (commands.length > 0) { + referencedCommands.html(commands); + referencedCommands.show(); + } else { + referencedCommands.html(''); + referencedCommands.hide(); + } +}; + +markdownPreview = new MarkdownPreview(); + +previewButtonSelector = '.js-md-preview-button'; +writeButtonSelector = '.js-md-write-button'; +lastTextareaPreviewed = null; +const markdownToolbar = $('.md-header-toolbar'); + +$.fn.setupMarkdownPreview = function () { + var $form = $(this); + $form.find('textarea.markdown-area').on('input', function () { + markdownPreview.hideReferencedUsers($form); }); +}; + +$(document).on('markdown-preview:show', function (e, $form) { + if (!$form) { + return; + } + + lastTextareaPreviewed = $form.find('textarea.markdown-area'); + lastTextareaHeight = lastTextareaPreviewed.height(); + + // toggle tabs + $form.find(writeButtonSelector).parent().removeClass('active'); + $form.find(previewButtonSelector).parent().addClass('active'); + + // toggle content + $form.find('.md-write-holder').hide(); + $form.find('.md-preview-holder').show(); + markdownToolbar.removeClass('active'); + markdownPreview.showPreview($form); +}); + +$(document).on('markdown-preview:hide', function (e, $form) { + if (!$form) { + return; + } + lastTextareaPreviewed = null; - $(document).on('click', previewButtonSelector, function (e) { - var $form; - e.preventDefault(); - $form = $(this).closest('form'); - $(document).triggerHandler('markdown-preview:show', [$form]); - }); - - $(document).on('click', writeButtonSelector, function (e) { - var $form; - e.preventDefault(); - $form = $(this).closest('form'); - $(document).triggerHandler('markdown-preview:hide', [$form]); - }); -}()); + if (lastTextareaHeight) { + $form.find('textarea.markdown-area').height(lastTextareaHeight); + } + + // toggle tabs + $form.find(writeButtonSelector).parent().addClass('active'); + $form.find(previewButtonSelector).parent().removeClass('active'); + + // toggle content + $form.find('.md-write-holder').show(); + $form.find('textarea.markdown-area').focus(); + $form.find('.md-preview-holder').hide(); + markdownToolbar.addClass('active'); + + markdownPreview.hideReferencedCommands($form); +}); + +$(document).on('markdown-preview:toggle', function (e, keyboardEvent) { + var $target; + $target = $(keyboardEvent.target); + if ($target.is('textarea.markdown-area')) { + $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]); + keyboardEvent.preventDefault(); + } else if (lastTextareaPreviewed) { + $target = lastTextareaPreviewed; + $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]); + keyboardEvent.preventDefault(); + } +}); + +$(document).on('click', previewButtonSelector, function (e) { + var $form; + e.preventDefault(); + $form = $(this).closest('form'); + $(document).triggerHandler('markdown-preview:show', [$form]); +}); + +$(document).on('click', writeButtonSelector, function (e) { + var $form; + e.preventDefault(); + $form = $(this).closest('form'); + $(document).triggerHandler('markdown-preview:hide', [$form]); +}); + +export default MarkdownPreview; diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index 78be6b6e884..1ffe482d782 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -1,9 +1,12 @@ <script> - import modal from '../../../vue_shared/components/modal.vue'; - import { __, s__, sprintf } from '../../../locale'; - import csrf from '../../../lib/utils/csrf'; + import modal from '~/vue_shared/components/modal.vue'; + import { __, s__, sprintf } from '~/locale'; + import csrf from '~/lib/utils/csrf'; export default { + components: { + modal, + }, props: { actionUrl: { type: String, @@ -22,12 +25,8 @@ return { enteredPassword: '', enteredUsername: '', - isOpen: false, }; }, - components: { - modal, - }, computed: { csrfToken() { return csrf.token; @@ -69,78 +68,68 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), return this.enteredUsername === this.username; }, - onSubmit(status) { - if (status) { - if (!this.canSubmit()) { - return; - } - - this.$refs.form.submit(); - } - - this.toggleOpen(false); - }, - toggleOpen(isOpen) { - this.isOpen = isOpen; + onSubmit() { + this.$refs.form.submit(); }, }, }; </script> <template> - <div> - <modal - v-if="isOpen" - :title="s__('Profiles|Delete your account?')" - :text="text" - :kind="`danger ${!canSubmit() && 'disabled'}`" - :primary-button-label="s__('Profiles|Delete account')" - @toggle="toggleOpen" - @submit="onSubmit"> - - <template slot="body" slot-scope="props"> - <p v-html="props.text"></p> + <modal + id="delete-account-modal" + :title="s__('Profiles|Delete your account?')" + :text="text" + kind="danger" + :primary-button-label="s__('Profiles|Delete account')" + @submit="onSubmit" + :submit-disabled="!canSubmit()"> - <form - ref="form" - :action="actionUrl" - method="post"> + <template + slot="body" + slot-scope="props"> + <p v-html="props.text"></p> - <input - type="hidden" - name="_method" - value="delete" /> - <input - type="hidden" - name="authenticity_token" - :value="csrfToken" /> + <form + ref="form" + :action="actionUrl" + method="post"> - <p id="input-label" v-html="inputLabel"></p> + <input + type="hidden" + name="_method" + value="delete" + /> + <input + type="hidden" + name="authenticity_token" + :value="csrfToken" + /> - <input - v-if="confirmWithPassword" - name="password" - class="form-control" - type="password" - v-model="enteredPassword" - aria-labelledby="input-label" /> - <input - v-else - name="username" - class="form-control" - type="text" - v-model="enteredUsername" - aria-labelledby="input-label" /> - </form> - </template> + <p + id="input-label" + v-html="inputLabel" + > + </p> - </modal> + <input + v-if="confirmWithPassword" + name="password" + class="form-control" + type="password" + v-model="enteredPassword" + aria-labelledby="input-label" + /> + <input + v-else + name="username" + class="form-control" + type="text" + v-model="enteredUsername" + aria-labelledby="input-label" + /> + </form> + </template> - <button - type="button" - class="btn btn-danger" - @click="toggleOpen(true)"> - {{ s__('Profiles|Delete account') }} - </button> - </div> + </modal> </template> diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js index 635056e0eeb..a93bc935dd0 100644 --- a/app/assets/javascripts/profile/account/index.js +++ b/app/assets/javascripts/profile/account/index.js @@ -1,7 +1,12 @@ import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; + import deleteAccountModal from './components/delete_account_modal.vue'; +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({ @@ -9,6 +14,9 @@ new Vue({ components: { deleteAccountModal, }, + mounted() { + deleteAccountButton.classList.remove('disabled'); + }, render(createElement) { return createElement('delete-account-modal', { props: { diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index 0dc02f012e4..ba4ac850346 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -1,4 +1,5 @@ /* 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'; @@ -7,6 +8,8 @@ import { getPagePath } from '../lib/utils/common_utils'; 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(); } @@ -25,6 +28,7 @@ import { getPagePath } from '../lib/utils/common_utils'; 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); @@ -82,6 +86,23 @@ import { getPagePath } from '../lib/utils/common_utils'; } }); } + + setNewRepoCookie() { + if (this.value === 'off') { + Cookies.remove('new_repo'); + } else { + Cookies.set('new_repo', true, { expires_in: 365 }); + } + } + + 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); + } + } } $(function() { diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 0da32b4a3cc..586d188350f 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -1,6 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, prefer-template, no-unused-vars, no-return-assign */ import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import axios from '~/lib/utils/axios_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; // highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> ) const highlighter = function(element, text, matches) { @@ -72,19 +75,14 @@ export default class ProjectFindFile { // files pathes load load(url) { - return $.ajax({ - url: url, - method: "get", - dataType: "json", - success: (function(_this) { - return function(data) { - _this.element.find(".loading").hide(); - _this.filePaths = data; - _this.findFile(); - return _this.element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus(); - }; - })(this) - }); + axios.get(url) + .then(({ data }) => { + this.element.find('.loading').hide(); + this.filePaths = data; + this.findFile(); + this.element.find('.files-slider tr.tree-item').eq(0).addClass('selected').focus(); + }) + .catch(() => flash(__('An error occurred while loading filenames'))); } // render result diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js index b65521b278f..64b7dd540f9 100644 --- a/app/assets/javascripts/project_label_subscription.js +++ b/app/assets/javascripts/project_label_subscription.js @@ -1,3 +1,7 @@ +import { __ } from './locale'; +import axios from './lib/utils/axios_utils'; +import flash from './flash'; + export default class ProjectLabelSubscription { constructor(container) { this.$container = $(container); @@ -17,10 +21,7 @@ export default class ProjectLabelSubscription { $btn.addClass('disabled'); $span.toggleClass('hidden'); - $.ajax({ - type: 'POST', - url, - }).done(() => { + axios.post(url).then(() => { let newStatus; let newAction; @@ -45,6 +46,6 @@ export default class ProjectLabelSubscription { return button; }); - }); + }).catch(() => flash(__('There was an error subscribing to this label.'))); } } diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue deleted file mode 100644 index 8fce4c63872..00000000000 --- a/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue +++ /dev/null @@ -1,104 +0,0 @@ -<script> -import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue'; - -export default { - props: { - name: { - type: String, - required: false, - default: '', - }, - options: { - type: Array, - required: false, - default: () => [], - }, - value: { - type: Number, - required: false, - default: 0, - }, - disabledInput: { - type: Boolean, - required: false, - default: false, - }, - }, - - components: { - projectFeatureToggle, - }, - - computed: { - featureEnabled() { - return this.value !== 0; - }, - - displayOptions() { - if (this.featureEnabled) { - return this.options; - } - return [ - [0, 'Enable feature to choose access level'], - ]; - }, - - displaySelectInput() { - return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2; - }, - }, - - model: { - prop: 'value', - event: 'change', - }, - - methods: { - toggleFeature(featureEnabled) { - if (featureEnabled === false || this.options.length < 1) { - this.$emit('change', 0); - } else { - const [firstOptionValue] = this.options[this.options.length - 1]; - this.$emit('change', firstOptionValue); - } - }, - - selectOption(e) { - this.$emit('change', Number(e.target.value)); - }, - }, -}; -</script> - -<template> - <div class="project-feature-controls" :data-for="name"> - <input - v-if="name" - type="hidden" - :name="name" - :value="value" - /> - <project-feature-toggle - :value="featureEnabled" - @change="toggleFeature" - :disabledInput="disabledInput" - /> - <div class="select-wrapper"> - <select - class="form-control project-repo-select select-control" - @change="selectOption" - :disabled="displaySelectInput" - > - <option - v-for="[optionValue, optionName] in displayOptions" - :key="optionValue" - :value="optionValue" - :selected="optionValue === value" - > - {{optionName}} - </option> - </select> - <i aria-hidden="true" class="fa fa-chevron-down"></i> - </div> - </div> -</template> diff --git a/app/assets/javascripts/projects/permissions/components/project_setting_row.vue b/app/assets/javascripts/projects/permissions/components/project_setting_row.vue deleted file mode 100644 index 6140d74fea8..00000000000 --- a/app/assets/javascripts/projects/permissions/components/project_setting_row.vue +++ /dev/null @@ -1,36 +0,0 @@ -<script> -export default { - props: { - label: { - type: String, - required: false, - default: null, - }, - helpPath: { - type: String, - required: false, - default: null, - }, - helpText: { - type: String, - required: false, - default: null, - }, - }, -}; -</script> - -<template> - <div class="project-feature-row"> - <label v-if="label" class="label-light"> - {{label}} - <a v-if="helpPath" :href="helpPath" target="_blank"> - <i aria-hidden="true" data-hidden="true" class="fa fa-question-circle"></i> - </a> - </label> - <span v-if="helpText" class="help-block"> - {{helpText}} - </span> - <slot /> - </div> -</template> diff --git a/app/assets/javascripts/projects/permissions/components/settings_panel.vue b/app/assets/javascripts/projects/permissions/components/settings_panel.vue deleted file mode 100644 index 639429baf26..00000000000 --- a/app/assets/javascripts/projects/permissions/components/settings_panel.vue +++ /dev/null @@ -1,312 +0,0 @@ -<script> -import projectFeatureSetting from './project_feature_setting.vue'; -import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue'; -import projectSettingRow from './project_setting_row.vue'; -import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; -import { toggleHiddenClassBySelector } from '../external'; - -export default { - props: { - currentSettings: { - type: Object, - required: true, - }, - canChangeVisibilityLevel: { - type: Boolean, - required: false, - default: false, - }, - allowedVisibilityOptions: { - type: Array, - required: false, - default: () => [0, 10, 20], - }, - lfsAvailable: { - type: Boolean, - required: false, - default: false, - }, - registryAvailable: { - type: Boolean, - required: false, - default: false, - }, - visibilityHelpPath: { - type: String, - required: false, - }, - lfsHelpPath: { - type: String, - required: false, - }, - registryHelpPath: { - type: String, - required: false, - }, - }, - - data() { - const defaults = { - visibilityOptions, - visibilityLevel: visibilityOptions.PUBLIC, - issuesAccessLevel: 20, - repositoryAccessLevel: 20, - mergeRequestsAccessLevel: 20, - buildsAccessLevel: 20, - wikiAccessLevel: 20, - snippetsAccessLevel: 20, - containerRegistryEnabled: true, - lfsEnabled: true, - requestAccessEnabled: true, - highlightChangesClass: false, - }; - - return { ...defaults, ...this.currentSettings }; - }, - - components: { - projectFeatureSetting, - projectFeatureToggle, - projectSettingRow, - }, - - computed: { - featureAccessLevelOptions() { - const options = [ - [10, 'Only Project Members'], - ]; - if (this.visibilityLevel !== visibilityOptions.PRIVATE) { - options.push([20, 'Everyone With Access']); - } - return options; - }, - - repoFeatureAccessLevelOptions() { - return this.featureAccessLevelOptions.filter( - ([value]) => value <= this.repositoryAccessLevel, - ); - }, - - repositoryEnabled() { - return this.repositoryAccessLevel > 0; - }, - - visibilityLevelDescription() { - return visibilityLevelDescriptions[this.visibilityLevel]; - }, - }, - - methods: { - highlightChanges() { - this.highlightChangesClass = true; - this.$nextTick(() => { - this.highlightChangesClass = false; - }); - }, - - visibilityAllowed(option) { - return this.allowedVisibilityOptions.includes(option); - }, - }, - - watch: { - visibilityLevel(value, oldValue) { - if (value === visibilityOptions.PRIVATE) { - // when private, features are restricted to "only team members" - this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel); - this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel); - this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel); - this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); - this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); - this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); - this.highlightChanges(); - } else if (oldValue === visibilityOptions.PRIVATE) { - // if changing away from private, make enabled features more permissive - if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20; - if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20; - if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20; - if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; - if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; - if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; - this.highlightChanges(); - } - }, - - repositoryAccessLevel(value, oldValue) { - if (value < oldValue) { - // sub-features cannot have more premissive access level - this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value); - this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value); - - if (value === 0) { - this.containerRegistryEnabled = false; - this.lfsEnabled = false; - } - } else if (oldValue === 0) { - this.mergeRequestsAccessLevel = value; - this.buildsAccessLevel = value; - this.containerRegistryEnabled = true; - this.lfsEnabled = true; - } - }, - - issuesAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.issues-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false); - }, - - mergeRequestsAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false); - }, - - buildsAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.builds-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false); - }, - }, -}; - -</script> - -<template> - <div> - <div class="project-visibility-setting"> - <project-setting-row - label="Project visibility" - :help-path="visibilityHelpPath" - > - <div class="project-feature-controls"> - <div class="select-wrapper"> - <select - name="project[visibility_level]" - v-model="visibilityLevel" - class="form-control select-control" - :disabled="!canChangeVisibilityLevel" - > - <option - :value="visibilityOptions.PRIVATE" - :disabled="!visibilityAllowed(visibilityOptions.PRIVATE)" - > - Private - </option> - <option - :value="visibilityOptions.INTERNAL" - :disabled="!visibilityAllowed(visibilityOptions.INTERNAL)" - > - Internal - </option> - <option - :value="visibilityOptions.PUBLIC" - :disabled="!visibilityAllowed(visibilityOptions.PUBLIC)" - > - Public - </option> - </select> - <i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i> - </div> - </div> - <span class="help-block">{{ visibilityLevelDescription }}</span> - <label v-if="visibilityLevel !== visibilityOptions.PUBLIC" class="request-access"> - <input - type="hidden" - name="project[request_access_enabled]" - :value="requestAccessEnabled" - /> - <input type="checkbox" v-model="requestAccessEnabled" /> - Allow users to request access - </label> - </project-setting-row> - </div> - <div class="project-feature-settings" :class="{ 'highlight-changes': highlightChangesClass }"> - <project-setting-row - label="Issues" - help-text="Lightweight issue tracking system for this project" - > - <project-feature-setting - name="project[project_feature_attributes][issues_access_level]" - :options="featureAccessLevelOptions" - v-model="issuesAccessLevel" - /> - </project-setting-row> - <project-setting-row - label="Repository" - help-text="View and edit files in this project" - > - <project-feature-setting - name="project[project_feature_attributes][repository_access_level]" - :options="featureAccessLevelOptions" - v-model="repositoryAccessLevel" - /> - </project-setting-row> - <div class="project-feature-setting-group"> - <project-setting-row - label="Merge requests" - help-text="Submit changes to be merged upstream" - > - <project-feature-setting - name="project[project_feature_attributes][merge_requests_access_level]" - :options="repoFeatureAccessLevelOptions" - v-model="mergeRequestsAccessLevel" - :disabledInput="!repositoryEnabled" - /> - </project-setting-row> - <project-setting-row - label="Pipelines" - help-text="Build, test, and deploy your changes" - > - <project-feature-setting - name="project[project_feature_attributes][builds_access_level]" - :options="repoFeatureAccessLevelOptions" - v-model="buildsAccessLevel" - :disabledInput="!repositoryEnabled" - /> - </project-setting-row> - <project-setting-row - v-if="registryAvailable" - label="Container registry" - :help-path="registryHelpPath" - help-text="Every project can have its own space to store its Docker images" - > - <project-feature-toggle - name="project[container_registry_enabled]" - v-model="containerRegistryEnabled" - :disabledInput="!repositoryEnabled" - /> - </project-setting-row> - <project-setting-row - v-if="lfsAvailable" - label="Git Large File Storage" - :help-path="lfsHelpPath" - help-text="Manages large files such as audio, video, and graphics files" - > - <project-feature-toggle - name="project[lfs_enabled]" - v-model="lfsEnabled" - :disabledInput="!repositoryEnabled" - /> - </project-setting-row> - </div> - <project-setting-row - label="Wiki" - help-text="Pages for project documentation" - > - <project-feature-setting - name="project[project_feature_attributes][wiki_access_level]" - :options="featureAccessLevelOptions" - v-model="wikiAccessLevel" - /> - </project-setting-row> - <project-setting-row - label="Snippets" - help-text="Share code pastes with others out of Git repository" - > - <project-feature-setting - name="project[project_feature_attributes][snippets_access_level]" - :options="featureAccessLevelOptions" - v-model="snippetsAccessLevel" - /> - </project-setting-row> - </div> - </div> -</template> diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 3ecc0c2a6e5..f5133111d04 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -1,6 +1,7 @@ let hasUserDefinedProjectPath = false; -const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => { +const deriveProjectPathFromUrl = ($projectImportUrl) => { + const $currentProjectPath = $projectImportUrl.parents('.toggle-import-form').find('#project_path'); if (hasUserDefinedProjectPath) { return; } @@ -21,7 +22,7 @@ const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => { // extract everything after the last slash const pathMatch = /\/([^/]+)$/.exec(importUrl); if (pathMatch) { - $projectPath.val(pathMatch[1]); + $currentProjectPath.val(pathMatch[1]); } }; @@ -96,11 +97,9 @@ const bindEvents = () => { hasUserDefinedProjectPath = $projectPath.val().trim().length > 0; }); - $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl, $projectPath)); + $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl)); }; -document.addEventListener('DOMContentLoaded', bindEvents); - export default { bindEvents, deriveProjectPathFromUrl, diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue new file mode 100644 index 00000000000..63f20a0041d --- /dev/null +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -0,0 +1,120 @@ +<script> + import Visibility from 'visibilityjs'; + import ciIcon from '~/vue_shared/components/ci_icon.vue'; + import loadingIcon from '~/vue_shared/components/loading_icon.vue'; + import Poll from '~/lib/utils/poll'; + import Flash from '~/flash'; + import { s__, sprintf } from '~/locale'; + import tooltip from '~/vue_shared/directives/tooltip'; + import CommitPipelineService from '../services/commit_pipeline_service'; + + export default { + directives: { + tooltip, + }, + components: { + ciIcon, + loadingIcon, + }, + props: { + endpoint: { + type: String, + required: true, + }, + /* This prop can be used to replace some of the `render_commit_status` + used across GitLab, this way we could use this vue component and add a + realtime status where it makes sense + realtime: { + type: Boolean, + required: false, + default: true, + }, */ + }, + data() { + return { + ciStatus: {}, + isLoading: true, + }; + }, + computed: { + statusTitle() { + return sprintf(s__('Commits|Commit: %{commitText}'), { commitText: this.ciStatus.text }); + }, + }, + mounted() { + this.service = new CommitPipelineService(this.endpoint); + this.initPolling(); + }, + methods: { + successCallback(res) { + const pipelines = res.data.pipelines; + if (pipelines.length > 0) { + // The pipeline entity always keeps the latest pipeline info on the `details.status` + this.ciStatus = pipelines[0].details.status; + } + this.isLoading = false; + }, + errorCallback() { + this.ciStatus = { + text: 'not found', + icon: 'status_notfound', + group: 'notfound', + }; + this.isLoading = false; + Flash(s__('Something went wrong on our end')); + }, + initPolling() { + this.poll = new Poll({ + resource: this.service, + method: 'fetchData', + successCallback: response => this.successCallback(response), + errorCallback: this.errorCallback, + }); + + if (!Visibility.hidden()) { + this.isLoading = true; + this.poll.makeRequest(); + } else { + this.fetchPipelineCommitData(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); + }, + fetchPipelineCommitData() { + this.service.fetchData() + .then(this.successCallback) + .catch(this.errorCallback); + }, + }, + destroy() { + this.poll.stop(); + }, + }; +</script> +<template> + <div> + <loading-icon + label="Loading pipeline status" + size="3" + v-if="isLoading" + /> + <a + v-else + :href="ciStatus.details_path" + > + <ci-icon + v-tooltip + :title="statusTitle" + :aria-label="statusTitle" + data-container="body" + :status="ciStatus" + /> + </a> + </div> +</template> diff --git a/app/assets/javascripts/projects/tree/services/commit_pipeline_service.js b/app/assets/javascripts/projects/tree/services/commit_pipeline_service.js new file mode 100644 index 00000000000..4b4189bc2de --- /dev/null +++ b/app/assets/javascripts/projects/tree/services/commit_pipeline_service.js @@ -0,0 +1,11 @@ +import axios from '~/lib/utils/axios_utils'; + +export default class CommitPipelineService { + constructor(endpoint) { + this.endpoint = endpoint; + } + + fetchData() { + return axios.get(this.endpoint); + } +} diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue index 7606605be32..34a60dd574b 100644 --- a/app/assets/javascripts/projects_dropdown/components/app.vue +++ b/app/assets/javascripts/projects_dropdown/components/app.vue @@ -47,6 +47,22 @@ export default { return this.store.getSearchedProjects(); }, }, + created() { + if (this.currentProject.id) { + this.logCurrentProjectAccess(); + } + + eventHub.$on('dropdownOpen', this.fetchFrequentProjects); + eventHub.$on('searchProjects', this.fetchSearchedProjects); + eventHub.$on('searchCleared', this.handleSearchClear); + eventHub.$on('searchFailed', this.handleSearchFailure); + }, + beforeDestroy() { + eventHub.$off('dropdownOpen', this.fetchFrequentProjects); + eventHub.$off('searchProjects', this.fetchSearchedProjects); + eventHub.$off('searchCleared', this.handleSearchClear); + eventHub.$off('searchFailed', this.handleSearchFailure); + }, methods: { toggleFrequentProjectsList(state) { this.isLoadingProjects = !state; @@ -108,22 +124,6 @@ export default { this.toggleSearchProjectsList(true); }, }, - created() { - if (this.currentProject.id) { - this.logCurrentProjectAccess(); - } - - eventHub.$on('dropdownOpen', this.fetchFrequentProjects); - eventHub.$on('searchProjects', this.fetchSearchedProjects); - eventHub.$on('searchCleared', this.handleSearchClear); - eventHub.$on('searchFailed', this.handleSearchFailure); - }, - beforeDestroy() { - eventHub.$off('dropdownOpen', this.fetchFrequentProjects); - eventHub.$off('searchProjects', this.fetchSearchedProjects); - eventHub.$off('searchCleared', this.handleSearchClear); - eventHub.$off('searchFailed', this.handleSearchFailure); - }, }; </script> diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue index 093554cd0bc..246dbeaaded 100644 --- a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue @@ -1,32 +1,32 @@ <script> -import { s__ } from '../../locale'; -import projectsListItem from './projects_list_item.vue'; + import { s__ } from '../../locale'; + import projectsListItem from './projects_list_item.vue'; -export default { - components: { - projectsListItem, - }, - props: { - projects: { - type: Array, - required: true, + export default { + components: { + projectsListItem, }, - localStorageFailed: { - type: Boolean, - required: true, + props: { + projects: { + type: Array, + required: true, + }, + localStorageFailed: { + type: Boolean, + required: true, + }, }, - }, - computed: { - isListEmpty() { - return this.projects.length === 0; + computed: { + isListEmpty() { + return this.projects.length === 0; + }, + listEmptyMessage() { + return this.localStorageFailed ? + s__('ProjectsDropdown|This feature requires browser localStorage support') : + s__('ProjectsDropdown|Projects you visit often will appear here'); + }, }, - listEmptyMessage() { - return this.localStorageFailed ? - s__('ProjectsDropdown|This feature requires browser localStorage support') : - s__('ProjectsDropdown|Projects you visit often will appear here'); - }, - }, -}; + }; </script> <template> @@ -40,7 +40,7 @@ export default { class="section-empty" v-if="isListEmpty" > - {{listEmptyMessage}} + {{ listEmptyMessage }} </li> <projects-list-item v-else diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue index d482a7025de..759cdd1ded9 100644 --- a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue @@ -1,76 +1,77 @@ <script> -import identicon from '../../vue_shared/components/identicon.vue'; + /* eslint-disable vue/require-default-prop, vue/require-prop-types */ + import identicon from '../../vue_shared/components/identicon.vue'; -export default { - components: { - identicon, - }, - props: { - matcher: { - type: String, - required: false, + export default { + components: { + identicon, }, - projectId: { - type: Number, - required: true, - }, - projectName: { - type: String, - required: true, - }, - namespace: { - type: String, - required: true, - }, - webUrl: { - type: String, - required: true, - }, - avatarUrl: { - required: true, - validator(value) { - return value === null || typeof value === 'string'; + props: { + matcher: { + type: String, + required: false, + }, + projectId: { + type: Number, + required: true, + }, + projectName: { + type: String, + required: true, + }, + namespace: { + type: String, + required: true, + }, + webUrl: { + type: String, + required: true, + }, + avatarUrl: { + required: true, + validator(value) { + return value === null || typeof value === 'string'; + }, }, }, - }, - computed: { - hasAvatar() { - return this.avatarUrl !== null; - }, - highlightedProjectName() { - if (this.matcher) { - const matcherRegEx = new RegExp(this.matcher, 'gi'); - const matches = this.projectName.match(matcherRegEx); + computed: { + hasAvatar() { + return this.avatarUrl !== null; + }, + highlightedProjectName() { + if (this.matcher) { + const matcherRegEx = new RegExp(this.matcher, 'gi'); + const matches = this.projectName.match(matcherRegEx); - if (matches && matches.length > 0) { - return this.projectName.replace(matches[0], `<b>${matches[0]}</b>`); + if (matches && matches.length > 0) { + return this.projectName.replace(matches[0], `<b>${matches[0]}</b>`); + } } - } - return this.projectName; - }, - /** - * Smartly truncates project namespace by doing two things; - * 1. Only include Group names in path by removing project name - * 2. Only include first and last group names in the path - * when namespace has more than 2 groups present - * - * First part (removal of project name from namespace) can be - * done from backend but doing so involves migration of - * existing project namespaces which is not wise thing to do. - */ - truncatedNamespace() { - const namespaceArr = this.namespace.split(' / '); - namespaceArr.splice(-1, 1); - let namespace = namespaceArr.join(' / '); + return this.projectName; + }, + /** + * Smartly truncates project namespace by doing two things; + * 1. Only include Group names in path by removing project name + * 2. Only include first and last group names in the path + * when namespace has more than 2 groups present + * + * First part (removal of project name from namespace) can be + * done from backend but doing so involves migration of + * existing project namespaces which is not wise thing to do. + */ + truncatedNamespace() { + const namespaceArr = this.namespace.split(' / '); + namespaceArr.splice(-1, 1); + let namespace = namespaceArr.join(' / '); - if (namespaceArr.length > 2) { - namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`; - } + if (namespaceArr.length > 2) { + namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`; + } - return namespace; + return namespace; + }, }, - }, -}; + }; </script> <template> @@ -92,7 +93,7 @@ export default { <identicon v-else size-class="s32" - :entity-id=projectId + :entity-id="projectId" :entity-name="projectName" /> </div> @@ -108,7 +109,7 @@ export default { <div class="project-namespace" :title="namespace" - >{{truncatedNamespace}}</div> + >{{ truncatedNamespace }}</div> </div> </a> </li> diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue index 53bc76d0f2d..0c46ed184be 100644 --- a/app/assets/javascripts/projects_dropdown/components/search.vue +++ b/app/assets/javascripts/projects_dropdown/components/search.vue @@ -1,47 +1,47 @@ <script> -import _ from 'underscore'; -import eventHub from '../event_hub'; + import _ from 'underscore'; + import eventHub from '../event_hub'; -export default { - data() { - return { - searchQuery: '', - }; - }, - watch: { - searchQuery() { - this.handleInput(); + export default { + data() { + return { + searchQuery: '', + }; }, - }, - methods: { - setFocus() { - this.$refs.search.focus(); + watch: { + searchQuery() { + this.handleInput(); + }, }, - emitSearchEvents() { - if (this.searchQuery) { - eventHub.$emit('searchProjects', this.searchQuery); - } else { - eventHub.$emit('searchCleared'); - } + mounted() { + eventHub.$on('dropdownOpen', this.setFocus); }, - /** - * Callback function within _.debounce is intentionally - * kept as ES5 `function() {}` instead of ES6 `() => {}` - * as it otherwise messes up function context - * and component reference is no longer accessible via `this` - */ - // eslint-disable-next-line func-names - handleInput: _.debounce(function () { - this.emitSearchEvents(); - }, 500), - }, - mounted() { - eventHub.$on('dropdownOpen', this.setFocus); - }, - beforeDestroy() { - eventHub.$off('dropdownOpen', this.setFocus); - }, -}; + beforeDestroy() { + eventHub.$off('dropdownOpen', this.setFocus); + }, + methods: { + setFocus() { + this.$refs.search.focus(); + }, + emitSearchEvents() { + if (this.searchQuery) { + eventHub.$emit('searchProjects', this.searchQuery); + } else { + eventHub.$emit('searchCleared'); + } + }, + /** + * Callback function within _.debounce is intentionally + * kept as ES5 `function() {}` instead of ES6 `() => {}` + * as it otherwise messes up function context + * and component reference is no longer accessible via `this` + */ + // eslint-disable-next-line func-names + handleInput: _.debounce(function () { + this.emitSearchEvents(); + }, 500), + }, + }; </script> <template> @@ -59,6 +59,7 @@ export default { v-if="!searchQuery" class="search-icon fa fa-fw fa-search" aria-hidden="true" - /> + > + </i> </div> </template> diff --git a/app/assets/javascripts/projects_dropdown/index.js b/app/assets/javascripts/projects_dropdown/index.js index 2660da3c558..e78ebce2923 100644 --- a/app/assets/javascripts/projects_dropdown/index.js +++ b/app/assets/javascripts/projects_dropdown/index.js @@ -19,11 +19,8 @@ document.addEventListener('DOMContentLoaded', () => { return; } - $(navEl).on('show.bs.dropdown', (e) => { - const dropdownEl = $(e.currentTarget).find('.projects-dropdown-menu'); - dropdownEl.one('transitionend', () => { - eventHub.$emit('dropdownOpen'); - }); + $(navEl).on('shown.bs.dropdown', () => { + eventHub.$emit('dropdownOpen'); }); // eslint-disable-next-line no-new diff --git a/app/assets/javascripts/projects_dropdown/service/projects_service.js b/app/assets/javascripts/projects_dropdown/service/projects_service.js index 9cbd8f21f2a..7231f520933 100644 --- a/app/assets/javascripts/projects_dropdown/service/projects_service.js +++ b/app/assets/javascripts/projects_dropdown/service/projects_service.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import VueResource from 'vue-resource'; diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js index 55c93923cc8..59ad5b45855 100644 --- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -1,3 +1,4 @@ +import axios from '../lib/utils/axios_utils'; import PANEL_STATE from './constants'; import { backOff } from '../lib/utils/common_utils'; @@ -81,24 +82,20 @@ export default class PrometheusMetrics { loadActiveMetrics() { this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING); backOff((next, stop) => { - $.ajax({ - url: this.activeMetricsEndpoint, - dataType: 'json', - global: false, - }) - .done((res) => { - if (res && res.success) { - stop(res); + axios.get(this.activeMetricsEndpoint) + .then(({ data }) => { + if (data && data.success) { + stop(data); } else { this.backOffRequestCounter = this.backOffRequestCounter += 1; if (this.backOffRequestCounter < 3) { next(); } else { - stop(res); + stop(data); } } }) - .fail(stop); + .catch(stop); }) .then((res) => { if (res && res.data && res.data.length) { diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js index 0a9fdb074e5..2948baeab11 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -1,6 +1,6 @@ import _ from 'underscore'; import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; -import ProtectedBranchDropdown from './protected_branch_dropdown'; +import CreateItemDropdown from '../create_item_dropdown'; import AccessorUtilities from '../lib/utils/accessor'; const PB_LOCAL_STORAGE_KEY = 'protected-branches-defaults'; @@ -35,10 +35,12 @@ export default class ProtectedBranchCreate { onSelect: this.onSelectCallback, }); - // Protected branch dropdown - this.protectedBranchDropdown = new ProtectedBranchDropdown({ + this.createItemDropdown = new CreateItemDropdown({ $dropdown: $protectedBranchDropdown, + defaultToggleLabel: 'Protected Branch', + fieldName: 'protected_branch[name]', onSelect: this.onSelectCallback, + getData: ProtectedBranchCreate.getProtectedBranches, }); this.loadPreviousSelection($allowedToMergeDropdown.data('glDropdown'), $allowedToPushDropdown.data('glDropdown')); @@ -60,6 +62,10 @@ export default class ProtectedBranchCreate { this.$form.find('input[type="submit"]').attr('disabled', completedForm); } + static getProtectedBranches(term, callback) { + callback(gon.open_branches); + } + loadPreviousSelection(mergeDropdown, pushDropdown) { let mergeIndex = 0; let pushIndex = 0; diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js deleted file mode 100644 index 678882a8d2c..00000000000 --- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js +++ /dev/null @@ -1,90 +0,0 @@ -import _ from 'underscore'; - -export default class ProtectedBranchDropdown { - /** - * @param {Object} options containing - * `$dropdown` target element - * `onSelect` event callback - * $dropdown must be an element created using `dropdown_branch()` rails helper - */ - constructor(options) { - this.onSelect = options.onSelect; - this.$dropdown = options.$dropdown; - this.$dropdownContainer = this.$dropdown.parent(); - this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); - this.$protectedBranch = this.$dropdownContainer.find('.js-create-new-protected-branch'); - - this.buildDropdown(); - this.bindEvents(); - - // Hide footer - this.toggleFooter(true); - } - - buildDropdown() { - this.$dropdown.glDropdown({ - data: this.getProtectedBranches.bind(this), - filterable: true, - remote: false, - search: { - fields: ['title'], - }, - selectable: true, - toggleLabel(selected) { - return (selected && 'id' in selected) ? selected.title : 'Protected Branch'; - }, - fieldName: 'protected_branch[name]', - text(protectedBranch) { - return _.escape(protectedBranch.title); - }, - id(protectedBranch) { - return _.escape(protectedBranch.id); - }, - onFilter: this.toggleCreateNewButton.bind(this), - clicked: (options) => { - options.e.preventDefault(); - this.onSelect(); - }, - }); - } - - bindEvents() { - this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this)); - } - - onClickCreateWildcard(e) { - e.preventDefault(); - - // Refresh the dropdown's data, which ends up calling `getProtectedBranches` - this.$dropdown.data('glDropdown').remote.execute(); - this.$dropdown.data('glDropdown').selectRowAtIndex(); - } - - getProtectedBranches(term, callback) { - if (this.selectedBranch) { - callback(gon.open_branches.concat(this.selectedBranch)); - } else { - callback(gon.open_branches); - } - } - - toggleCreateNewButton(branchName) { - if (branchName) { - this.selectedBranch = { - title: branchName, - id: branchName, - text: branchName, - }; - - this.$dropdownContainer - .find('.js-create-new-protected-branch code') - .text(branchName); - } - - this.toggleFooter(!branchName); - } - - toggleFooter(toggleState) { - this.$dropdownFooter.toggleClass('hidden', toggleState); - } -} diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js index 632625da8e7..b51b3e9a6ff 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -1,5 +1,5 @@ -/* eslint-disable no-new */ -import Flash from '../flash'; +import flash from '../flash'; +import axios from '../lib/utils/axios_utils'; import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; export default class ProtectedBranchEdit { @@ -38,29 +38,25 @@ export default class ProtectedBranchEdit { this.$allowedToMergeDropdown.disable(); this.$allowedToPushDropdown.disable(); - $.ajax({ - type: 'POST', - url: this.$wrap.data('url'), - dataType: 'json', - data: { - _method: 'PATCH', - protected_branch: { - merge_access_levels_attributes: [{ - id: this.$allowedToMergeDropdown.data('access-level-id'), - access_level: $allowedToMergeInput.val(), - }], - push_access_levels_attributes: [{ - id: this.$allowedToPushDropdown.data('access-level-id'), - access_level: $allowedToPushInput.val(), - }], - }, + axios.patch(this.$wrap.data('url'), { + protected_branch: { + merge_access_levels_attributes: [{ + id: this.$allowedToMergeDropdown.data('access-level-id'), + access_level: $allowedToMergeInput.val(), + }], + push_access_levels_attributes: [{ + id: this.$allowedToPushDropdown.data('access-level-id'), + access_level: $allowedToPushInput.val(), + }], }, - error() { - new Flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list')); - }, - }).always(() => { + }).then(() => { + this.$allowedToMergeDropdown.enable(); + this.$allowedToPushDropdown.enable(); + }).catch(() => { this.$allowedToMergeDropdown.enable(); this.$allowedToPushDropdown.enable(); + + flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list')); }); } } diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js index 91bd140bd12..d1e4a75c17b 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_create.js +++ b/app/assets/javascripts/protected_tags/protected_tag_create.js @@ -1,5 +1,5 @@ import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; -import ProtectedTagDropdown from './protected_tag_dropdown'; +import CreateItemDropdown from '../create_item_dropdown'; export default class ProtectedTagCreate { constructor() { @@ -24,9 +24,12 @@ export default class ProtectedTagCreate { $allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0); // Protected tag dropdown - this.protectedTagDropdown = new ProtectedTagDropdown({ + this.createItemDropdown = new CreateItemDropdown({ $dropdown: this.$form.find('.js-protected-tag-select'), + defaultToggleLabel: 'Protected Tag', + fieldName: 'protected_tag[name]', onSelect: this.onSelectCallback, + getData: ProtectedTagCreate.getProtectedTags, }); } @@ -38,4 +41,8 @@ export default class ProtectedTagCreate { this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToCreateInput.length)); } + + static getProtectedTags(term, callback) { + callback(gon.open_tags); + } } diff --git a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js deleted file mode 100644 index a0224213aa0..00000000000 --- a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js +++ /dev/null @@ -1,88 +0,0 @@ -import _ from 'underscore'; - -export default class ProtectedTagDropdown { - /** - * @param {Object} options containing - * `$dropdown` target element - * `onSelect` event callback - * $dropdown must be an element created using `dropdown_tag()` rails helper - */ - constructor(options) { - this.onSelect = options.onSelect; - this.$dropdown = options.$dropdown; - this.$dropdownContainer = this.$dropdown.parent(); - this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); - this.$protectedTag = this.$dropdownContainer.find('.js-create-new-protected-tag'); - - this.buildDropdown(); - this.bindEvents(); - - // Hide footer - this.toggleFooter(true); - } - - buildDropdown() { - this.$dropdown.glDropdown({ - data: this.getProtectedTags.bind(this), - filterable: true, - remote: false, - search: { - fields: ['title'], - }, - selectable: true, - toggleLabel(selected) { - return (selected && 'id' in selected) ? selected.title : 'Protected Tag'; - }, - fieldName: 'protected_tag[name]', - text(protectedTag) { - return _.escape(protectedTag.title); - }, - id(protectedTag) { - return _.escape(protectedTag.id); - }, - onFilter: this.toggleCreateNewButton.bind(this), - clicked: (options) => { - options.e.preventDefault(); - this.onSelect(); - }, - }); - } - - bindEvents() { - this.$protectedTag.on('click', this.onClickCreateWildcard.bind(this)); - } - - onClickCreateWildcard(e) { - this.$dropdown.data('glDropdown').remote.execute(); - this.$dropdown.data('glDropdown').selectRowAtIndex(); - e.preventDefault(); - } - - getProtectedTags(term, callback) { - if (this.selectedTag) { - callback(gon.open_tags.concat(this.selectedTag)); - } else { - callback(gon.open_tags); - } - } - - toggleCreateNewButton(tagName) { - if (tagName) { - this.selectedTag = { - title: tagName, - id: tagName, - text: tagName, - }; - - this.$dropdownContainer - .find('.js-create-new-protected-tag code') - .text(tagName); - } - - this.toggleFooter(!tagName); - } - - toggleFooter(toggleState) { - this.$dropdownFooter.toggleClass('hidden', toggleState); - } -} diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js index dad0ad25b65..21a258cf93c 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js @@ -1,5 +1,5 @@ -/* eslint-disable no-new */ -import Flash from '../flash'; +import flash from '../flash'; +import axios from '../lib/utils/axios_utils'; import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; export default class ProtectedTagEdit { @@ -28,24 +28,19 @@ export default class ProtectedTagEdit { this.$allowedToCreateDropdownButton.disable(); - $.ajax({ - type: 'POST', - url: this.$wrap.data('url'), - dataType: 'json', - data: { - _method: 'PATCH', - protected_tag: { - create_access_levels_attributes: [{ - id: this.$allowedToCreateDropdownButton.data('access-level-id'), - access_level: $allowedToCreateInput.val(), - }], - }, + axios.patch(this.$wrap.data('url'), { + protected_tag: { + create_access_levels_attributes: [{ + id: this.$allowedToCreateDropdownButton.data('access-level-id'), + access_level: $allowedToCreateInput.val(), + }], }, - error() { - new Flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list')); - }, - }).always(() => { + }).then(() => { + this.$allowedToCreateDropdownButton.enable(); + }).catch(() => { this.$allowedToCreateDropdownButton.enable(); + + flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list')); }); } } diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index 2d8ca443ea7..ea0f7199a70 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -1,14 +1,17 @@ <script> - /* globals Flash */ import { mapGetters, mapActions } from 'vuex'; - import '../../flash'; + import Flash from '../../flash'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import store from '../stores'; import collapsibleContainer from './collapsible_container.vue'; import { errorMessages, errorMessagesTypes } from '../constants'; export default { - name: 'registryListApp', + name: 'RegistryListApp', + components: { + collapsibleContainer, + loadingIcon, + }, props: { endpoint: { type: String, @@ -16,22 +19,12 @@ }, }, store, - components: { - collapsibleContainer, - loadingIcon, - }, computed: { ...mapGetters([ 'isLoading', 'repos', ]), }, - methods: { - ...mapActions([ - 'setMainEndpoint', - 'fetchRepos', - ]), - }, created() { this.setMainEndpoint(this.endpoint); }, @@ -39,6 +32,12 @@ this.fetchRepos() .catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS])); }, + methods: { + ...mapActions([ + 'setMainEndpoint', + 'fetchRepos', + ]), + }, }; </script> <template> @@ -46,17 +45,18 @@ <loading-icon v-if="isLoading" size="3" - /> + /> <collapsible-container v-else-if="!isLoading && repos.length" v-for="(item, index) in repos" :key="index" :repo="item" - /> + /> <p v-else-if="!isLoading && !repos.length"> - {{__("No container images stored for this project. Add one by following the instructions above.")}} + {{ __(`No container images stored for this project. +Add one by following the instructions above.`) }} </p> </div> </template> diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index ac1c3ec253c..b4906ba4ee5 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -1,7 +1,6 @@ <script> - /* globals Flash */ import { mapActions } from 'vuex'; - import '../../flash'; + import Flash from '../../flash'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -9,13 +8,7 @@ import { errorMessages, errorMessagesTypes } from '../constants'; export default { - name: 'collapsibeContainerRegisty', - props: { - repo: { - type: Object, - required: true, - }, - }, + name: 'CollapsibeContainerRegisty', components: { clipboardButton, loadingIcon, @@ -24,6 +17,12 @@ directives: { tooltip, }, + props: { + repo: { + type: Object, + required: true, + }, + }, data() { return { isOpen: false, @@ -65,28 +64,29 @@ <template> <div class="container-image"> - <div - class="container-image-head"> + <div class="container-image-head"> <button type="button" @click="toggleRepo" - class="js-toggle-repo btn-link"> + class="js-toggle-repo btn-link" + > <i class="fa" :class="{ 'fa-chevron-right': !isOpen, 'fa-chevron-up': isOpen, }" - aria-hidden="true"> + aria-hidden="true" + > </i> - {{repo.name}} + {{ repo.name }} </button> <clipboard-button v-if="repo.location" :text="clipboardText" :title="repo.location" - /> + /> <div class="controls hidden-xs pull-right"> <button @@ -96,35 +96,38 @@ :title="s__('ContainerRegistry|Remove repository')" :aria-label="s__('ContainerRegistry|Remove repository')" v-tooltip - @click="handleDeleteRepository"> + @click="handleDeleteRepository" + > <i class="fa fa-trash" - aria-hidden="true"> + aria-hidden="true" + > </i> </button> </div> - </div> <loading-icon v-if="repo.isLoading" class="append-bottom-20" size="2" - /> + /> <div v-else-if="!repo.isLoading && isOpen" - class="container-image-tags"> + class="container-image-tags" + > <table-registry v-if="repo.list.length" :repo="repo" - /> + /> <div v-else - class="nothing-here-block"> - {{s__("ContainerRegistry|No tags in Container Registry for this container image.")}} + class="nothing-here-block" + > + {{ s__("ContainerRegistry|No tags in Container Registry for this container image.") }} </div> </div> </div> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index 14d43e135fe..bef850eddc0 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -1,8 +1,7 @@ <script> - /* globals Flash */ import { mapActions } from 'vuex'; import { n__ } from '../../locale'; - import '../../flash'; + import Flash from '../../flash'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -11,21 +10,21 @@ import { numberToHumanSize } from '../../lib/utils/number_utils'; export default { - props: { - repo: { - type: Object, - required: true, - }, - }, components: { clipboardButton, tablePagination, }, + directives: { + tooltip, + }, mixins: [ timeagoMixin, ], - directives: { - tooltip, + props: { + repo: { + type: Object, + required: true, + }, }, computed: { shouldRenderPagination() { @@ -68,75 +67,78 @@ }; </script> <template> -<div> - <table class="table tags"> - <thead> - <tr> - <th>{{s__('ContainerRegistry|Tag')}}</th> - <th>{{s__('ContainerRegistry|Tag ID')}}</th> - <th>{{s__("ContainerRegistry|Size")}}</th> - <th>{{s__("ContainerRegistry|Created")}}</th> - <th></th> - </tr> - </thead> - <tbody> - <tr - v-for="(item, i) in repo.list" - :key="i"> - <td> + <div> + <table class="table tags"> + <thead> + <tr> + <th>{{ s__('ContainerRegistry|Tag') }}</th> + <th>{{ s__('ContainerRegistry|Tag ID') }}</th> + <th>{{ s__("ContainerRegistry|Size") }}</th> + <th>{{ s__("ContainerRegistry|Created") }}</th> + <th></th> + </tr> + </thead> + <tbody> + <tr + v-for="(item, i) in repo.list" + :key="i"> + <td> - {{item.tag}} + {{ item.tag }} - <clipboard-button - v-if="item.location" - :title="item.location" - :text="clipboardText(item.location)" + <clipboard-button + v-if="item.location" + :title="item.location" + :text="clipboardText(item.location)" /> - </td> - <td> - <span - v-tooltip - :title="item.revision" - data-placement="bottom"> - {{item.shortRevision}} + </td> + <td> + <span + v-tooltip + :title="item.revision" + data-placement="bottom" + > + {{ item.shortRevision }} </span> - </td> - <td> - {{formatSize(item.size)}} - <template v-if="item.size && item.layers"> - · - </template> - {{layers(item)}} - </td> + </td> + <td> + {{ formatSize(item.size) }} + <template v-if="item.size && item.layers"> + · + </template> + {{ layers(item) }} + </td> - <td> - {{timeFormated(item.createdAt)}} - </td> + <td> + {{ timeFormated(item.createdAt) }} + </td> - <td class="content"> - <button - v-if="item.canDelete" - type="button" - class="js-delete-registry btn btn-danger hidden-xs pull-right" - :title="s__('ContainerRegistry|Remove tag')" - :aria-label="s__('ContainerRegistry|Remove tag')" - data-container="body" - v-tooltip - @click="handleDeleteRegistry(item)"> - <i - class="fa fa-trash" - aria-hidden="true"> - </i> - </button> - </td> - </tr> - </tbody> - </table> + <td class="content"> + <button + v-if="item.canDelete" + type="button" + class="js-delete-registry btn btn-danger hidden-xs pull-right" + :title="s__('ContainerRegistry|Remove tag')" + :aria-label="s__('ContainerRegistry|Remove tag')" + data-container="body" + v-tooltip + @click="handleDeleteRegistry(item)" + > + <i + class="fa fa-trash" + aria-hidden="true" + > + </i> + </button> + </td> + </tr> + </tbody> + </table> - <table-pagination - v-if="shouldRenderPagination" - :change="onPageChange" - :page-info="repo.pagination" + <table-pagination + v-if="shouldRenderPagination" + :change="onPageChange" + :page-info="repo.pagination" /> -</div> + </div> </template> diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js index 7518ed69cdf..ed7f013ec75 100644 --- a/app/assets/javascripts/render_math.js +++ b/app/assets/javascripts/render_math.js @@ -1,4 +1,5 @@ -import Flash from './flash'; +import { __ } from './locale'; +import flash from './flash'; // Renders math using KaTeX in any element with the // `js-render-math` class @@ -16,7 +17,7 @@ function renderWithKaTeX(elements, katex) { const display = $this.attr('data-math-style') === 'display'; try { - katex.render($this.text(), mathNode.get(0), { displayMode: display }); + katex.render($this.text(), mathNode.get(0), { displayMode: display, throwOnError: false }); mathNode.insertAfter($this); $this.remove(); } catch (err) { @@ -33,9 +34,7 @@ export default function renderMath($els) { renderWithKaTeX($els, katex); }) .catch((err) => { - Flash(`Can't load katex css ${err}`); + flash(`Can't load katex css ${err}`); }); - }).catch((err) => { - Flash(`Can't load katex module: ${err}`); - }); + }).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 41942c04a4e..31c7a772cf4 100644 --- a/app/assets/javascripts/render_mermaid.js +++ b/app/assets/javascripts/render_mermaid.js @@ -19,12 +19,34 @@ export default function renderMermaid($els) { import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid').then((mermaid) => { mermaid.initialize({ - loadOnStart: false, + // mermaid core options + mermaid: { + startOnLoad: false, + }, + // mermaidAPI options theme: 'neutral', }); $els.each((i, el) => { - mermaid.init(undefined, el); + const source = el.textContent; + + mermaid.init(undefined, el, (id) => { + const svg = document.getElementById(id); + + svg.classList.add('mermaid'); + + // pre > code > svg + svg.closest('pre').replaceWith(svg); + + // We need to add the original source into the DOM to allow Copy-as-GFM + // to access it. + const sourceEl = document.createElement('text'); + sourceEl.classList.add('source'); + sourceEl.setAttribute('display', 'none'); + sourceEl.textContent = source; + + svg.appendChild(sourceEl); + }); }); }).catch((err) => { Flash(`Can't load mermaid module: ${err}`); diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index b830fcf7e80..01c3be5411f 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -2,6 +2,8 @@ import _ from 'underscore'; import Cookies from 'js-cookie'; +import flash from './flash'; +import axios from './lib/utils/axios_utils'; function Sidebar(currentUser) { this.toggleTodo = this.toggleTodo.bind(this); @@ -62,7 +64,7 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { Sidebar.prototype.toggleTodo = function(e) { var $btnText, $this, $todoLoading, ajaxType, url; $this = $(e.currentTarget); - ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST'; + ajaxType = $this.attr('data-delete-path') ? 'delete' : 'post'; if ($this.attr('data-delete-path')) { url = "" + ($this.attr('data-delete-path')); } else { @@ -71,25 +73,14 @@ Sidebar.prototype.toggleTodo = function(e) { $this.tooltip('hide'); - return $.ajax({ - url: url, - type: ajaxType, - dataType: 'json', - data: { - issuable_id: $this.data('issuable-id'), - issuable_type: $this.data('issuable-type') - }, - beforeSend: (function(_this) { - return function() { - $('.js-issuable-todo').disable() - .addClass('is-loading'); - }; - })(this) - }).done((function(_this) { - return function(data) { - return _this.todoUpdateDone(data); - }; - })(this)); + $('.js-issuable-todo').disable().addClass('is-loading'); + + axios[ajaxType](url, { + issuable_id: $this.data('issuable-id'), + issuable_type: $this.data('issuable-type'), + }).then(({ data }) => { + this.todoUpdateDone(data); + }).catch(() => flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`)); }; Sidebar.prototype.todoUpdateDone = function(data) { diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js new file mode 100644 index 00000000000..db466f722c4 --- /dev/null +++ b/app/assets/javascripts/shared/milestones/form.js @@ -0,0 +1,9 @@ +import ZenMode from '../../zen_mode'; +import DueDateSelectors from '../../due_date_select'; +import GLForm from '../../gl_form'; + +export default (initGFM = true) => { + new ZenMode(); // eslint-disable-line no-new + new DueDateSelectors(); // eslint-disable-line no-new + new GLForm($('.milestone-form'), initGFM); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/shared/sessions/u2f.js b/app/assets/javascripts/shared/sessions/u2f.js new file mode 100644 index 00000000000..1d075f7e872 --- /dev/null +++ b/app/assets/javascripts/shared/sessions/u2f.js @@ -0,0 +1,16 @@ +import U2FAuthenticate from '../../u2f/authenticate'; + +export default () => { + if (!gon.u2f) return; + + const u2fAuthenticate = new U2FAuthenticate( + $('#js-authenticate-u2f'), + '#js-login-u2f-form', + gon.u2f, + document.querySelector('#js-login-2fa-device'), + document.querySelector('.js-2fa-form'), + ); + u2fAuthenticate.start(); + // needed in rspec + gl.u2fAuthenticate = u2fAuthenticate; +}; diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index d2f0d7410da..c5dddd001bb 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,5 +1,6 @@ import Cookies from 'js-cookie'; import Mousetrap from 'mousetrap'; +import axios from './lib/utils/axios_utils'; import { refreshCurrentPage, visitUrl } from './lib/utils/url_utility'; import findAndFollowLink from './shortcuts_dashboard_navigation'; @@ -13,12 +14,10 @@ Mousetrap.stopCallback = (e, element, combo) => { }; export default class Shortcuts { - constructor(skipResetBindings) { + constructor() { this.onToggleHelp = this.onToggleHelp.bind(this); this.enabledHelp = []; - if (!skipResetBindings) { - Mousetrap.reset(); - } + Mousetrap.bind('?', this.onToggleHelp); Mousetrap.bind('s', Shortcuts.focusSearch); Mousetrap.bind('f', this.focusFilter.bind(this)); @@ -62,7 +61,7 @@ export default class Shortcuts { e.preventDefault(); const performanceBarCookieName = 'perf_bar_enabled'; if (Cookies.get(performanceBarCookieName) === 'true') { - Cookies.remove(performanceBarCookieName, { path: '/' }); + Cookies.set(performanceBarCookieName, 'false', { path: '/' }); } else { Cookies.set(performanceBarCookieName, 'true', { path: '/' }); } @@ -87,21 +86,21 @@ export default class Shortcuts { $modal.modal('toggle'); } - $.ajax({ - url: gon.shortcuts_path, - dataType: 'script', - success() { - if (location && location.length > 0) { - const results = []; - for (let i = 0, len = location.length; i < len; i += 1) { - results.push($(location[i]).show()); - } - return results; + return axios.get(gon.shortcuts_path, { + responseType: 'text', + }).then(({ data }) => { + $.globalEval(data); + + if (location && location.length > 0) { + const results = []; + for (let i = 0, len = location.length; i < len; i += 1) { + results.push($(location[i]).show()); } + return results; + } - $('.hidden-shortcut').show(); - return $('.js-more-help-button').remove(); - }, + $('.hidden-shortcut').show(); + return $('.js-more-help-button').remove(); }); } diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js index cf309be4f6f..908b9cab93d 100644 --- a/app/assets/javascripts/shortcuts_blob.js +++ b/app/assets/javascripts/shortcuts_blob.js @@ -1,4 +1,4 @@ -/* global Mousetrap */ +import Mousetrap from 'mousetrap'; import { getLocationHash, visitUrl } from './lib/utils/url_utility'; import Shortcuts from './shortcuts'; diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index 81286c0010c..1e246a56b85 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -1,5 +1,4 @@ -/* global Mousetrap */ - +import Mousetrap from 'mousetrap'; import ShortcutsNavigation from './shortcuts_navigation'; export default class ShortcutsFindFile extends ShortcutsNavigation { diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 292e3d6a657..689befc742e 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,12 +1,10 @@ -/* global Mousetrap */ - +import Mousetrap from 'mousetrap'; import _ from 'underscore'; -import 'mousetrap'; import Sidebar from './right_sidebar'; -import ShortcutsNavigation from './shortcuts_navigation'; +import Shortcuts from './shortcuts'; import { CopyAsGFM } from './behaviors/copy_as_gfm'; -export default class ShortcutsIssuable extends ShortcutsNavigation { +export default class ShortcutsIssuable extends Shortcuts { constructor(isMergeRequest) { super(); diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index b4562701a3e..a4d10850471 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -1,5 +1,4 @@ -/* global Mousetrap */ - +import Mousetrap from 'mousetrap'; import findAndFollowLink from './shortcuts_dashboard_navigation'; import Shortcuts from './shortcuts'; diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js index 21823085ac4..a88c280fa3b 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/shortcuts_network.js @@ -1,4 +1,4 @@ -/* global Mousetrap */ +import Mousetrap from 'mousetrap'; import ShortcutsNavigation from './shortcuts_navigation'; export default class ShortcutsNetwork extends ShortcutsNavigation { diff --git a/app/assets/javascripts/shortcuts_wiki.js b/app/assets/javascripts/shortcuts_wiki.js index 59b967dbe09..41865dcf4ba 100644 --- a/app/assets/javascripts/shortcuts_wiki.js +++ b/app/assets/javascripts/shortcuts_wiki.js @@ -1,16 +1,14 @@ -/* eslint-disable class-methods-use-this */ -/* global Mousetrap */ - +import Mousetrap from 'mousetrap'; import ShortcutsNavigation from './shortcuts_navigation'; import findAndFollowLink from './shortcuts_dashboard_navigation'; export default class ShortcutsWiki extends ShortcutsNavigation { constructor() { super(); - Mousetrap.bind('e', this.editWiki); + Mousetrap.bind('e', ShortcutsWiki.editWiki); } - editWiki() { + static editWiki() { findAndFollowLink('.js-wiki-edit'); } } diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js index 77f070d48cc..129ba2e4e89 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js @@ -39,7 +39,7 @@ export default { class="js-sidebar-dropdown-toggle edit-link pull-right" href="#" > - Edit + {{ __('Edit') }} </a> <a v-if="showToggle" diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.js b/app/assets/javascripts/sidebar/components/assignees/assignees.js index 7e5feac622c..643877b9d47 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.js +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.js @@ -84,7 +84,7 @@ export default { return !this.showLess || (index < this.defaultRenderCount && this.showLess); }, avatarUrl(user) { - return user.avatar || user.avatar_url; + return user.avatar || user.avatar_url || gon.default_avatar_url; }, assigneeUrl(user) { return `${this.rootPath}${user.username}`; 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 6ee4d487c0b..02153fb86a5 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -1,48 +1,51 @@ <script> -import Flash from '../../../flash'; -import editForm from './edit_form.vue'; -import Icon from '../../../vue_shared/components/icon.vue'; + import Flash from '../../../flash'; + import editForm from './edit_form.vue'; + import Icon from '../../../vue_shared/components/icon.vue'; -export default { - components: { - editForm, - Icon, - }, - props: { - isConfidential: { - required: true, - type: Boolean, + export default { + components: { + editForm, + Icon, }, - isEditable: { - required: true, - type: Boolean, + props: { + isConfidential: { + required: true, + type: Boolean, + }, + isEditable: { + required: true, + type: Boolean, + }, + service: { + required: true, + type: Object, + }, }, - service: { - required: true, - type: Object, + data() { + return { + edit: false, + }; }, - }, - data() { - return { - edit: false, - }; - }, - computed: { - confidentialityIcon() { - return this.isConfidential ? 'eye-slash' : 'eye'; + computed: { + confidentialityIcon() { + return this.isConfidential ? 'eye-slash' : 'eye'; + }, }, - }, - methods: { - toggleForm() { - this.edit = !this.edit; + methods: { + toggleForm() { + this.edit = !this.edit; + }, + updateConfidentialAttribute(confidential) { + this.service.update('issue', { confidential }) + .then(() => location.reload()) + .catch(() => { + Flash(`Something went wrong trying to + change the confidentiality of this issue`); + }); + }, }, - updateConfidentialAttribute(confidential) { - this.service.update('issue', { confidential }) - .then(() => location.reload()) - .catch(() => new Flash('Something went wrong trying to change the confidentiality of this issue')); - }, - }, -}; + }; </script> <template> @@ -51,8 +54,8 @@ export default { <icon :name="confidentialityIcon" :size="16" - aria-hidden="true"> - </icon> + aria-hidden="true" + /> </div> <div class="title hide-collapsed"> Confidentiality @@ -62,7 +65,7 @@ export default { href="#" @click.prevent="toggleForm" > - Edit + {{ __('Edit') }} </a> </div> <div class="value sidebar-item-value hide-collapsed"> @@ -72,22 +75,26 @@ export default { :is-confidential="isConfidential" :update-confidential-attribute="updateConfidentialAttribute" /> - <div v-if="!isConfidential" class="no-value sidebar-item-value"> + <div + v-if="!isConfidential" + class="no-value sidebar-item-value"> <icon name="eye" :size="16" aria-hidden="true" - class="sidebar-item-icon inline"> - </icon> + class="sidebar-item-icon inline" + /> Not confidential </div> - <div v-else class="value sidebar-item-value hide-collapsed"> + <div + v-else + class="value sidebar-item-value hide-collapsed"> <icon name="eye-slash" :size="16" aria-hidden="true" - class="sidebar-item-icon inline is-active"> - </icon> + class="sidebar-item-icon inline is-active" + /> This issue is confidential </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 dd17b5abd46..6a81235a1a7 100644 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue @@ -1,26 +1,25 @@ <script> -import editFormButtons from './edit_form_buttons.vue'; + import editFormButtons from './edit_form_buttons.vue'; -export default { - props: { - isConfidential: { - required: true, - type: Boolean, + export default { + components: { + editFormButtons, }, - toggleForm: { - required: true, - type: Function, + props: { + isConfidential: { + required: true, + type: Boolean, + }, + toggleForm: { + required: true, + type: Function, + }, + updateConfidentialAttribute: { + required: true, + type: Function, + }, }, - updateConfidentialAttribute: { - required: true, - type: Function, - }, - }, - - components: { - editFormButtons, - }, -}; + }; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue index 242e826d471..e7a87636aa7 100644 --- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue @@ -1,45 +1,47 @@ <script> -import editFormButtons from './edit_form_buttons.vue'; -import issuableMixin from '../../../vue_shared/mixins/issuable'; + import editFormButtons from './edit_form_buttons.vue'; + import issuableMixin from '../../../vue_shared/mixins/issuable'; -export default { - props: { - isLocked: { - required: true, - type: Boolean, + export default { + components: { + editFormButtons, }, - - toggleForm: { - required: true, - type: Function, - }, - - updateLockedAttribute: { - required: true, - type: Function, + mixins: [ + issuableMixin, + ], + props: { + isLocked: { + required: true, + type: Boolean, + }, + + toggleForm: { + required: true, + type: Function, + }, + + updateLockedAttribute: { + required: true, + type: Function, + }, }, - }, - - mixins: [ - issuableMixin, - ], - - components: { - editFormButtons, - }, -}; + }; </script> <template> <div class="dropdown open"> <div class="dropdown-menu sidebar-item-warning-message"> - <p class="text" v-if="isLocked"> + <p + class="text" + v-if="isLocked"> Unlock this {{ issuableDisplayName }}? <strong>Everyone</strong> will be able to comment. </p> - <p class="text" v-else> + <p + class="text" + v-else> Lock this {{ issuableDisplayName }}? Only <strong>project members</strong> 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 04c3a96bf74..02876a6c175 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -1,63 +1,63 @@ <script> -/* global Flash */ -import editForm from './edit_form.vue'; -import issuableMixin from '../../../vue_shared/mixins/issuable'; -import Icon from '../../../vue_shared/components/icon.vue'; + 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'; -export default { - props: { - isLocked: { - required: true, - type: Boolean, + export default { + components: { + editForm, + Icon, }, + mixins: [ + issuableMixin, + ], - isEditable: { - required: true, - type: Boolean, - }, - - mediator: { - required: true, - type: Object, - validator(mediatorObject) { - return mediatorObject.service && mediatorObject.service.update && mediatorObject.store; + props: { + isLocked: { + required: true, + type: Boolean, }, - }, - }, - - mixins: [ - issuableMixin, - ], - components: { - editForm, - Icon, - }, + isEditable: { + required: true, + type: Boolean, + }, - computed: { - lockIcon() { - return this.isLocked ? 'lock' : 'lock-open'; + mediator: { + required: true, + type: Object, + validator(mediatorObject) { + return mediatorObject.service && mediatorObject.service.update && mediatorObject.store; + }, + }, }, - isLockDialogOpen() { - return this.mediator.store.isLockDialogOpen; - }, - }, + computed: { + lockIcon() { + return this.isLocked ? 'lock' : 'lock-open'; + }, - methods: { - toggleForm() { - this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; + isLockDialogOpen() { + return this.mediator.store.isLockDialogOpen; + }, }, - updateLockedAttribute(locked) { - this.mediator.service.update(this.issuableType, { - discussion_locked: locked, - }) - .then(() => location.reload()) - .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName}`))); + methods: { + toggleForm() { + this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; + }, + + updateLockedAttribute(locked) { + this.mediator.service.update(this.issuableType, { + discussion_locked: locked, + }) + .then(() => location.reload()) + .catch(() => Flash(this.__(`Something went wrong trying to + change the locked state of this ${this.issuableDisplayName}`))); + }, }, - }, -}; + }; </script> <template> @@ -67,8 +67,8 @@ export default { :name="lockIcon" :size="16" aria-hidden="true" - class="sidebar-item-icon is-active"> - </icon> + class="sidebar-item-icon is-active" + /> </div> <div class="title hide-collapsed"> @@ -100,8 +100,8 @@ export default { name="lock" :size="16" aria-hidden="true" - class="sidebar-item-icon inline is-active"> - </icon> + class="sidebar-item-icon inline is-active" + /> {{ __('Locked') }} </div> @@ -113,8 +113,8 @@ export default { name="lock-open" :size="16" aria-hidden="true" - class="sidebar-item-icon inline"> - </icon> + class="sidebar-item-icon inline" + /> {{ __('Unlocked') }} </div> </div> diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index b8510a6ce3a..006a6d2905d 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -1,73 +1,73 @@ <script> -import { __, n__, sprintf } from '../../../locale'; -import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; -import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue'; + import { __, n__, sprintf } from '../../../locale'; + import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; + import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue'; -export default { - props: { - loading: { - type: Boolean, - required: false, - default: false, + export default { + components: { + loadingIcon, + userAvatarImage, }, - participants: { - type: Array, - required: false, - default: () => [], + props: { + loading: { + type: Boolean, + required: false, + default: false, + }, + participants: { + type: Array, + required: false, + default: () => [], + }, + numberOfLessParticipants: { + type: Number, + required: false, + default: 7, + }, }, - numberOfLessParticipants: { - type: Number, - required: false, - default: 7, + data() { + return { + isShowingMoreParticipants: false, + }; }, - }, - data() { - return { - isShowingMoreParticipants: false, - }; - }, - components: { - loadingIcon, - userAvatarImage, - }, - computed: { - lessParticipants() { - return this.participants.slice(0, this.numberOfLessParticipants); - }, - visibleParticipants() { - return this.isShowingMoreParticipants ? this.participants : this.lessParticipants; - }, - hasMoreParticipants() { - return this.participants.length > this.numberOfLessParticipants; - }, - toggleLabel() { - let label = ''; - if (this.isShowingMoreParticipants) { - label = __('- show less'); - } else { - label = sprintf(__('+ %{moreCount} more'), { - moreCount: this.participants.length - this.numberOfLessParticipants, - }); - } + computed: { + lessParticipants() { + return this.participants.slice(0, this.numberOfLessParticipants); + }, + visibleParticipants() { + return this.isShowingMoreParticipants ? this.participants : this.lessParticipants; + }, + hasMoreParticipants() { + return this.participants.length > this.numberOfLessParticipants; + }, + toggleLabel() { + let label = ''; + if (this.isShowingMoreParticipants) { + label = __('- show less'); + } else { + label = sprintf(__('+ %{moreCount} more'), { + moreCount: this.participants.length - this.numberOfLessParticipants, + }); + } - return label; - }, - participantLabel() { - return sprintf( - n__('%{count} participant', '%{count} participants', this.participants.length), - { count: this.loading ? '' : this.participantCount }, - ); - }, - participantCount() { - return this.participants.length; + return label; + }, + participantLabel() { + return sprintf( + n__('%{count} participant', '%{count} participants', this.participants.length), + { count: this.loading ? '' : this.participantCount }, + ); + }, + participantCount() { + return this.participants.length; + }, }, - }, - methods: { - toggleMoreParticipants() { - this.isShowingMoreParticipants = !this.isShowingMoreParticipants; + methods: { + toggleMoreParticipants() { + this.isShowingMoreParticipants = !this.isShowingMoreParticipants; + }, }, - }, -}; + }; </script> <template> @@ -75,14 +75,17 @@ export default { <div class="sidebar-collapsed-icon"> <i class="fa fa-users" - aria-hidden="true"> + aria-hidden="true" + > </i> <loading-icon v-if="loading" - class="js-participants-collapsed-loading-icon" /> + class="js-participants-collapsed-loading-icon" + /> <span v-else - class="js-participants-collapsed-count"> + class="js-participants-collapsed-count" + > {{ participantCount }} </span> </div> @@ -90,34 +93,40 @@ export default { <loading-icon v-if="loading" :inline="true" - class="js-participants-expanded-loading-icon" /> + class="js-participants-expanded-loading-icon" + /> {{ participantLabel }} </div> <div class="participants-list hide-collapsed"> <div v-for="participant in visibleParticipants" :key="participant.id" - class="participants-author js-participants-author"> + class="participants-author js-participants-author" + > <a class="author_link" - :href="participant.web_url"> + :href="participant.web_url" + > <user-avatar-image :lazy="true" :img-src="participant.avatar_url" css-classes="avatar-inline" :size="24" :tooltip-text="participant.name" - tooltip-placement="bottom" /> + tooltip-placement="bottom" + /> </a> </div> </div> <div v-if="hasMoreParticipants" - class="participants-more hide-collapsed"> + class="participants-more hide-collapsed" + > <button type="button" class="btn-transparent btn-blank js-toggle-participants-button" - @click="toggleMoreParticipants"> + @click="toggleMoreParticipants" + > {{ toggleLabel }} </button> </div> diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue index 6fcd2f95309..5c1ead1a8ac 100644 --- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue @@ -1,23 +1,23 @@ <script> -import Store from '../../stores/sidebar_store'; -import participants from './participants.vue'; + import Store from '../../stores/sidebar_store'; + import participants from './participants.vue'; -export default { - data() { - return { - store: new Store(), - }; - }, - props: { - mediator: { - type: Object, - required: true, + export default { + components: { + participants, }, - }, - components: { - participants, - }, -}; + props: { + mediator: { + type: Object, + required: true, + }, + }, + data() { + return { + store: new Store(), + }; + }, + }; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue index f4bae1d3dd5..3e8cc7a6630 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue @@ -6,10 +6,8 @@ import { __ } from '../../../locale'; import subscriptions from './subscriptions.vue'; export default { - data() { - return { - store: new Store(), - }; + components: { + subscriptions, }, props: { mediator: { @@ -17,10 +15,17 @@ export default { required: true, }, }, - components: { - subscriptions, + data() { + return { + store: new Store(), + }; + }, + created() { + eventHub.$on('toggleSubscription', this.onToggleSubscription); + }, + beforeDestroy() { + eventHub.$off('toggleSubscription', this.onToggleSubscription); }, - methods: { onToggleSubscription() { this.mediator.toggleSubscription() @@ -29,14 +34,6 @@ export default { }); }, }, - - created() { - eventHub.$on('toggleSubscription', this.onToggleSubscription); - }, - - beforeDestroy() { - eventHub.$off('toggleSubscription', this.onToggleSubscription); - }, }; </script> @@ -44,6 +41,7 @@ export default { <div class="block subscriptions"> <subscriptions :loading="store.isFetching.subscriptions" - :subscribed="store.subscribed" /> + :subscribed="store.subscribed" + /> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue index 940e1764f3d..d69d100a26c 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -1,64 +1,85 @@ <script> -import { __ } from '../../../locale'; -import eventHub from '../../event_hub'; -import loadingButton from '../../../vue_shared/components/loading_button.vue'; + import { __ } from '~/locale'; + import icon from '~/vue_shared/components/icon.vue'; + import toggleButton from '~/vue_shared/components/toggle_button.vue'; + import tooltip from '~/vue_shared/directives/tooltip'; + import eventHub from '../../event_hub'; -export default { - props: { - loading: { - type: Boolean, - required: false, - default: false, + const ICON_ON = 'notifications'; + const ICON_OFF = 'notifications-off'; + const LABEL_ON = __('Notifications on'); + const LABEL_OFF = __('Notifications off'); + + export default { + directives: { + tooltip, }, - subscribed: { - type: Boolean, - required: false, + components: { + icon, + toggleButton, }, - id: { - type: Number, - required: false, + props: { + loading: { + type: Boolean, + required: false, + default: false, + }, + subscribed: { + type: Boolean, + required: false, + default: null, + }, + id: { + type: Number, + required: false, + default: null, + }, }, - }, - components: { - loadingButton, - }, - computed: { - buttonLabel() { - let label; - if (this.subscribed === false) { - label = __('Subscribe'); - } else if (this.subscribed === true) { - label = __('Unsubscribe'); - } - - return label; + computed: { + showLoadingState() { + return this.subscribed === null; + }, + notificationIcon() { + return this.subscribed ? ICON_ON : ICON_OFF; + }, + notificationTooltip() { + return this.subscribed ? LABEL_ON : LABEL_OFF; + }, }, - }, - methods: { - toggleSubscription() { - eventHub.$emit('toggleSubscription', this.id); + methods: { + toggleSubscription() { + eventHub.$emit('toggleSubscription', this.id); + }, }, - }, -}; + }; </script> <template> <div> <div class="sidebar-collapsed-icon"> - <i - class="fa fa-rss" - aria-hidden="true"> - </i> + <span + v-tooltip + :title="notificationTooltip" + data-container="body" + data-placement="left" + > + <icon + :name="notificationIcon" + :size="16" + aria-hidden="true" + class="sidebar-item-icon is-active" + /> + </span> </div> <span class="issuable-header-text hide-collapsed pull-left"> {{ __('Notifications') }} </span> - <loading-button - ref="loadingButton" - class="btn btn-default pull-right hide-collapsed js-issuable-subscribe-button" - :loading="loading" - :label="buttonLabel" - @click="toggleSubscription" + <toggle-button + ref="toggleButton" + class="pull-right hide-collapsed js-issuable-subscribe-button" + :is-loading="showLoadingState" + :value="subscribed" + @change="toggleSubscription" /> </div> </template> diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 95e51bc4e7a..48dd91bdf06 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -1,5 +1,8 @@ /* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */ +import { __ } from './locale'; +import axios from './lib/utils/axios_utils'; +import createFlash from './flash'; import FilesCommentButton from './files_comment_button'; import imageDiffHelper from './image_diff/helpers/index'; import syntaxHighlight from './syntax_highlight'; @@ -60,30 +63,31 @@ export default class SingleFileDiff { getContentHTML(cb) { this.collapsedContent.hide(); this.loadingContent.show(); - $.get(this.diffForPath, (function(_this) { - return function(data) { - _this.loadingContent.hide(); + + axios.get(this.diffForPath) + .then(({ data }) => { + this.loadingContent.hide(); if (data.html) { - _this.content = $(data.html); - syntaxHighlight(_this.content); + this.content = $(data.html); + syntaxHighlight(this.content); } else { - _this.hasError = true; - _this.content = $(ERROR_HTML); + this.hasError = true; + this.content = $(ERROR_HTML); } - _this.collapsedContent.after(_this.content); + this.collapsedContent.after(this.content); if (typeof gl.diffNotesCompileComponents !== 'undefined') { gl.diffNotesCompileComponents(); } - const $file = $(_this.file); + const $file = $(this.file); FilesCommentButton.init($file); const canCreateNote = $file.closest('.files').is('[data-can-create-note]'); imageDiffHelper.initImageDiff($file[0], canCreateNote); if (cb) cb(); - }; - })(this)); + }) + .catch(createFlash(__('An error occurred while retrieving diff'))); } } diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js index dcbec40c79e..129a551cbcd 100644 --- a/app/assets/javascripts/task_list.js +++ b/app/assets/javascripts/task_list.js @@ -1,4 +1,5 @@ import 'deckar01-task_list'; +import axios from './lib/utils/axios_utils'; import Flash from './flash'; export default class TaskList { @@ -7,11 +8,11 @@ export default class TaskList { this.dataType = options.dataType; this.fieldName = options.fieldName; this.onSuccess = options.onSuccess || (() => {}); - this.onError = function showFlash(response) { + this.onError = function showFlash(e) { let errorMessages = ''; - if (response.responseJSON) { - errorMessages = response.responseJSON.errors.join(' '); + if (e.response.data && typeof e.response.data === 'object') { + errorMessages = e.response.data.errors.join(' '); } return new Flash(errorMessages || 'Update failed', 'alert'); @@ -38,12 +39,9 @@ export default class TaskList { patchData[this.dataType] = { [this.fieldName]: $target.val(), }; - return $.ajax({ - type: 'PATCH', - url: $target.data('update-url') || $('form.js-issuable-update').attr('action'), - data: patchData, - success: this.onSuccess, - error: this.onError, - }); + + return axios.patch($target.data('update-url') || $('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 8e167f5bf08..4cc1c96b870 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js +++ b/app/assets/javascripts/templates/issuable_template_selector.js @@ -32,8 +32,8 @@ export default class IssuableTemplateSelector extends TemplateSelector { this.startLoadingSpinner(); Api.issueTemplate(this.namespacePath, this.projectPath, query.name, this.issuableType, (err, currentTemplate) => { this.currentTemplate = currentTemplate; - if (err) return; // Error handled by global AJAX error handler this.stopLoadingSpinner(); + if (err) return; // Error handled by global AJAX error handler this.setInputValueToTemplateContent(); }); return; diff --git a/app/assets/javascripts/toggle_buttons.js b/app/assets/javascripts/toggle_buttons.js new file mode 100644 index 00000000000..2d680d0f0dc --- /dev/null +++ b/app/assets/javascripts/toggle_buttons.js @@ -0,0 +1,61 @@ +import $ from 'jquery'; +import Flash from './flash'; +import { __ } from './locale'; +import { convertPermissionToBoolean } from './lib/utils/common_utils'; + +/* + example HAML: + ``` + %button.js-project-feature-toggle.project-feature-toggle{ type: "button", + class: "#{'is-checked' if enabled?}", + 'aria-label': _('Toggle Cluster') } + %input{ type: "hidden", class: 'js-project-feature-toggle-input', value: enabled? } + ``` +*/ + +function updateToggle(toggle, isOn) { + toggle.classList.toggle('is-checked', isOn); +} + +function onToggleClicked(toggle, input, clickCallback) { + const previousIsOn = convertPermissionToBoolean(input.value); + + // Visually change the toggle and start loading + updateToggle(toggle, !previousIsOn); + toggle.setAttribute('disabled', true); + toggle.classList.toggle('is-loading', true); + + Promise.resolve(clickCallback(!previousIsOn, toggle)) + .then(() => { + // Actually change the input value + input.setAttribute('value', !previousIsOn); + }) + .catch(() => { + // Revert the visuals if something goes wrong + updateToggle(toggle, previousIsOn); + }) + .then(() => { + // Remove the loading indicator in any case + toggle.removeAttribute('disabled'); + toggle.classList.toggle('is-loading', false); + + $(input).trigger('trigger-change'); + }) + .catch(() => { + Flash(__('Something went wrong when toggling the button')); + }); +} + +export default function setupToggleButtons(container, clickCallback = () => {}) { + const toggles = container.querySelectorAll('.js-project-feature-toggle'); + + toggles.forEach((toggle) => { + const input = toggle.querySelector('.js-project-feature-toggle-input'); + const isOn = convertPermissionToBoolean(input.value); + + // Get the visible toggle in sync with the hidden input + updateToggle(toggle, isOn); + + toggle.addEventListener('click', onToggleClicked.bind(null, toggle, input, clickCallback)); + }); +} diff --git a/app/assets/javascripts/usage_ping.js b/app/assets/javascripts/usage_ping.js deleted file mode 100644 index 2389056bd02..00000000000 --- a/app/assets/javascripts/usage_ping.js +++ /dev/null @@ -1,12 +0,0 @@ -export default function UsagePing() { - const usageDataUrl = $('.usage-data').data('endpoint'); - - $.ajax({ - type: 'GET', - url: usageDataUrl, - dataType: 'html', - success(html) { - $('.usage-data').html(html); - }, - }); -} diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/users/activity_calendar.js index 0581239d5a5..57306322aa4 100644 --- a/app/assets/javascripts/users/activity_calendar.js +++ b/app/assets/javascripts/users/activity_calendar.js @@ -1,7 +1,10 @@ import _ from 'underscore'; import { scaleLinear, scaleThreshold } from 'd3-scale'; import { select } from 'd3-selection'; -import { getDayName, getDayDifference } from '../lib/utils/datetime_utility'; +import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility'; +import axios from '~/lib/utils/axios_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; const d3 = { select, scaleLinear, scaleThreshold }; @@ -98,7 +101,7 @@ export default class ActivityCalendar { const secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth(); if (lastColMonth !== secondLastColMonth) { - extraWidthPadding = 3; + extraWidthPadding = 6; } return extraWidthPadding; @@ -221,14 +224,16 @@ export default class ActivityCalendar { this.currentSelectedDate.getDate(), ].join('-'); - $.ajax({ - url: this.calendarActivitiesPath, - data: { date }, - cache: false, - dataType: 'html', - beforeSend: () => $('.user-calendar-activities').html(LOADING_HTML), - success: data => $('.user-calendar-activities').html(data), - }); + $('.user-calendar-activities').html(LOADING_HTML); + + axios.get(this.calendarActivitiesPath, { + params: { + date, + }, + responseType: 'text', + }) + .then(({ data }) => $('.user-calendar-activities').html(data)) + .catch(() => flash(__('An error occurred while retrieving calendar activity'))); } else { this.currentSelectedDate = ''; $('.user-calendar-activities').html(''); diff --git a/app/assets/javascripts/users/user_tabs.js b/app/assets/javascripts/users/user_tabs.js index 992baa9a1ef..e13b9839a20 100644 --- a/app/assets/javascripts/users/user_tabs.js +++ b/app/assets/javascripts/users/user_tabs.js @@ -1,6 +1,9 @@ +import axios from '../lib/utils/axios_utils'; import Activities from '../activities'; import ActivityCalendar from './activity_calendar'; import { localTimeAgo } from '../lib/utils/datetime_utility'; +import { __ } from '../locale'; +import flash from '../flash'; /** * UserTabs @@ -131,18 +134,20 @@ export default class UserTabs { } loadTab(action, endpoint) { - return $.ajax({ - beforeSend: () => this.toggleLoading(true), - complete: () => this.toggleLoading(false), - dataType: 'json', - url: endpoint, - success: (data) => { + this.toggleLoading(true); + + return axios.get(endpoint) + .then(({ data }) => { const tabSelector = `div#${action}`; this.$parentEl.find(tabSelector).html(data.html); this.loaded[action] = true; localTimeAgo($('.js-timeago', tabSelector)); - }, - }); + + this.toggleLoading(false); + }) + .catch(() => { + this.toggleLoading(false); + }); } loadActivities() { @@ -158,17 +163,15 @@ export default class UserTabs { utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${(utcOffset / 3600)}`; } - $.ajax({ - dataType: 'json', - url: calendarPath, - success: (activityData) => { + axios.get(calendarPath) + .then(({ data }) => { $calendarWrap.html(CALENDAR_TEMPLATE); $calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`); // eslint-disable-next-line no-new - new ActivityCalendar('.js-contrib-calendar', activityData, calendarActivitiesPath, utcOffset); - }, - }); + new ActivityCalendar('.js-contrib-calendar', data, calendarActivitiesPath, utcOffset); + }) + .catch(() => flash(__('There was an error loading users activity calendar.'))); // eslint-disable-next-line no-new new Activities(); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 759cc9925f4..eaed81cf79e 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -2,6 +2,7 @@ /* global Issuable */ /* global emitSidebarEvent */ import _ from 'underscore'; +import axios from './lib/utils/axios_utils'; // TODO: remove eventHub hack after code splitting refactor window.emitSidebarEvent = window.emitSidebarEvent || $.noop; @@ -177,32 +178,28 @@ function UsersSelect(currentUser, els, options = {}) { $loading.removeClass('hidden').fadeIn(); $dropdown.trigger('loading.gl.dropdown'); - return $.ajax({ - type: 'PUT', - dataType: 'json', - url: issueURL, - data: data - }).done(function(data) { - var user; - $dropdown.trigger('loaded.gl.dropdown'); - $loading.fadeOut(); - if (data.assignee) { - user = { - name: data.assignee.name, - username: data.assignee.username, - avatar: data.assignee.avatar_url - }; - } else { - user = { - name: 'Unassigned', - username: '', - avatar: '' - }; - } - $value.html(assigneeTemplate(user)); - $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle'); - return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); - }); + return axios.put(issueURL, data) + .then(({ data }) => { + var user; + $dropdown.trigger('loaded.gl.dropdown'); + $loading.fadeOut(); + if (data.assignee) { + user = { + name: data.assignee.name, + username: data.assignee.username, + avatar: data.assignee.avatar_url + }; + } else { + user = { + name: 'Unassigned', + username: '', + avatar: '' + }; + } + $value.html(assigneeTemplate(user)); + $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle'); + return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); + }); }; collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>'); assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>'); @@ -492,7 +489,7 @@ function UsersSelect(currentUser, els, options = {}) { renderRow: function(user) { var avatar, img, listClosingTags, listWithName, listWithUserName, username; username = user.username ? "@" + user.username : ""; - avatar = user.avatar_url ? user.avatar_url : false; + avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url; let selected = false; @@ -513,9 +510,7 @@ function UsersSelect(currentUser, els, options = {}) { if (user.beforeDivider != null) { `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(user.name)}</a></li>`; } else { - if (avatar) { - img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />"; - } + img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />"; } return ` @@ -541,7 +536,6 @@ function UsersSelect(currentUser, els, options = {}) { options.projectId = $(select).data('project-id'); options.groupId = $(select).data('group-id'); options.showCurrentUser = $(select).data('current-user'); - options.pushCodeToProtectedBranches = $(select).data('push-code-to-protected-branches'); options.authorId = $(select).data('author-id'); options.skipUsers = $(select).data('skip-users'); showNullUser = $(select).data('null-user'); @@ -663,39 +657,33 @@ UsersSelect.prototype.user = function(user_id, callback) { var url; url = this.buildUrl(this.userPath); url = url.replace(':id', user_id); - return $.ajax({ - url: url, - dataType: "json" - }).done(function(user) { - return callback(user); - }); + return axios.get(url) + .then(({ data }) => { + callback(data); + }); }; // Return users list. Filtered by query // Only active users retrieved UsersSelect.prototype.users = function(query, options, callback) { - var url; - url = this.buildUrl(this.usersPath); - return $.ajax({ - url: url, - data: { - search: query, - per_page: options.perPage || 20, - active: true, - project_id: options.projectId || null, - group_id: options.groupId || null, - skip_ldap: options.skipLdap || null, - todo_filter: options.todoFilter || null, - todo_state_filter: options.todoStateFilter || null, - current_user: options.showCurrentUser || null, - push_code_to_protected_branches: options.pushCodeToProtectedBranches || null, - author_id: options.authorId || null, - skip_users: options.skipUsers || null - }, - dataType: "json" - }).done(function(users) { - return callback(users); - }); + 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, + skip_ldap: options.skipLdap || null, + todo_filter: options.todoFilter || null, + todo_state_filter: options.todoStateFilter || null, + current_user: options.showCurrentUser || null, + author_id: options.authorId || null, + skip_users: options.skipUsers || null + }; + return axios.get(url, { params }) + .then(({ data }) => { + callback(data); + }); }; UsersSelect.prototype.buildUrl = function(url) { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js deleted file mode 100644 index 982b5e8e373..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js +++ /dev/null @@ -1,28 +0,0 @@ -import tooltip from '../../vue_shared/directives/tooltip'; - -export default { - name: 'MRWidgetAuthor', - props: { - author: { type: Object, required: true }, - showAuthorName: { type: Boolean, required: false, default: true }, - showAuthorTooltip: { type: Boolean, required: false, default: false }, - }, - directives: { - tooltip, - }, - template: ` - <a - :href="author.webUrl || author.web_url" - class="author-link inline" - :v-tooltip="showAuthorTooltip" - :title="author.name"> - <img - :src="author.avatarUrl || author.avatar_url" - class="avatar avatar-inline s16" /> - <span - v-if="showAuthorName" - class="author">{{author.name}} - </span> - </a> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue new file mode 100644 index 00000000000..cb6e9858736 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue @@ -0,0 +1,53 @@ +<script> + import tooltip from '../../vue_shared/directives/tooltip'; + + export default { + name: 'MRWidgetAuthor', + directives: { + tooltip, + }, + props: { + author: { + type: Object, + required: true, + }, + showAuthorName: { + type: Boolean, + required: false, + default: true, + }, + showAuthorTooltip: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + authorUrl() { + return this.author.webUrl || this.author.web_url; + }, + avatarUrl() { + return this.author.avatarUrl || this.author.avatar_url; + }, + }, + }; +</script> +<template> + <a + :href="authorUrl" + class="author-link inline" + :v-tooltip="showAuthorTooltip" + :title="author.name" + > + <img + :src="avatarUrl" + class="avatar avatar-inline s16" + /> + <span + class="author" + v-if="showAuthorName" + > + {{ author.name }} + </span> + </a> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js deleted file mode 100644 index 6d2ed5fda64..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js +++ /dev/null @@ -1,27 +0,0 @@ -import MRWidgetAuthor from './mr_widget_author'; - -export default { - name: 'MRWidgetAuthorTime', - props: { - actionText: { type: String, required: true }, - author: { type: Object, required: true }, - dateTitle: { type: String, required: true }, - dateReadable: { type: String, required: true }, - }, - components: { - 'mr-widget-author': MRWidgetAuthor, - }, - template: ` - <h4 class="js-mr-widget-author"> - {{actionText}} - <mr-widget-author :author="author" /> - <time - :title="dateTitle" - data-toggle="tooltip" - data-placement="top" - data-container="body"> - {{dateReadable}} - </time> - </h4> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue new file mode 100644 index 00000000000..8f1fd809a81 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue @@ -0,0 +1,42 @@ +<script> + import mrWidgetAuthor from './mr_widget_author.vue'; + + export default { + name: 'MRWidgetAuthorTime', + components: { + mrWidgetAuthor, + }, + props: { + actionText: { + type: String, + required: true, + }, + author: { + type: Object, + required: true, + }, + dateTitle: { + type: String, + required: true, + }, + dateReadable: { + type: String, + required: true, + }, + }, + }; +</script> +<template> + <h4 class="js-mr-widget-author"> + {{ actionText }} + <mr-widget-author :author="author" /> + <time + :title="dateTitle" + data-toggle="tooltip" + data-placement="top" + data-container="body" + > + {{ dateReadable }} + </time> + </h4> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js index ee1a45cc754..d174a900f63 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js @@ -2,7 +2,7 @@ import { getTimeago } from '~/lib/utils/datetime_utility'; import { visitUrl } from '../../lib/utils/url_utility'; import Flash from '../../flash'; import MemoryUsage from './mr_widget_memory_usage'; -import StatusIcon from './mr_widget_status_icon'; +import StatusIcon from './mr_widget_status_icon.vue'; import MRWidgetService from '../services/mr_widget_service'; export default { @@ -34,10 +34,10 @@ export default { if (isConfirmed) { MRWidgetService.stopEnvironment(deployment.stop_url) - .then(res => res.json()) - .then((res) => { - if (res.redirect_url) { - visitUrl(res.redirect_url); + .then(res => res.data) + .then((data) => { + if (data.redirect_url) { + visitUrl(data.redirect_url); } }) .catch(() => { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js deleted file mode 100644 index 13e4cb5717e..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js +++ /dev/null @@ -1,113 +0,0 @@ -import tooltip from '../../vue_shared/directives/tooltip'; -import { pluralize } from '../../lib/utils/text_utility'; - -export default { - name: 'MRWidgetHeader', - props: { - mr: { type: Object, required: true }, - }, - directives: { - tooltip, - }, - computed: { - shouldShowCommitsBehindText() { - return this.mr.divergedCommitsCount > 0; - }, - commitsText() { - return pluralize('commit', this.mr.divergedCommitsCount); - }, - branchNameClipboardData() { - // This supports code in app/assets/javascripts/copy_to_clipboard.js that - // works around ClipboardJS limitations to allow the context-specific - // copy/pasting of plain text or GFM. - return JSON.stringify({ - text: this.mr.sourceBranch, - gfm: `\`${this.mr.sourceBranch}\``, - }); - }, - }, - methods: { - isBranchTitleLong(branchTitle) { - return branchTitle.length > 32; - }, - }, - template: ` - <div class="mr-source-target"> - <div class="normal"> - <strong> - Request to merge - <span - class="label-branch" - :class="{'label-truncated': isBranchTitleLong(mr.sourceBranch)}" - :title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''" - data-placement="bottom" - :v-tooltip="isBranchTitleLong(mr.sourceBranch)" - v-html="mr.sourceBranchLink"></span> - <button - v-tooltip - class="btn btn-transparent btn-clipboard" - data-title="Copy branch name to clipboard" - :data-clipboard-text="branchNameClipboardData"> - <i - aria-hidden="true" - class="fa fa-clipboard"></i> - </button> - into - <span - class="label-branch" - :v-tooltip="isBranchTitleLong(mr.sourceBranch)" - :class="{'label-truncatedtooltip': isBranchTitleLong(mr.targetBranch)}" - :title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''" - data-placement="bottom"> - <a :href="mr.targetBranchTreePath">{{mr.targetBranch}}</a> - </span> - </strong> - <span - v-if="shouldShowCommitsBehindText" - class="diverged-commits-count"> - (<a :href="mr.targetBranchPath">{{mr.divergedCommitsCount}} {{commitsText}} behind</a>) - </span> - </div> - <div v-if="mr.isOpen"> - <a - href="#modal_merge_info" - data-toggle="modal" - class="btn btn-sm inline"> - Check out branch - </a> - <span class="dropdown prepend-left-10"> - <a - class="btn btn-sm inline dropdown-toggle" - data-toggle="dropdown" - aria-label="Download as" - role="button"> - <i - class="fa fa-download" - aria-hidden="true"> - </i> - <i - class="fa fa-caret-down" - aria-hidden="true"> - </i> - </a> - <ul class="dropdown-menu dropdown-menu-align-right"> - <li> - <a - :href="mr.emailPatchesPath" - download> - Email patches - </a> - </li> - <li> - <a - :href="mr.plainDiffPath" - download> - Plain diff - </a> - </li> - </ul> - </span> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue new file mode 100644 index 00000000000..18a3787857d --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -0,0 +1,145 @@ +<script> + import tooltip from '~/vue_shared/directives/tooltip'; + import { n__ } from '~/locale'; + import icon from '~/vue_shared/components/icon.vue'; + import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; + + export default { + name: 'MRWidgetHeader', + directives: { + tooltip, + }, + components: { + icon, + clipboardButton, + }, + props: { + mr: { + type: Object, + required: true, + }, + }, + computed: { + shouldShowCommitsBehindText() { + return this.mr.divergedCommitsCount > 0; + }, + commitsText() { + return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount); + }, + branchNameClipboardData() { + // This supports code in app/assets/javascripts/copy_to_clipboard.js that + // works around ClipboardJS limitations to allow the context-specific + // copy/pasting of plain text or GFM. + return JSON.stringify({ + text: this.mr.sourceBranch, + gfm: `\`${this.mr.sourceBranch}\``, + }); + }, + isSourceBranchLong() { + return this.isBranchTitleLong(this.mr.sourceBranch); + }, + isTargetBranchLong() { + return this.isBranchTitleLong(this.mr.targetBranch); + }, + }, + methods: { + isBranchTitleLong(branchTitle) { + return branchTitle.length > 32; + }, + }, + }; +</script> +<template> + <div class="mr-source-target"> + <div class="normal"> + <strong> + {{ s__("mrWidget|Request to merge") }} + <span + class="label-branch js-source-branch" + :class="{ 'label-truncated': isSourceBranchLong }" + :title="isSourceBranchLong ? mr.sourceBranch : ''" + data-placement="bottom" + :v-tooltip="isSourceBranchLong" + v-html="mr.sourceBranchLink" + > + </span> + + <clipboard-button + :text="branchNameClipboardData" + :title="__('Copy branch name to clipboard')" + /> + + {{ s__("mrWidget|into") }} + + <span + class="label-branch" + :v-tooltip="isTargetBranchLong" + :class="{ 'label-truncatedtooltip': isTargetBranchLong }" + :title="isTargetBranchLong ? mr.targetBranch : ''" + data-placement="bottom" + > + <a + :href="mr.targetBranchTreePath" + class="js-target-branch" + > + {{ mr.targetBranch }} + </a> + </span> + </strong> + <span + v-if="shouldShowCommitsBehindText" + class="diverged-commits-count" + > + (<a :href="mr.targetBranchPath">{{ commitsText }}</a>) + </span> + </div> + + <div v-if="mr.isOpen"> + <button + data-target="#modal_merge_info" + data-toggle="modal" + :disabled="mr.sourceBranchRemoved" + class="btn btn-sm btn-default inline js-check-out-branch" + type="button" + > + {{ s__("mrWidget|Check out branch") }} + </button> + <span class="dropdown prepend-left-10"> + <button + type="button" + class="btn btn-sm inline dropdown-toggle" + data-toggle="dropdown" + aria-label="Download as" + aria-haspopup="true" + aria-expanded="false" + > + <icon name="download" /> + <i + class="fa fa-caret-down" + aria-hidden="true"> + </i> + </button> + <ul class="dropdown-menu dropdown-menu-align-right"> + <li> + <a + class="js-download-email-patches" + :href="mr.emailPatchesPath" + download + > + {{ s__("mrWidget|Email patches") }} + </a> + </li> + <li> + <a + class="js-download-plain-diff" + :href="mr.plainDiffPath" + download + > + {{ s__("mrWidget|Plain diff") }} + </a> + </li> + </ul> + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js index a8c686e5065..69e70ba1dd6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js @@ -102,11 +102,11 @@ export default { return res; } - return res.json(); + return res.data; }) - .then((res) => { - this.computeGraphData(res.metrics, res.deployment_time); - return res; + .then((data) => { + this.computeGraphData(data.metrics, data.deployment_time); + return data; }) .catch(() => { this.loadFailed = true; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js deleted file mode 100644 index 1d9f9863dd9..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js +++ /dev/null @@ -1,23 +0,0 @@ -export default { - name: 'MRWidgetMergeHelp', - props: { - missingBranch: { type: String, required: false, default: '' }, - }, - template: ` - <section class="mr-widget-help"> - <template - v-if="missingBranch"> - If the {{missingBranch}} branch exists in your local repository, you - </template> - <template v-else> - You - </template> - can merge this merge request manually using the - <a - data-toggle="modal" - href="#modal_merge_info"> - command line - </a> - </section> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue new file mode 100644 index 00000000000..62b61e1f41f --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue @@ -0,0 +1,41 @@ +<script> + import { sprintf, s__ } from '~/locale'; + + export default { + name: 'MRWidgetMergeHelp', + props: { + missingBranch: { + type: String, + required: false, + default: '', + }, + }, + computed: { + missingBranchInfo() { + return sprintf( + s__('mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the'), + { branch: this.missingBranch }, + ); + }, + }, + }; +</script> +<template> + <section class="mr-widget-help"> + <template v-if="missingBranch"> + {{ missingBranchInfo }} + </template> + <template v-else> + {{ s__("mrWidget|You can merge this merge request manually using the") }} + </template> + + <button + type="button" + class="btn-link btn-blank js-open-modal-help" + data-toggle="modal" + data-target="#modal_merge_info" + > + {{ s__("mrWidget|command line") }} + </button> + </section> +</template> 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 dbc65462377..109a302a172 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,10 +1,16 @@ <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'; export default { name: 'MRWidgetPipeline', + components: { + pipelineStage, + ciIcon, + icon, + }, props: { pipeline: { type: Object, @@ -21,11 +27,6 @@ required: false, }, }, - components: { - pipelineStage, - ciIcon, - icon, - }, computed: { hasPipeline() { return this.pipeline && Object.keys(this.pipeline).length > 0; @@ -62,7 +63,8 @@ <template v-else-if="hasPipeline"> <a class="append-right-10" - :href="this.status.details_path"> + :href="status.details_path" + > <ci-icon :status="status" /> </a> @@ -70,33 +72,37 @@ Pipeline <a :href="pipeline.path" - class="pipeline-id"> - #{{pipeline.id}} + class="pipeline-id" + > + #{{ pipeline.id }} </a> - {{pipeline.details.status.label}} for + {{ pipeline.details.status.label }} for <a :href="pipeline.commit.commit_path" - class="commit-sha js-commit-link"> - {{pipeline.commit.short_id}}</a>. + class="commit-sha js-commit-link" + > + {{ pipeline.commit.short_id }}</a>. <span class="mr-widget-pipeline-graph"> - <span class="stage-cell"> + <span + class="stage-cell" + v-if="hasStages" + > <div - v-if="hasStages" v-for="(stage, i) in pipeline.details.stages" :key="i" - class="stage-container dropdown js-mini-pipeline-graph"> + class="stage-container dropdown js-mini-pipeline-graph" + > <pipeline-stage :stage="stage" /> </div> </span> </span> <template v-if="pipeline.coverage"> - Coverage {{pipeline.coverage}}% + Coverage {{ pipeline.coverage }}% </template> - </div> </template> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js deleted file mode 100644 index 563267ad044..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js +++ /dev/null @@ -1,37 +0,0 @@ -export default { - name: 'MRWidgetRelatedLinks', - props: { - relatedLinks: { type: Object, required: true }, - state: { type: String, required: false }, - }, - computed: { - hasLinks() { - const { closing, mentioned, assignToMe } = this.relatedLinks; - return closing || mentioned || assignToMe; - }, - closesText() { - if (this.state === 'merged') { - return 'Closed'; - } - if (this.state === 'closed') { - return 'Did not close'; - } - return 'Closes'; - }, - }, - template: ` - <section - v-if="hasLinks" - class="mr-info-list mr-links"> - <p v-if="relatedLinks.closing"> - {{closesText}} <span v-html="relatedLinks.closing"></span> - </p> - <p v-if="relatedLinks.mentioned"> - Mentions <span v-html="relatedLinks.mentioned"></span> - </p> - <p v-if="relatedLinks.assignToMe"> - <span v-html="relatedLinks.assignToMe"></span> - </p> - </section> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue new file mode 100644 index 00000000000..88d0fcd70f5 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue @@ -0,0 +1,43 @@ +<script> + import { s__ } from '~/locale'; + + export default { + name: 'MRWidgetRelatedLinks', + props: { + relatedLinks: { + type: Object, + required: true, + default: () => ({}), + }, + state: { + type: String, + required: false, + default: '', + }, + }, + computed: { + closesText() { + if (this.state === 'merged') { + return s__('mrWidget|Closed'); + } + if (this.state === 'closed') { + return s__('mrWidget|Did not close'); + } + return s__('mrWidget|Closes'); + }, + }, + }; +</script> +<template> + <section class="mr-info-list mr-links"> + <p v-if="relatedLinks.closing"> + {{ closesText }} <span v-html="relatedLinks.closing"></span> + </p> + <p v-if="relatedLinks.mentioned"> + {{ s__("mrWidget|Mentions") }} <span v-html="relatedLinks.mentioned"></span> + </p> + <p v-if="relatedLinks.assignToMe"> + <span v-html="relatedLinks.assignToMe"></span> + </p> + </section> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js deleted file mode 100644 index eeb990908f6..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js +++ /dev/null @@ -1,36 +0,0 @@ -import ciIcon from '../../vue_shared/components/ci_icon.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - -export default { - props: { - status: { type: String, required: true }, - showDisabledButton: { type: Boolean, required: false }, - }, - components: { - ciIcon, - loadingIcon, - }, - computed: { - statusObj() { - return { - group: this.status, - icon: `status_${this.status}`, - }; - }, - }, - template: ` - <div class="space-children flex-container-block append-right-10"> - <div v-if="status === 'loading'" class="mr-widget-icon"> - <loading-icon /> - </div> - <ci-icon v-else :status="statusObj" /> - <button - v-if="showDisabledButton" - type="button" - class="js-disabled-merge-button btn btn-success btn-sm" - disabled="true"> - Merge - </button> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue new file mode 100644 index 00000000000..1fdc3218671 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue @@ -0,0 +1,57 @@ +<script> + import ciIcon from '../../vue_shared/components/ci_icon.vue'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + + export default { + components: { + ciIcon, + loadingIcon, + }, + props: { + status: { + type: String, + required: true, + }, + showDisabledButton: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + isLoading() { + return this.status === 'loading'; + }, + statusObj() { + return { + group: this.status, + icon: `status_${this.status}`, + }; + }, + }, + }; +</script> +<template> + <div class="space-children flex-container-block append-right-10"> + <div + v-if="isLoading" + class="mr-widget-icon" + > + <loading-icon /> + </div> + + <ci-icon + v-else + :status="statusObj" + /> + + <button + v-if="showDisabledButton" + type="button" + class="js-disabled-merge-button btn btn-success btn-sm" + disabled="true" + > + {{ s__("mrWidget|Merge") }} + </button> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js deleted file mode 100644 index b4e4a6aa161..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js +++ /dev/null @@ -1,26 +0,0 @@ -import statusIcon from '../mr_widget_status_icon'; - -export default { - name: 'MRWidgetArchived', - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <div class="space-children"> - <status-icon status="failed" /> - <button - type="button" - class="btn btn-success btn-sm" - disabled="true"> - Merge - </button> - </div> - <div class="media-body"> - <span class="bold"> - This project is archived, write access has been disabled - </span> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue new file mode 100644 index 00000000000..cfbd44d41b2 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue @@ -0,0 +1,31 @@ +<script> + import statusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetArchived', + components: { + statusIcon, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <div class="space-children"> + <status-icon + status="warning" + /> + <button + type="button" + class="btn btn-success btn-sm" + disabled="true" + > + {{ s__("mrWidget|Merge") }} + </button> + </div> + <div class="media-body"> + <span class="bold"> + {{ s__("mrWidget|This project is archived, write access has been disabled") }} + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js deleted file mode 100644 index 5648208f7b1..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js +++ /dev/null @@ -1,47 +0,0 @@ -import eventHub from '../../event_hub'; -import statusIcon from '../mr_widget_status_icon'; - -export default { - name: 'MRWidgetAutoMergeFailed', - props: { - mr: { type: Object, required: true }, - }, - data() { - return { - isRefreshing: false, - }; - }, - components: { - statusIcon, - }, - methods: { - refreshWidget() { - this.isRefreshing = true; - eventHub.$emit('MRWidgetUpdateRequested', () => { - this.isRefreshing = false; - }); - }, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="failed" /> - <div class="media-body space-children"> - <span class="bold"> - <template v-if="mr.mergeError">{{mr.mergeError}}.</template> - This merge request failed to be merged automatically - </span> - <button - @click="refreshWidget" - :disabled="isRefreshing" - type="button" - class="btn btn-xs btn-default"> - <i - v-if="isRefreshing" - class="fa fa-spinner fa-spin" - aria-hidden="true" /> - Refresh - </button> - </div> - </div> - `, -}; 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 new file mode 100644 index 00000000000..40c3cb500bb --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue @@ -0,0 +1,52 @@ +<script> + import loadingIcon from '~/vue_shared/components/loading_icon.vue'; + import eventHub from '../../event_hub'; + import statusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetAutoMergeFailed', + components: { + statusIcon, + loadingIcon, + }, + props: { + mr: { + type: Object, + required: true, + }, + }, + data() { + return { + isRefreshing: false, + }; + }, + methods: { + refreshWidget() { + this.isRefreshing = true; + eventHub.$emit('MRWidgetUpdateRequested', () => { + this.isRefreshing = false; + }); + }, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon status="warning" /> + <div class="media-body space-children"> + <span class="bold"> + <template v-if="mr.mergeError">{{ mr.mergeError }}.</template> + {{ s__("mrWidget|This merge request failed to be merged automatically") }} + </span> + <button + @click="refreshWidget" + :disabled="isRefreshing" + type="button" + class="btn btn-xs btn-default" + > + <loading-icon v-if="isRefreshing" /> + {{ s__("mrWidget|Refresh") }} + </button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js deleted file mode 100644 index 09561694939..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js +++ /dev/null @@ -1,18 +0,0 @@ -import statusIcon from '../mr_widget_status_icon'; - -export default { - name: 'MRWidgetChecking', - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="loading" :show-disabled-button="true" /> - <div class="media-body space-children"> - <span class="bold"> - Checking ability to merge automatically - </span> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue new file mode 100644 index 00000000000..caeaac75b45 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue @@ -0,0 +1,23 @@ +<script> + import statusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetChecking', + components: { + statusIcon, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon + status="loading" + :show-disabled-button="true" + /> + <div class="media-body space-children"> + <span class="bold"> + {{ s__("mrWidget|Checking ability to merge automatically") }} + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js deleted file mode 100644 index b25cc3443ef..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js +++ /dev/null @@ -1,35 +0,0 @@ -import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; -import statusIcon from '../mr_widget_status_icon'; - -export default { - name: 'MRWidgetClosed', - props: { - mr: { type: Object, required: true }, - }, - components: { - 'mr-widget-author-and-time': mrWidgetAuthorTime, - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="failed" /> - <div class="media-body"> - <mr-widget-author-and-time - actionText="Closed by" - :author="mr.closedEvent.author" - :dateTitle="mr.closedEvent.updatedAt" - :dateReadable="mr.closedEvent.formattedUpdatedAt" - /> - <section class="mr-info-list"> - <p> - The changes were not merged into - <a - :href="mr.targetBranchPath" - class="label-branch"> - {{mr.targetBranch}}</a> - </p> - </section> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue new file mode 100644 index 00000000000..68b691fc914 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue @@ -0,0 +1,48 @@ +<script> + import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; + import statusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetClosed', + components: { + mrWidgetAuthorTime, + statusIcon, + }, + props: { + /* TODO: This is providing all store and service down when it + only needs metrics and targetBranch */ + mr: { + type: Object, + required: true, + default: () => ({}), + }, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon + status="warning" + /> + <div class="media-body"> + <mr-widget-author-time + :action-text="s__('mrWidget|Closed by')" + :author="mr.metrics.closedBy" + :date-title="mr.metrics.closedAt" + :date-readable="mr.metrics.readableClosedAt" + /> + + <section class="mr-info-list"> + <p> + {{ s__("mrWidget|The changes were not merged into") }} + <a + :href="mr.targetBranchPath" + class="label-branch" + > + {{ mr.targetBranch }} + </a> + </p> + </section> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js deleted file mode 100644 index 5d468a085cb..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js +++ /dev/null @@ -1,47 +0,0 @@ -import statusIcon from '../mr_widget_status_icon'; - -export default { - name: 'MRWidgetConflicts', - props: { - mr: { type: Object, required: true }, - }, - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon - status="failed" - :show-disabled-button="true" /> - <div class="media-body space-children"> - <span - v-if="mr.shouldBeRebased" - class="bold"> - Fast-forward merge is not possible. - To merge this request, first rebase locally. - </span> - <template v-else> - <span class="bold"> - There are merge conflicts<span v-if="!mr.canMerge">.</span> - <span v-if="!mr.canMerge"> - Resolve these conflicts or ask someone with write access to this repository to merge it locally - </span> - </span> - <a - v-if="mr.canMerge && mr.conflictResolutionPath" - :href="mr.conflictResolutionPath" - class="js-resolve-conflicts-button btn btn-default btn-xs"> - Resolve conflicts - </a> - <a - v-if="mr.canMerge" - class="js-merge-locally-button btn btn-default btn-xs" - data-toggle="modal" - href="#modal_merge_info"> - Merge locally - </a> - </template> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue new file mode 100644 index 00000000000..dad4b0fe49d --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -0,0 +1,61 @@ +<script> + import statusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetConflicts', + components: { + statusIcon, + }, + props: { + /* TODO: This is providing all store and service down when it + only needs a few props */ + mr: { + type: Object, + required: true, + default: () => ({}), + }, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon + status="warning" + :show-disabled-button="true" + /> + + <div class="media-body space-children"> + <span + v-if="mr.shouldBeRebased" + class="bold" + > + {{ s__(`mrWidget|Fast-forward merge is not possible. +To merge this request, first rebase locally.`) }} + </span> + <template v-else> + <span class="bold"> + {{ s__("mrWidget|There are merge conflicts") }}<span v-if="!mr.canMerge">.</span> + <span v-if="!mr.canMerge"> + {{ s__(`mrWidget|Resolve these conflicts or ask someone + with write access to this repository to merge it locally`) }} + </span> + </span> + <a + v-if="mr.canMerge && mr.conflictResolutionPath" + :href="mr.conflictResolutionPath" + class="js-resolve-conflicts-button btn btn-default btn-xs" + > + {{ s__("mrWidget|Resolve conflicts") }} + </a> + <button + v-if="mr.canMerge" + class="js-merge-locally-button btn btn-default btn-xs" + data-toggle="modal" + data-target="#modal_merge_info" + > + {{ s__("mrWidget|Merge locally") }} + </button> + </template> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js deleted file mode 100644 index c25d6c359bb..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js +++ /dev/null @@ -1,78 +0,0 @@ -import statusIcon from '../mr_widget_status_icon'; -import eventHub from '../../event_hub'; - -export default { - name: 'MRWidgetFailedToMerge', - props: { - mr: { type: Object, required: true }, - }, - data() { - return { - timer: 10, - isRefreshing: false, - }; - }, - mounted() { - setInterval(() => { - this.updateTimer(); - }, 1000); - }, - created() { - eventHub.$emit('DisablePolling'); - }, - computed: { - timerText() { - return this.timer > 1 ? `${this.timer} seconds` : 'a second'; - }, - }, - methods: { - refresh() { - this.isRefreshing = true; - eventHub.$emit('MRWidgetUpdateRequested'); - eventHub.$emit('EnablePolling'); - }, - updateTimer() { - this.timer = this.timer - 1; - - if (this.timer === 0) { - this.refresh(); - } - }, - }, - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <template v-if="isRefreshing"> - <status-icon status="loading" /> - <span class="media-body bold js-refresh-label"> - Refreshing now - </span> - </template> - <template v-else> - <status-icon status="failed" :show-disabled-button="true" /> - <div class="media-body space-children"> - <span class="bold"> - <span - class="has-error-message" - v-if="mr.mergeError"> - {{mr.mergeError}}. - </span> - <span v-else>Merge failed.</span> - <span - :class="{ 'has-custom-error': mr.mergeError }"> - Refreshing in {{timerText}} to show the updated status... - </span> - </span> - <button - @click="refresh" - class="btn btn-default btn-xs js-refresh-button" - type="button"> - Refresh now - </button> - </div> - </template> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue new file mode 100644 index 00000000000..602b68ea572 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue @@ -0,0 +1,105 @@ +<script> + import { n__ } from '~/locale'; + import statusIcon from '../mr_widget_status_icon.vue'; + import eventHub from '../../event_hub'; + + export default { + name: 'MRWidgetFailedToMerge', + + components: { + statusIcon, + }, + + props: { + mr: { + type: Object, + required: true, + default: () => ({}), + }, + }, + + data() { + return { + timer: 10, + isRefreshing: false, + }; + }, + + computed: { + timerText() { + return n__( + 'Refreshing in a second to show the updated status...', + 'Refreshing in %d seconds to show the updated status...', + this.timer, + ); + }, + }, + + mounted() { + setInterval(() => { + this.updateTimer(); + }, 1000); + }, + + created() { + eventHub.$emit('DisablePolling'); + }, + + methods: { + refresh() { + this.isRefreshing = true; + eventHub.$emit('MRWidgetUpdateRequested'); + eventHub.$emit('EnablePolling'); + }, + updateTimer() { + this.timer = this.timer - 1; + + if (this.timer === 0) { + this.refresh(); + } + }, + }, + + }; +</script> +<template> + <div class="mr-widget-body media"> + <template v-if="isRefreshing"> + <status-icon status="loading" /> + <span class="media-body bold js-refresh-label"> + {{ s__("mrWidget|Refreshing now") }} + </span> + </template> + <template v-else> + <status-icon + status="warning" + :show-disabled-button="true" + /> + <div class="media-body space-children"> + <span class="bold"> + <span + class="has-error-message" + v-if="mr.mergeError" + > + {{ mr.mergeError }}. + </span> + <span v-else> + {{ s__("mrWidget|Merge failed.") }} + </span> + <span + :class="{ 'has-custom-error': mr.mergeError }" + > + {{ timerText }} + </span> + </span> + <button + @click="refresh" + class="btn btn-default btn-xs js-refresh-button" + type="button" + > + {{ s__("mrWidget|Refresh now") }} + </button> + </div> + </template> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js deleted file mode 100644 index 43b2d238f65..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js +++ /dev/null @@ -1,124 +0,0 @@ -import Flash from '../../../flash'; -import statusIcon from '../mr_widget_status_icon'; -import MRWidgetAuthor from '../../components/mr_widget_author'; -import eventHub from '../../event_hub'; - -export default { - name: 'MRWidgetMergeWhenPipelineSucceeds', - props: { - mr: { type: Object, required: true }, - service: { type: Object, required: true }, - }, - components: { - 'mr-widget-author': MRWidgetAuthor, - statusIcon, - }, - data() { - return { - isCancellingAutoMerge: false, - isRemovingSourceBranch: false, - }; - }, - computed: { - canRemoveSourceBranch() { - const { shouldRemoveSourceBranch, canRemoveSourceBranch, - mergeUserId, currentUserId } = this.mr; - - return !shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId; - }, - }, - methods: { - cancelAutomaticMerge() { - this.isCancellingAutoMerge = true; - this.service.cancelAutomaticMerge() - .then(res => res.json()) - .then((res) => { - eventHub.$emit('UpdateWidgetData', res); - }) - .catch(() => { - this.isCancellingAutoMerge = false; - new Flash('Something went wrong. Please try again.'); // eslint-disable-line - }); - }, - removeSourceBranch() { - const options = { - sha: this.mr.sha, - merge_when_pipeline_succeeds: true, - should_remove_source_branch: true, - }; - - this.isRemovingSourceBranch = true; - this.service.mergeResource.save(options) - .then(res => res.json()) - .then((res) => { - if (res.status === 'merge_when_pipeline_succeeds') { - eventHub.$emit('MRWidgetUpdateRequested'); - } - }) - .catch(() => { - this.isRemovingSourceBranch = false; - new Flash('Something went wrong. Please try again.'); // eslint-disable-line - }); - }, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="success" /> - <div class="media-body"> - <h4 class="flex-container-block"> - <span class="append-right-10"> - Set by - <mr-widget-author :author="mr.setToMWPSBy" /> - to be merged automatically when the pipeline succeeds - </span> - <a - v-if="mr.canCancelAutomaticMerge" - @click.prevent="cancelAutomaticMerge" - :disabled="isCancellingAutoMerge" - role="button" - href="#" - class="btn btn-xs btn-default js-cancel-auto-merge"> - <i - v-if="isCancellingAutoMerge" - class="fa fa-spinner fa-spin" - aria-hidden="true" /> - Cancel automatic merge - </a> - </h4> - <section class="mr-info-list"> - <p>The changes will be merged into - <a - :href="mr.targetBranchPath" - class="label-branch"> - {{mr.targetBranch}} - </a> - </p> - <p v-if="mr.shouldRemoveSourceBranch"> - The source branch will be removed - </p> - <p - v-else - class="flex-container-block" - > - <span class="append-right-10"> - The source branch will not be removed - </span> - <a - v-if="canRemoveSourceBranch" - :disabled="isRemovingSourceBranch" - @click.prevent="removeSourceBranch" - role="button" - class="btn btn-xs btn-default js-remove-source-branch" - href="#"> - <i - v-if="isRemovingSourceBranch" - class="fa fa-spinner fa-spin" - aria-hidden="true" /> - Remove source branch - </a> - </p> - </section> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue new file mode 100644 index 00000000000..de98a77be6f --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue @@ -0,0 +1,147 @@ +<script> + import Flash from '../../../flash'; + import statusIcon from '../mr_widget_status_icon.vue'; + import mrWidgetAuthor from '../../components/mr_widget_author.vue'; + import eventHub from '../../event_hub'; + + export default { + name: 'MRWidgetMergeWhenPipelineSucceeds', + components: { + mrWidgetAuthor, + statusIcon, + }, + props: { + mr: { + type: Object, + required: true, + default: () => ({}), + }, + service: { + type: Object, + required: true, + default: () => ({}), + }, + }, + data() { + return { + isCancellingAutoMerge: false, + isRemovingSourceBranch: false, + }; + }, + computed: { + canRemoveSourceBranch() { + const { + shouldRemoveSourceBranch, + canRemoveSourceBranch, + mergeUserId, + currentUserId, + } = this.mr; + + return !shouldRemoveSourceBranch && + canRemoveSourceBranch && + mergeUserId === currentUserId; + }, + }, + methods: { + cancelAutomaticMerge() { + this.isCancellingAutoMerge = true; + this.service.cancelAutomaticMerge() + .then(res => res.data) + .then((data) => { + eventHub.$emit('UpdateWidgetData', data); + }) + .catch(() => { + this.isCancellingAutoMerge = false; + Flash('Something went wrong. Please try again.'); + }); + }, + removeSourceBranch() { + const options = { + sha: this.mr.sha, + merge_when_pipeline_succeeds: true, + should_remove_source_branch: true, + }; + + this.isRemovingSourceBranch = true; + this.service.mergeResource.save(options) + .then(res => res.data) + .then((data) => { + if (data.status === 'merge_when_pipeline_succeeds') { + eventHub.$emit('MRWidgetUpdateRequested'); + } + }) + .catch(() => { + this.isRemovingSourceBranch = false; + Flash('Something went wrong. Please try again.'); + }); + }, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon status="success" /> + <div class="media-body"> + <h4 class="flex-container-block"> + <span class="append-right-10"> + {{ s__("mrWidget|Set by") }} + <mr-widget-author :author="mr.setToMWPSBy" /> + {{ s__("mrWidget|to be merged automatically when the pipeline succeeds") }} + </span> + <a + v-if="mr.canCancelAutomaticMerge" + @click.prevent="cancelAutomaticMerge" + :disabled="isCancellingAutoMerge" + role="button" + href="#" + class="btn btn-xs btn-default js-cancel-auto-merge"> + <i + v-if="isCancellingAutoMerge" + class="fa fa-spinner fa-spin" + aria-hidden="true" + > + </i> + {{ s__("mrWidget|Cancel automatic merge") }} + </a> + </h4> + <section class="mr-info-list"> + <p> + {{ s__("mrWidget|The changes will be merged into") }} + <a + :href="mr.targetBranchPath" + class="label-branch" + > + {{ mr.targetBranch }} + </a> + </p> + <p v-if="mr.shouldRemoveSourceBranch"> + {{ s__("mrWidget|The source branch will be removed") }} + </p> + <p + v-else + class="flex-container-block" + > + <span class="append-right-10"> + {{ s__("mrWidget|The source branch will not be removed") }} + </span> + <a + v-if="canRemoveSourceBranch" + :disabled="isRemovingSourceBranch" + @click.prevent="removeSourceBranch" + role="button" + class="btn btn-xs btn-default js-remove-source-branch" + href="#" + > + <i + v-if="isRemovingSourceBranch" + class="fa fa-spinner fa-spin" + aria-hidden="true" + > + </i> + {{ s__("mrWidget|Remove source branch") }} + </a> + </p> + </section> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js deleted file mode 100644 index 2dfd87ed904..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js +++ /dev/null @@ -1,139 +0,0 @@ -import Flash from '../../../flash'; -import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; -import tooltip from '../../../vue_shared/directives/tooltip'; -import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; -import statusIcon from '../mr_widget_status_icon'; -import eventHub from '../../event_hub'; - -export default { - name: 'MRWidgetMerged', - props: { - mr: { type: Object, required: true }, - service: { type: Object, required: true }, - }, - data() { - return { - isMakingRequest: false, - }; - }, - directives: { - tooltip, - }, - components: { - 'mr-widget-author-and-time': mrWidgetAuthorTime, - loadingIcon, - statusIcon, - }, - computed: { - shouldShowRemoveSourceBranch() { - const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr; - - return !sourceBranchRemoved && canRemoveSourceBranch && - !this.isMakingRequest && !isRemovingSourceBranch; - }, - shouldShowSourceBranchRemoving() { - const { sourceBranchRemoved, isRemovingSourceBranch } = this.mr; - return !sourceBranchRemoved && (isRemovingSourceBranch || this.isMakingRequest); - }, - shouldShowMergedButtons() { - const { canRevertInCurrentMR, canCherryPickInCurrentMR, revertInForkPath, - cherryPickInForkPath } = this.mr; - - return canRevertInCurrentMR || canCherryPickInCurrentMR || - revertInForkPath || cherryPickInForkPath; - }, - }, - methods: { - removeSourceBranch() { - this.isMakingRequest = true; - this.service.removeSourceBranch() - .then(res => res.json()) - .then((res) => { - if (res.message === 'Branch was removed') { - eventHub.$emit('MRWidgetUpdateRequested', () => { - this.isMakingRequest = false; - }); - } - }) - .catch(() => { - this.isMakingRequest = false; - new Flash('Something went wrong. Please try again.'); // eslint-disable-line - }); - }, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="success" /> - <div class="media-body"> - <div class="space-children"> - <mr-widget-author-and-time - actionText="Merged by" - :author="mr.mergedEvent.author" - :date-title="mr.mergedEvent.updatedAt" - :date-readable="mr.mergedEvent.formattedUpdatedAt" /> - <a - v-if="mr.canRevertInCurrentMR" - v-tooltip - class="btn btn-close btn-xs" - href="#modal-revert-commit" - data-toggle="modal" - data-container="body" - title="Revert this merge request in a new merge request"> - Revert - </a> - <a - v-else-if="mr.revertInForkPath" - v-tooltip - class="btn btn-close btn-xs" - data-method="post" - :href="mr.revertInForkPath" - title="Revert this merge request in a new merge request"> - Revert - </a> - <a - v-if="mr.canCherryPickInCurrentMR" - v-tooltip - class="btn btn-default btn-xs" - href="#modal-cherry-pick-commit" - data-toggle="modal" - data-container="body" - title="Cherry-pick this merge request in a new merge request"> - Cherry-pick - </a> - <a - v-else-if="mr.cherryPickInForkPath" - v-tooltip - class="btn btn-default btn-xs" - data-method="post" - :href="mr.cherryPickInForkPath" - title="Cherry-pick this merge request in a new merge request"> - Cherry-pick - </a> - </div> - <section class="mr-info-list"> - <p> - The changes were merged into - <span class="label-branch"> - <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a> - </span> - </p> - <p v-if="mr.sourceBranchRemoved">The source branch has been removed</p> - <p v-if="shouldShowRemoveSourceBranch" class="space-children"> - <span>You can remove source branch now</span> - <button - @click="removeSourceBranch" - :disabled="isMakingRequest" - type="button" - class="btn btn-xs btn-default js-remove-branch-button"> - Remove Source Branch - </button> - </p> - <p v-if="shouldShowSourceBranchRemoving"> - <loading-icon inline /> - <span>The source branch is being removed</span> - </p> - </section> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue new file mode 100644 index 00000000000..c1618bc6ea0 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -0,0 +1,192 @@ +<script> + import Flash from '~/flash'; + import tooltip from '~/vue_shared/directives/tooltip'; + import loadingIcon from '~/vue_shared/components/loading_icon.vue'; + import { s__, __ } from '~/locale'; + import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; + import statusIcon from '../mr_widget_status_icon.vue'; + import eventHub from '../../event_hub'; + + export default { + name: 'MRWidgetMerged', + directives: { + tooltip, + }, + components: { + mrWidgetAuthorTime, + loadingIcon, + statusIcon, + }, + props: { + mr: { + type: Object, + required: true, + default: () => ({}), + }, + service: { + type: Object, + required: true, + default: () => ({}), + }, + }, + data() { + return { + isMakingRequest: false, + }; + }, + computed: { + shouldShowRemoveSourceBranch() { + const { + sourceBranchRemoved, + isRemovingSourceBranch, + canRemoveSourceBranch, + } = this.mr; + + return !sourceBranchRemoved && + canRemoveSourceBranch && + !this.isMakingRequest && + !isRemovingSourceBranch; + }, + shouldShowSourceBranchRemoving() { + const { + sourceBranchRemoved, + isRemovingSourceBranch, + } = this.mr; + return !sourceBranchRemoved && + (isRemovingSourceBranch || this.isMakingRequest); + }, + shouldShowMergedButtons() { + const { + canRevertInCurrentMR, + canCherryPickInCurrentMR, + revertInForkPath, + cherryPickInForkPath, + } = this.mr; + + return canRevertInCurrentMR || + canCherryPickInCurrentMR || + revertInForkPath || + cherryPickInForkPath; + }, + revertTitle() { + return s__('mrWidget|Revert this merge request in a new merge request'); + }, + cherryPickTitle() { + return s__('mrWidget|Cherry-pick this merge request in a new merge request'); + }, + revertLabel() { + return s__('mrWidget|Revert'); + }, + cherryPickLabel() { + return s__('mrWidget|Cherry-pick'); + }, + }, + methods: { + removeSourceBranch() { + this.isMakingRequest = true; + + this.service.removeSourceBranch() + .then(res => res.data) + .then((data) => { + if (data.message === 'Branch was removed') { + eventHub.$emit('MRWidgetUpdateRequested', () => { + this.isMakingRequest = false; + }); + } + }) + .catch(() => { + this.isMakingRequest = false; + Flash(__('Something went wrong. Please try again.')); + }); + }, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon status="success" /> + <div class="media-body"> + <div class="space-children"> + <mr-widget-author-time + :action-text="s__('mrWidget|Merged by')" + :author="mr.metrics.mergedBy" + :date-title="mr.metrics.mergedAt" + :date-readable="mr.metrics.readableMergedAt" + /> + <a + v-if="mr.canRevertInCurrentMR" + v-tooltip + class="btn btn-close btn-xs" + href="#modal-revert-commit" + data-toggle="modal" + data-container="body" + :title="revertTitle" + > + {{ revertLabel }} + </a> + <a + v-else-if="mr.revertInForkPath" + v-tooltip + class="btn btn-close btn-xs" + data-method="post" + :href="mr.revertInForkPath" + :title="revertTitle" + > + {{ revertLabel }} + </a> + <a + v-if="mr.canCherryPickInCurrentMR" + v-tooltip + class="btn btn-default btn-xs" + href="#modal-cherry-pick-commit" + data-toggle="modal" + data-container="body" + :title="cherryPickTitle" + > + {{ cherryPickLabel }} + </a> + <a + v-else-if="mr.cherryPickInForkPath" + v-tooltip + class="btn btn-default btn-xs" + data-method="post" + :href="mr.cherryPickInForkPath" + :title="cherryPickTitle" + > + {{ cherryPickLabel }} + </a> + </div> + <section class="mr-info-list"> + <p> + {{ s__("mrWidget|The changes were merged into") }} + <span class="label-branch"> + <a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a> + </span> + </p> + <p v-if="mr.sourceBranchRemoved"> + {{ s__("mrWidget|The source branch has been removed") }} + </p> + <p + v-if="shouldShowRemoveSourceBranch" + class="space-children" + > + <span>{{ s__("mrWidget|You can remove source branch now") }}</span> + <button + @click="removeSourceBranch" + :disabled="isMakingRequest" + type="button" + class="btn btn-xs btn-default js-remove-branch-button" + > + {{ s__("mrWidget|Remove Source Branch") }} + </button> + </p> + <p v-if="shouldShowSourceBranchRemoving"> + <loading-icon :inline="true" /> + <span> + {{ s__("mrWidget|The source branch is being removed") }} + </span> + </p> + </section> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js deleted file mode 100644 index f6d1a4feeb2..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js +++ /dev/null @@ -1,29 +0,0 @@ -import statusIcon from '../mr_widget_status_icon'; - -export default { - name: 'MRWidgetMerging', - props: { - mr: { type: Object, required: true }, - }, - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body mr-state-locked media"> - <status-icon status="loading" /> - <div class="media-body"> - <h4> - This merge request is in the process of being merged - </h4> - <section class="mr-info-list"> - <p> - The changes will be merged into - <span class="label-branch"> - <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a> - </span> - </p> - </section> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue new file mode 100644 index 00000000000..953ddf40a51 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue @@ -0,0 +1,35 @@ +<script> + import statusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetMerging', + components: { + statusIcon, + }, + props: { + mr: { + type: Object, + required: true, + default: () => ({}), + }, + }, + }; +</script> +<template> + <div class="mr-widget-body mr-state-locked media"> + <status-icon status="loading" /> + <div class="media-body"> + <h4> + {{ s__("mrWidget|This merge request is in the process of being merged") }} + </h4> + <section class="mr-info-list"> + <p> + {{ s__("mrWidget|The changes will be merged into") }} + <span class="label-branch"> + <a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a> + </span> + </p> + </section> + </div> + </div> +</template> 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 index 1bc0b7e0819..7733fb74afe 100644 --- 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 @@ -1,6 +1,6 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; -import mrWidgetMergeHelp from '../../components/mr_widget_merge_help'; +import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue'; export default { name: 'MRWidgetMissingBranch', @@ -24,7 +24,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" :show-disabled-button="true" /> + <status-icon status="warning" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold js-branch-text"> <span class="capitalize"> 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 index 00047718201..cea3d97fa88 100644 --- 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 @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetNotAllowed', 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 index 1cedf86e811..e66ce071ab4 100644 --- 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 @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetPipelineBlocked', @@ -7,7 +7,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" :show-disabled-button="true" /> + <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 diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js index 6853ba4b9f8..4d9a2ca530f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetPipelineBlocked', @@ -7,7 +7,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" :show-disabled-button="true" /> + <status-icon status="warning" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure 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 be37dd87de9..7ba6c29006a 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 @@ -1,8 +1,9 @@ import successSvg from 'icons/_icon_status_success.svg'; import warningSvg from 'icons/_icon_status_warning.svg'; import simplePoll from '~/lib/utils/simple_poll'; +import MergeRequest from '../../../merge_request'; import Flash from '../../../flash'; -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; export default { @@ -68,7 +69,7 @@ export default { }, iconClass() { if (this.status === 'failed' || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge) { - return 'failed'; + return 'warning'; } return 'success'; }, @@ -135,16 +136,16 @@ export default { this.isMakingRequest = true; this.service.merge(options) - .then(res => res.json()) - .then((res) => { - const hasError = res.status === 'failed' || res.status === 'hook_validation_error'; + .then(res => res.data) + .then((data) => { + const hasError = data.status === 'failed' || data.status === 'hook_validation_error'; - if (res.status === 'merge_when_pipeline_succeeds') { + if (data.status === 'merge_when_pipeline_succeeds') { eventHub.$emit('MRWidgetUpdateRequested'); - } else if (res.status === 'success') { + } else if (data.status === 'success') { this.initiateMergePolling(); } else if (hasError) { - eventHub.$emit('FailedToMerge', res.merge_error); + eventHub.$emit('FailedToMerge', data.merge_error); } }) .catch(() => { @@ -159,26 +160,24 @@ export default { }, handleMergePolling(continuePolling, stopPolling) { this.service.poll() - .then(res => res.json()) - .then((res) => { - if (res.state === 'merged') { + .then(res => res.data) + .then((data) => { + if (data.state === 'merged') { // If state is merged we should update the widget and stop the polling eventHub.$emit('MRWidgetUpdateRequested'); eventHub.$emit('FetchActionsContent'); - if (window.mergeRequest) { - window.mergeRequest.updateStatusText('status-box-open', 'status-box-merged', 'Merged'); - window.mergeRequest.hideCloseButton(); - window.mergeRequest.decreaseCounter(); - } + MergeRequest.setStatusBoxToMerged(); + MergeRequest.hideCloseButton(); + MergeRequest.decreaseCounter(); stopPolling(); // If user checked remove source branch and we didn't remove the branch yet // we should start another polling for source branch remove process - if (this.removeSourceBranch && res.source_branch_exists) { + if (this.removeSourceBranch && data.source_branch_exists) { this.initiateRemoveSourceBranchPolling(); } - } else if (res.merge_error) { - eventHub.$emit('FailedToMerge', res.merge_error); + } else if (data.merge_error) { + eventHub.$emit('FailedToMerge', data.merge_error); stopPolling(); } else { // MR is not merged yet, continue polling until the state becomes 'merged' @@ -199,11 +198,11 @@ export default { }, handleRemoveBranchPolling(continuePolling, stopPolling) { this.service.poll() - .then(res => res.json()) - .then((res) => { + .then(res => res.data) + .then((data) => { // If source branch exists then we should continue polling // because removing a source branch is a background task and takes time - if (res.source_branch_exists) { + if (data.source_branch_exists) { continuePolling(); } else { // Branch is removed. Update widget, stop polling and hide the spinner 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 new file mode 100644 index 00000000000..2968af0d5cb --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -0,0 +1,137 @@ +<script> + import simplePoll from '../../../lib/utils/simple_poll'; + import eventHub from '../../event_hub'; + import statusIcon from '../mr_widget_status_icon.vue'; + import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; + import Flash from '../../../flash'; + + export default { + name: 'MRWidgetRebase', + components: { + statusIcon, + loadingIcon, + }, + props: { + mr: { + type: Object, + required: true, + }, + service: { + type: Object, + required: true, + }, + }, + data() { + return { + isMakingRequest: false, + rebasingError: null, + }; + }, + computed: { + status() { + if (this.mr.rebaseInProgress || this.isMakingRequest) { + return 'loading'; + } + if (!this.mr.canPushToSourceBranch && !this.mr.rebaseInProgress) { + return 'warning'; + } + return 'success'; + }, + showDisabledButton() { + return ['failed', 'loading'].includes(this.status); + }, + }, + methods: { + rebase() { + this.isMakingRequest = true; + this.rebasingError = null; + + this.service.rebase() + .then(() => { + simplePoll(this.checkRebaseStatus); + }) + .catch((error) => { + this.rebasingError = error.merge_error; + this.isMakingRequest = false; + Flash('Something went wrong. Please try again.'); + }); + }, + checkRebaseStatus(continuePolling, stopPolling) { + this.service.poll() + .then(res => res.data) + .then((res) => { + if (res.rebase_in_progress) { + continuePolling(); + } else { + this.isMakingRequest = false; + + if (res.merge_error && res.merge_error.length) { + this.rebasingError = res.merge_error; + Flash('Something went wrong. Please try again.'); + } + + eventHub.$emit('MRWidgetUpdateRequested'); + stopPolling(); + } + }) + .catch(() => { + this.isMakingRequest = false; + Flash('Something went wrong. Please try again.'); + stopPolling(); + }); + }, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon + :status="status" + :show-disabled-button="showDisabledButton" + /> + + <div class="rebase-state-find-class-convention media media-body space-children"> + <template v-if="mr.rebaseInProgress || isMakingRequest"> + <span class="bold"> + Rebase in progress + </span> + </template> + <template v-if="!mr.rebaseInProgress && !mr.canPushToSourceBranch"> + <span class="bold"> + Fast-forward merge is not possible. + Rebase the source branch onto + <span class="label-branch">{{ mr.targetBranch }}</span> + to allow this merge request to be merged. + </span> + </template> + <template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest"> + <div + class="accept-merge-holder clearfix +js-toggle-container accept-action media space-children"> + <button + type="button" + class="btn btn-sm btn-reopen btn-success" + :disabled="isMakingRequest" + @click="rebase" + > + <loading-icon v-if="isMakingRequest" /> + Rebase + </button> + <span + v-if="!rebasingError" + class="bold" + > + Fast-forward merge is not possible. + Rebase the source branch onto the target branch or merge target + branch into source branch to allow this merge request to be merged. + </span> + <span + v-else + class="bold danger"> + {{ rebasingError }} + </span> + </div> + </template> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js index af19cf6ab87..142ddf477f1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetSHAMismatch', @@ -7,7 +7,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" :show-disabled-button="true" /> + <status-icon status="warning" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> The source branch HEAD has recently changed. Please reload the page and review the changes before merging diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js index a119ecbbdfe..67b271c69ca 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetUnresolvedDiscussions', @@ -10,7 +10,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" :show-disabled-button="true" /> + <status-icon status="warning" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> There are unresolved discussions. Please resolve these discussions diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js index 4f83350e07c..bbca641f65e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; import eventHub from '../../event_hub'; @@ -23,9 +23,9 @@ export default { removeWIP() { this.isMakingRequest = true; this.service.removeWIP() - .then(res => res.json()) - .then((res) => { - eventHub.$emit('UpdateWidgetData', res); + .then(res => res.data) + .then((data) => { + eventHub.$emit('UpdateWidgetData', data); new window.Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line $('.merge-request .detail-page-description .title').text(this.mr.title); }) @@ -37,7 +37,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" :show-disabled-button="Boolean(mr.removeWIPPath)" /> + <status-icon status="warning" :show-disabled-button="Boolean(mr.removeWIPPath)" /> <div class="media-body space-children"> <span class="bold"> This is a Work in Progress diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js index 5bd8b99420a..7ca15537719 100644 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js @@ -11,18 +11,18 @@ export { default as Vue } from 'vue'; export { default as SmartInterval } from '~/smart_interval'; -export { default as WidgetHeader } from './components/mr_widget_header'; -export { default as WidgetMergeHelp } from './components/mr_widget_merge_help'; +export { default as WidgetHeader } from './components/mr_widget_header.vue'; +export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue'; export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue'; export { default as WidgetDeployment } from './components/mr_widget_deployment'; -export { default as WidgetRelatedLinks } from './components/mr_widget_related_links'; -export { default as MergedState } from './components/states/mr_widget_merged'; -export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge'; -export { default as ClosedState } from './components/states/mr_widget_closed'; -export { default as MergingState } from './components/states/mr_widget_merging'; +export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue'; +export { default as MergedState } from './components/states/mr_widget_merged.vue'; +export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue'; +export { default as ClosedState } from './components/states/mr_widget_closed.vue'; +export { default as MergingState } from './components/states/mr_widget_merging.vue'; export { default as WipState } from './components/states/mr_widget_wip'; -export { default as ArchivedState } from './components/states/mr_widget_archived'; -export { default as ConflictsState } from './components/states/mr_widget_conflicts'; +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'; @@ -31,9 +31,10 @@ export { default as SHAMismatchState } from './components/states/mr_widget_sha_m 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 PipelineFailedState } from './components/states/mr_widget_pipeline_failed'; -export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds'; -export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed'; -export { default as CheckingState } from './components/states/mr_widget_checking'; +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'; +export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue'; +export { default as CheckingState } from './components/states/mr_widget_checking.vue'; export { default as MRWidgetStore } from './stores/mr_widget_store'; export { default as MRWidgetService } from './services/mr_widget_service'; export { default as eventHub } from './event_hub'; diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index 43ef468c303..6b9918b65b0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -2,6 +2,9 @@ import { Vue, mrWidgetOptions, } from './dependencies'; +import Translate from '../vue_shared/translate'; + +Vue.use(Translate); document.addEventListener('DOMContentLoaded', () => { gl.mrWidgetData.gitlabLogo = gon.gitlab_logo; 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 8a9129c385b..edf67fcd0a7 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 @@ -1,4 +1,4 @@ -import Project from '~/project'; +import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; import Flash from '../flash'; import { @@ -10,6 +10,7 @@ import { MergedState, ClosedState, MergingState, + RebaseState, WipState, ArchivedState, ConflictsState, @@ -79,19 +80,20 @@ export default { ciEnvironmentsStatusPath: store.ciEnvironmentsStatusPath, statusPath: store.statusPath, mergeActionsContentPath: store.mergeActionsContentPath, + rebasePath: store.rebasePath, }; return new MRWidgetService(endpoints); }, checkStatus(cb) { return this.service.checkStatus() - .then(res => res.json()) - .then((res) => { - this.handleNotification(res); - this.mr.setData(res); + .then(res => res.data) + .then((data) => { + this.handleNotification(data); + this.mr.setData(data); this.setFaviconHelper(); if (cb) { - cb.call(null, res); + cb.call(null, data); } }) .catch(() => { @@ -124,10 +126,10 @@ export default { }, fetchDeployments() { return this.service.fetchDeployments() - .then(res => res.json()) - .then((res) => { - if (res.length) { - this.mr.deployments = res; + .then(res => res.data) + .then((data) => { + if (data.length) { + this.mr.deployments = data; } }) .catch(() => { @@ -137,9 +139,9 @@ export default { fetchActionsContent() { this.service.fetchMergeActionsContent() .then((res) => { - if (res.body) { + if (res.data) { const el = document.createElement('div'); - el.innerHTML = res.body; + el.innerHTML = res.data; document.body.appendChild(el); Project.initRefSwitcher(); } @@ -232,6 +234,7 @@ export default { 'mr-widget-pipeline-failed': PipelineFailedState, 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState, 'mr-widget-auto-merge-failed': AutoMergeFailed, + 'mr-widget-rebase': RebaseState, }, template: ` <div class="mr-state-widget prepend-top-default"> @@ -254,7 +257,8 @@ export default { <mr-widget-related-links v-if="shouldRenderRelatedLinks" :state="mr.state" - :related-links="mr.relatedLinks" /> + :related-links="mr.relatedLinks" + /> </div> <div class="mr-widget-footer" diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js index 5fa838baba3..fecbfec2214 100644 --- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js +++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js @@ -1,57 +1,51 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; - -Vue.use(VueResource); +import axios from '../../lib/utils/axios_utils'; export default class MRWidgetService { constructor(endpoints) { - this.mergeResource = Vue.resource(endpoints.mergePath); - this.mergeCheckResource = Vue.resource(`${endpoints.statusPath}?serializer=widget`); - this.cancelAutoMergeResource = Vue.resource(endpoints.cancelAutoMergePath); - this.removeWIPResource = Vue.resource(endpoints.removeWIPPath); - this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath); - this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath); - this.pollResource = Vue.resource(`${endpoints.statusPath}?serializer=basic`); - this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath); + this.endpoints = endpoints; } merge(data) { - return this.mergeResource.save(data); + return axios.post(this.endpoints.mergePath, data); } cancelAutomaticMerge() { - return this.cancelAutoMergeResource.save(); + return axios.post(this.endpoints.cancelAutoMergePath); } removeWIP() { - return this.removeWIPResource.save(); + return axios.post(this.endpoints.removeWIPPath); } removeSourceBranch() { - return this.removeSourceBranchResource.delete(); + return axios.delete(this.endpoints.sourceBranchPath); } fetchDeployments() { - return this.deploymentsResource.get(); + return axios.get(this.endpoints.ciEnvironmentsStatusPath); } poll() { - return this.pollResource.get(); + return axios.get(`${this.endpoints.statusPath}?serializer=basic`); } checkStatus() { - return this.mergeCheckResource.get(); + return axios.get(`${this.endpoints.statusPath}?serializer=widget`); } fetchMergeActionsContent() { - return this.mergeActionsContentResource.get(); + return axios.get(this.endpoints.mergeActionsContentPath); + } + + rebase() { + return axios.post(this.endpoints.rebasePath); } static stopEnvironment(url) { - return Vue.http.post(url); + return axios.post(url); } static fetchMetrics(metricsUrl) { - return Vue.http.get(`${metricsUrl}.json`); + return axios.get(`${metricsUrl}.json`); } } diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js index 2bace3311c8..f7f0c1b6cb7 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js @@ -25,6 +25,8 @@ export default function deviseState(data) { return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds; } else if (!this.canMerge) { return stateKey.notAllowedToMerge; + } else if (this.shouldBeRebased) { + return stateKey.rebase; } else if (this.canBeMerged) { return stateKey.readyToMerge; } 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 93d31a2a684..ed004b3bb08 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 @@ -26,6 +26,7 @@ export default class MergeRequestStore { this.divergedCommitsCount = data.diverged_commits_count; this.pipeline = data.pipeline || {}; this.deployments = this.deployments || data.deployments || []; + this.initRebase(data); if (data.issues_links) { const links = data.issues_links; @@ -39,9 +40,8 @@ export default class MergeRequestStore { } this.updatedAt = data.updated_at; - this.mergedEvent = MergeRequestStore.getEventObject(data.merge_event); - this.closedEvent = MergeRequestStore.getEventObject(data.closed_event); - this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} }); + this.metrics = MergeRequestStore.buildMetrics(data.metrics); + this.setToMWPSBy = MergeRequestStore.formatUserObject(data.merge_user || {}); this.mergeUserId = data.merge_user_id; this.currentUserId = gon.current_user_id; this.sourceBranchPath = data.source_branch_path; @@ -125,43 +125,49 @@ export default class MergeRequestStore { return this.state === stateKey.nothingToMerge; } - static getEventObject(event) { - return { - author: MergeRequestStore.getAuthorObject(event), - updatedAt: formatDate(MergeRequestStore.getEventUpdatedAtDate(event)), - formattedUpdatedAt: MergeRequestStore.getEventDate(event), - }; + initRebase(data) { + this.canPushToSourceBranch = data.can_push_to_source_branch; + this.rebaseInProgress = data.rebase_in_progress; + this.approvalsLeft = !data.approved; + this.rebasePath = data.rebase_path; } - static getAuthorObject(event) { - if (!event) { + static buildMetrics(metrics) { + if (!metrics) { return {}; } return { - name: event.author.name || '', - username: event.author.username || '', - webUrl: event.author.web_url || '', - avatarUrl: event.author.avatar_url || '', + mergedBy: MergeRequestStore.formatUserObject(metrics.merged_by), + closedBy: MergeRequestStore.formatUserObject(metrics.closed_by), + mergedAt: formatDate(metrics.merged_at), + closedAt: formatDate(metrics.closed_at), + readableMergedAt: MergeRequestStore.getReadableDate(metrics.merged_at), + readableClosedAt: MergeRequestStore.getReadableDate(metrics.closed_at), }; } - static getEventUpdatedAtDate(event) { - if (!event) { - return ''; + static formatUserObject(user) { + if (!user) { + return {}; } - return event.updated_at; + return { + name: user.name || '', + username: user.username || '', + webUrl: user.web_url || '', + avatarUrl: user.avatar_url || '', + }; } - static getEventDate(event) { - const timeagoInstance = new Timeago(); - - if (!event) { + static getReadableDate(date) { + if (!date) { return ''; } - return timeagoInstance.format(MergeRequestStore.getEventUpdatedAtDate(event)); + const timeagoInstance = new Timeago(); + + return timeagoInstance.format(date); } } diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js index de980c175fb..29d5bd4a1da 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js @@ -17,6 +17,7 @@ const stateToComponentMap = { failedToMerge: 'mr-widget-failed-to-merge', autoMergeFailed: 'mr-widget-auto-merge-failed', shaMismatch: 'mr-widget-sha-mismatch', + rebase: 'mr-widget-rebase', }; const statesToShowHelpWidget = [ @@ -29,6 +30,7 @@ const statesToShowHelpWidget = [ 'pipelineFailed', 'pipelineBlocked', 'autoMergeFailed', + 'rebase', ]; export const stateKey = { @@ -46,6 +48,7 @@ export const stateKey = { mergeWhenPipelineSucceeds: 'mergeWhenPipelineSucceeds', notAllowedToMerge: 'notAllowedToMerge', readyToMerge: 'readyToMerge', + rebase: 'rebase', }; export default { diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue index fc795936abf..5324d5dc797 100644 --- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue +++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue @@ -23,6 +23,12 @@ */ export default { + components: { + ciIcon, + }, + directives: { + tooltip, + }, props: { status: { type: Object, @@ -34,12 +40,6 @@ default: true, }, }, - components: { - ciIcon, - }, - directives: { - tooltip, - }, computed: { cssClass() { const className = this.status.group; @@ -53,11 +53,12 @@ :href="status.details_path" :class="cssClass" v-tooltip - :title="!showText ? status.text : ''"> + :title="!showText ? status.text : ''" + > <ci-icon :status="status" /> <template v-if="showText"> - {{status.text}} + {{ status.text }} </template> </a> </template> diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index 2a018f38366..8fea746f4de 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -23,6 +23,9 @@ * - Jobs show view sidebar */ export default { + components: { + icon, + }, props: { status: { type: Object, @@ -30,10 +33,6 @@ }, }, - components: { - icon, - }, - computed: { cssClass() { const status = this.status.group; @@ -43,9 +42,7 @@ }; </script> <template> - <span - :class="cssClass"> - <icon - :name="status.icon"/> + <span :class="cssClass"> + <icon :name="status.icon" /> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue index 3a7143c450e..31d9b9d9c48 100644 --- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -1,10 +1,14 @@ <script> + import tooltip from '../directives/tooltip'; /** * Falls back to the code used in `copy_to_clipboard.js` */ export default { - name: 'clipboardButton', + name: 'ClipboardButton', + directives: { + tooltip, + }, props: { text: { type: String, @@ -14,6 +18,16 @@ type: String, required: true, }, + tooltipPlacement: { + type: String, + required: false, + default: 'top', + }, + tooltipContainer: { + type: [String, Boolean], + required: false, + default: false, + }, }, }; </script> @@ -22,11 +36,16 @@ <button type="button" class="btn btn-transparent btn-clipboard" - :data-title="title" - :data-clipboard-text="text"> - <i - aria-hidden="true" - class="fa fa-clipboard"> - </i> + :title="title" + :data-clipboard-text="text" + v-tooltip + :data-container="tooltipContainer" + :data-placement="tooltipPlacement" + > + <i + aria-hidden="true" + class="fa fa-clipboard" + > + </i> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index 52814de8b2d..6d1fe7ee8ca 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -2,13 +2,21 @@ import commitIconSvg from 'icons/_icon_commit.svg'; import userAvatarLink from './user_avatar/user_avatar_link.vue'; import tooltip from '../directives/tooltip'; + import icon from '../../vue_shared/components/icon.vue'; export default { + directives: { + tooltip, + }, + components: { + userAvatarLink, + icon, + }, props: { /** * Indicates the existance of a tag. * Used to render the correct icon, if true will render `fa-tag` icon, - * if false will render `fa-code-fork` icon. + * if false will render a svg sprite fork icon */ tag: { type: Boolean, @@ -102,12 +110,6 @@ this.author.username ? `${this.author.username}'s avatar` : null; }, }, - directives: { - tooltip, - }, - components: { - userAvatarLink, - }, created() { this.commitIconSvg = commitIconSvg; }, @@ -116,18 +118,17 @@ <template> <div class="branch-commit"> <template v-if="hasCommitRef && showBranch"> - <div - class="icon-container hidden-xs"> + <div class="icon-container hidden-xs"> <i v-if="tag" class="fa fa-tag" - aria-hidden="true"> + aria-hidden="true" + > </i> - <i + <icon v-if="!tag" - class="fa fa-code-fork" - aria-hidden="true"> - </i> + name="fork" + /> </div> <a @@ -135,25 +136,29 @@ :href="commitRef.ref_url" v-tooltip data-container="body" - :title="commitRef.name"> - {{commitRef.name}} + :title="commitRef.name" + > + {{ commitRef.name }} </a> </template> <div v-html="commitIconSvg" - class="commit-icon js-commit-icon"> + class="commit-icon js-commit-icon" + > </div> <a class="commit-sha" - :href="commitUrl"> - {{shortSha}} + :href="commitUrl" + > + {{ shortSha }} </a> <div class="commit-title flex-truncate-parent"> <span v-if="title" - class="flex-truncate-child"> + class="flex-truncate-child" + > <user-avatar-link v-if="hasAuthor" class="avatar-image-container" @@ -164,8 +169,9 @@ /> <a class="commit-row-message" - :href="commitUrl"> - {{title}} + :href="commitUrl" + > + {{ title }} </a> </span> <span v-else> diff --git a/app/assets/javascripts/vue_shared/components/confirmation_input.vue b/app/assets/javascripts/vue_shared/components/confirmation_input.vue new file mode 100644 index 00000000000..1aa03ea6317 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/confirmation_input.vue @@ -0,0 +1,62 @@ +<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 new file mode 100644 index 00000000000..3595a9389e9 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/expand_button.vue @@ -0,0 +1,46 @@ +<script> + import { __ } from '~/locale'; + /** + * Port of detail_behavior expand button. + * + * @example + * <expand-button> + * <template slot="expanded"> + * Text goes here. + * </template> + * </expand-button> + */ + export default { + name: 'ExpandButton', + data() { + return { + isCollapsed: true, + }; + }, + computed: { + ariaLabel() { + return __('Click to expand text'); + }, + }, + methods: { + onClick() { + this.isCollapsed = !this.isCollapsed; + }, + }, + }; +</script> +<template> + <span> + <button + type="button" + v-show="isCollapsed" + class="text-expander btn-blank" + :aria-label="ariaLabel" + @click="onClick"> + ... + </button> + <span v-show="!isCollapsed"> + <slot name="expanded"></slot> + </span> + </span> +</template> diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue new file mode 100644 index 00000000000..c9d7c0f4999 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/file_icon.vue @@ -0,0 +1,92 @@ +<script> + import getIconForFile from './file_icon/file_icon_map'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import icon from '../../vue_shared/components/icon.vue'; + + /* This is a re-usable vue component for rendering a svg sprite + icon + + Sample configuration: + + <file-icon + name="retry" + :size="32" + css-classes="top" + /> + + */ + export default { + components: { + loadingIcon, + icon, + }, + props: { + fileName: { + type: String, + required: true, + }, + + folder: { + type: Boolean, + required: false, + default: false, + }, + + opened: { + type: Boolean, + required: false, + default: false, + }, + + loading: { + type: Boolean, + required: false, + default: false, + }, + + size: { + type: Number, + required: false, + default: 16, + }, + + cssClasses: { + type: String, + required: false, + default: '', + }, + }, + computed: { + spriteHref() { + const iconName = getIconForFile(this.fileName) || 'file'; + return `${gon.sprite_file_icons}#${iconName}`; + }, + folderIconName() { + // We don't have a open folder icon yet + return this.opened ? 'folder' : 'folder'; + }, + iconSizeClass() { + return this.size ? `s${this.size}` : ''; + }, + }, + }; +</script> +<template> + <span> + <svg + :class="[iconSizeClass, cssClasses]" + v-if="!loading && !folder" + > + <use v-bind="{ 'xlink:href':spriteHref }" /> + </svg> + <icon + v-if="!loading && folder" + :name="folderIconName" + :size="size" + /> + <loading-icon + v-if="loading" + :inline="true" + /> + </span> +</template> diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js new file mode 100644 index 00000000000..9ffbaae3ea5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js @@ -0,0 +1,589 @@ +const fileExtensionIcons = { + html: 'html', + htm: 'html', + html_vm: 'html', + asp: 'html', + jade: 'pug', + pug: 'pug', + md: 'markdown', + 'md.rendered': 'markdown', + markdown: 'markdown', + 'markdown.rendered': 'markdown', + rst: 'markdown', + blink: 'blink', + css: 'css', + scss: 'sass', + sass: 'sass', + less: 'less', + json: 'json', + yaml: 'yaml', + 'YAML-tmLanguage': 'yaml', + yml: 'yaml', + xml: 'xml', + plist: 'xml', + xsd: 'xml', + dtd: 'xml', + xsl: 'xml', + xslt: 'xml', + resx: 'xml', + iml: 'xml', + xquery: 'xml', + tmLanguage: 'xml', + manifest: 'xml', + project: 'xml', + png: 'image', + jpeg: 'image', + jpg: 'image', + gif: 'image', + svg: 'image', + ico: 'image', + tif: 'image', + tiff: 'image', + psd: 'image', + psb: 'image', + ami: 'image', + apx: 'image', + bmp: 'image', + bpg: 'image', + brk: 'image', + cur: 'image', + dds: 'image', + dng: 'image', + exr: 'image', + fpx: 'image', + gbr: 'image', + img: 'image', + jbig2: 'image', + jb2: 'image', + jng: 'image', + jxr: 'image', + pbm: 'image', + pgf: 'image', + pic: 'image', + raw: 'image', + webp: 'image', + js: 'javascript', + ejs: 'javascript', + esx: 'javascript', + jsx: 'react', + tsx: 'react', + ini: 'settings', + dlc: 'settings', + dll: 'settings', + config: 'settings', + conf: 'settings', + properties: 'settings', + prop: 'settings', + settings: 'settings', + option: 'settings', + props: 'settings', + toml: 'settings', + prefs: 'settings', + 'sln.dotsettings': 'settings', + 'sln.dotsettings.user': 'settings', + ts: 'typescript', + 'd.ts': 'typescript-def', + marko: 'markojs', + pdf: 'pdf', + xlsx: 'table', + xls: 'table', + csv: 'table', + tsv: 'table', + vscodeignore: 'vscode', + vsixmanifest: 'vscode', + vsix: 'vscode', + 'code-workplace': 'vscode', + suo: 'visualstudio', + sln: 'visualstudio', + csproj: 'visualstudio', + vb: 'visualstudio', + pdb: 'database', + sql: 'database', + pks: 'database', + pkb: 'database', + accdb: 'database', + mdb: 'database', + sqlite: 'database', + cs: 'csharp', + zip: 'zip', + tar: 'zip', + gz: 'zip', + xz: 'zip', + bzip2: 'zip', + gzip: 'zip', + '7z': 'zip', + rar: 'zip', + tgz: 'zip', + exe: 'exe', + msi: 'exe', + java: 'java', + jar: 'java', + jsp: 'java', + c: 'c', + m: 'c', + h: 'h', + cc: 'cpp', + cpp: 'cpp', + mm: 'cpp', + cxx: 'cpp', + hpp: 'hpp', + go: 'go', + py: 'python', + url: 'url', + sh: 'console', + ksh: 'console', + csh: 'console', + tcsh: 'console', + zsh: 'console', + bash: 'console', + bat: 'console', + cmd: 'console', + ps1: 'powershell', + psm1: 'powershell', + psd1: 'powershell', + ps1xml: 'powershell', + psc1: 'powershell', + pssc: 'powershell', + gradle: 'gradle', + doc: 'word', + docx: 'word', + rtf: 'word', + cer: 'certificate', + cert: 'certificate', + crt: 'certificate', + pub: 'key', + key: 'key', + pem: 'key', + asc: 'key', + gpg: 'key', + woff: 'font', + woff2: 'font', + ttf: 'font', + eot: 'font', + suit: 'font', + otf: 'font', + bmap: 'font', + fnt: 'font', + odttf: 'font', + ttc: 'font', + font: 'font', + fonts: 'font', + sui: 'font', + ntf: 'font', + mrf: 'font', + lib: 'lib', + bib: 'lib', + rb: 'ruby', + erb: 'ruby', + fs: 'fsharp', + fsx: 'fsharp', + fsi: 'fsharp', + fsproj: 'fsharp', + swift: 'swift', + ino: 'arduino', + dockerignore: 'docker', + dockerfile: 'docker', + tex: 'tex', + cls: 'tex', + sty: 'tex', + pptx: 'powerpoint', + ppt: 'powerpoint', + pptm: 'powerpoint', + potx: 'powerpoint', + pot: 'powerpoint', + potm: 'powerpoint', + ppsx: 'powerpoint', + ppsm: 'powerpoint', + pps: 'powerpoint', + ppam: 'powerpoint', + ppa: 'powerpoint', + webm: 'movie', + mkv: 'movie', + flv: 'movie', + vob: 'movie', + ogv: 'movie', + ogg: 'movie', + gifv: 'movie', + avi: 'movie', + mov: 'movie', + qt: 'movie', + wmv: 'movie', + yuv: 'movie', + rm: 'movie', + rmvb: 'movie', + mp4: 'movie', + m4v: 'movie', + mpg: 'movie', + mp2: 'movie', + mpeg: 'movie', + mpe: 'movie', + mpv: 'movie', + m2v: 'movie', + vdi: 'virtual', + vbox: 'virtual', + 'vbox-prev': 'virtual', + ics: 'email', + mp3: 'music', + flac: 'music', + m4a: 'music', + wma: 'music', + aiff: 'music', + coffee: 'coffee', + txt: 'document', + graphql: 'graphql', + rs: 'rust', + raml: 'raml', + xaml: 'xaml', + hs: 'haskell', + kt: 'kotlin', + kts: 'kotlin', + patch: 'git', + lua: 'lua', + clj: 'clojure', + cljs: 'clojure', + groovy: 'groovy', + r: 'r', + rmd: 'r', + dart: 'dart', + as: 'actionscript', + mxml: 'mxml', + ahk: 'autohotkey', + swf: 'flash', + swc: 'swc', + cmake: 'cmake', + asm: 'assembly', + a51: 'assembly', + inc: 'assembly', + nasm: 'assembly', + s: 'assembly', + ms: 'assembly', + agc: 'assembly', + ags: 'assembly', + aea: 'assembly', + argus: 'assembly', + mitigus: 'assembly', + binsource: 'assembly', + vue: 'vue', + ml: 'ocaml', + mli: 'ocaml', + cmx: 'ocaml', + 'js.map': 'javascript-map', + 'css.map': 'css-map', + lock: 'lock', + hbs: 'handlebars', + mustache: 'handlebars', + pl: 'perl', + pm: 'perl', + hx: 'haxe', + 'spec.ts': 'test-ts', + 'test.ts': 'test-ts', + 'ts.snap': 'test-ts', + 'spec.tsx': 'test-jsx', + 'test.tsx': 'test-jsx', + 'tsx.snap': 'test-jsx', + 'spec.jsx': 'test-jsx', + 'test.jsx': 'test-jsx', + 'jsx.snap': 'test-jsx', + 'spec.js': 'test-js', + 'test.js': 'test-js', + 'js.snap': 'test-js', + 'routing.ts': 'angular-routing', + 'routing.js': 'angular-routing', + 'module.ts': 'angular', + 'module.js': 'angular', + 'ng-template': 'angular', + 'component.ts': 'angular-component', + 'component.js': 'angular-component', + 'guard.ts': 'angular-guard', + 'guard.js': 'angular-guard', + 'service.ts': 'angular-service', + 'service.js': 'angular-service', + 'pipe.ts': 'angular-pipe', + 'pipe.js': 'angular-pipe', + 'filter.js': 'angular-pipe', + 'directive.ts': 'angular-directive', + 'directive.js': 'angular-directive', + 'resolver.ts': 'angular-resolver', + 'resolver.js': 'angular-resolver', + pp: 'puppet', + ex: 'elixir', + exs: 'elixir', + ls: 'livescript', + erl: 'erlang', + twig: 'twig', + jl: 'julia', + elm: 'elm', + pure: 'purescript', + tpl: 'smarty', + styl: 'stylus', + re: 'reason', + rei: 'reason', + cmj: 'bucklescript', + merlin: 'merlin', + v: 'verilog', + vhd: 'verilog', + sv: 'verilog', + svh: 'verilog', + nb: 'mathematica', + wl: 'wolframlanguage', + wls: 'wolframlanguage', + njk: 'nunjucks', + nunjucks: 'nunjucks', + robot: 'robot', + sol: 'solidity', + au3: 'autoit', + haml: 'haml', + yang: 'yang', + tf: 'terraform', + 'tf.json': 'terraform', + tfvars: 'terraform', + tfstate: 'terraform', + 'blade.php': 'laravel', + 'inky.php': 'laravel', + applescript: 'applescript', + cake: 'cake', + feature: 'cucumber', + nim: 'nim', + nimble: 'nim', + apib: 'apiblueprint', + apiblueprint: 'apiblueprint', + tag: 'riot', + vfl: 'vfl', + kl: 'kl', + pcss: 'postcss', + sss: 'postcss', + todo: 'todo', + cfml: 'coldfusion', + cfc: 'coldfusion', + lucee: 'coldfusion', + cabal: 'cabal', + nix: 'nix', + slim: 'slim', + http: 'http', + rest: 'http', + rql: 'restql', + restql: 'restql', + kv: 'kivy', + graphcool: 'graphcool', + sbt: 'sbt', + 'reducer.ts': 'ngrx-reducer', + 'rootReducer.ts': 'ngrx-reducer', + 'state.ts': 'ngrx-state', + 'actions.ts': 'ngrx-actions', + 'effects.ts': 'ngrx-effects', + cr: 'crystal', + 'drone.yml': 'drone', + cu: 'cuda', + cuh: 'cuda', + log: 'log', +}; + +const fileNameIcons = { + '.jscsrc': 'json', + '.jshintrc': 'json', + 'tsconfig.json': 'json', + 'tslint.json': 'json', + 'composer.lock': 'json', + '.jsbeautifyrc': 'json', + '.esformatter': 'json', + 'cdp.pid': 'json', + '.htaccess': 'xml', + '.jshintignore': 'settings', + '.buildignore': 'settings', + makefile: 'settings', + '.mrconfig': 'settings', + '.yardopts': 'settings', + 'gradle.properties': 'gradle', + gradlew: 'gradle', + 'gradle-wrapper.properties': 'gradle', + license: 'certificate', + 'license.md': 'certificate', + 'license.md.rendered': 'certificate', + 'license.txt': 'certificate', + licence: 'certificate', + 'licence.md': 'certificate', + 'licence.md.rendered': 'certificate', + 'licence.txt': 'certificate', + dockerfile: 'docker', + 'docker-compose.yml': 'docker', + '.mailmap': 'email', + '.gitignore': 'git', + '.gitconfig': 'git', + '.gitattributes': 'git', + '.gitmodules': 'git', + '.gitkeep': 'git', + 'git-history': 'git', + '.Rhistory': 'r', + 'cmakelists.txt': 'cmake', + 'cmakecache.txt': 'cmake', + 'angular-cli.json': 'angular', + '.angular-cli.json': 'angular', + '.vfl': 'vfl', + '.kl': 'kl', + 'postcss.config.js': 'postcss', + '.postcssrc.js': 'postcss', + 'project.graphcool': 'graphcool', + 'webpack.js': 'webpack', + 'webpack.ts': 'webpack', + 'webpack.base.js': 'webpack', + 'webpack.base.ts': 'webpack', + 'webpack.config.js': 'webpack', + 'webpack.config.ts': 'webpack', + 'webpack.common.js': 'webpack', + 'webpack.common.ts': 'webpack', + 'webpack.config.common.js': 'webpack', + 'webpack.config.common.ts': 'webpack', + 'webpack.config.common.babel.js': 'webpack', + 'webpack.config.common.babel.ts': 'webpack', + 'webpack.dev.js': 'webpack', + 'webpack.dev.ts': 'webpack', + 'webpack.config.dev.js': 'webpack', + 'webpack.config.dev.ts': 'webpack', + 'webpack.config.dev.babel.js': 'webpack', + 'webpack.config.dev.babel.ts': 'webpack', + 'webpack.prod.js': 'webpack', + 'webpack.prod.ts': 'webpack', + 'webpack.server.js': 'webpack', + 'webpack.server.ts': 'webpack', + 'webpack.client.js': 'webpack', + 'webpack.client.ts': 'webpack', + 'webpack.config.server.js': 'webpack', + 'webpack.config.server.ts': 'webpack', + 'webpack.config.client.js': 'webpack', + 'webpack.config.client.ts': 'webpack', + 'webpack.config.production.babel.js': 'webpack', + 'webpack.config.production.babel.ts': 'webpack', + 'webpack.config.prod.babel.js': 'webpack', + 'webpack.config.prod.babel.ts': 'webpack', + 'webpack.config.prod.js': 'webpack', + 'webpack.config.prod.ts': 'webpack', + 'webpack.config.production.js': 'webpack', + 'webpack.config.production.ts': 'webpack', + 'webpack.config.staging.js': 'webpack', + 'webpack.config.staging.ts': 'webpack', + 'webpack.config.babel.js': 'webpack', + 'webpack.config.babel.ts': 'webpack', + 'webpack.config.base.babel.js': 'webpack', + 'webpack.config.base.babel.ts': 'webpack', + 'webpack.config.base.js': 'webpack', + 'webpack.config.base.ts': 'webpack', + 'webpack.config.staging.babel.js': 'webpack', + 'webpack.config.staging.babel.ts': 'webpack', + 'webpack.config.coffee': 'webpack', + 'webpack.config.test.js': 'webpack', + 'webpack.config.test.ts': 'webpack', + 'webpack.config.vendor.js': 'webpack', + 'webpack.config.vendor.ts': 'webpack', + 'webpack.config.vendor.production.js': 'webpack', + 'webpack.config.vendor.production.ts': 'webpack', + 'webpack.test.js': 'webpack', + 'webpack.test.ts': 'webpack', + 'webpack.dist.js': 'webpack', + 'webpack.dist.ts': 'webpack', + 'webpackfile.js': 'webpack', + 'webpackfile.ts': 'webpack', + 'ionic.config.json': 'ionic', + '.io-config.json': 'ionic', + 'gulpfile.js': 'gulp', + 'gulpfile.ts': 'gulp', + 'gulpfile.babel.js': 'gulp', + 'package.json': 'nodejs', + 'package-lock.json': 'nodejs', + '.nvmrc': 'nodejs', + '.npmignore': 'npm', + '.npmrc': 'npm', + '.yarnrc': 'yarn', + 'yarn.lock': 'yarn', + '.yarnclean': 'yarn', + '.yarn-integrity': 'yarn', + 'yarn-error.log': 'yarn', + 'androidmanifest.xml': 'android', + '.env': 'tune', + '.env.example': 'tune', + '.babelrc': 'babel', + 'contributing.md': 'contributing', + 'contributing.md.rendered': 'contributing', + 'readme.md': 'readme', + 'readme.md.rendered': 'readme', + changelog: 'changelog', + 'changelog.md': 'changelog', + 'changelog.md.rendered': 'changelog', + CREDITS: 'credits', + 'credits.txt': 'credits', + 'credits.md': 'credits', + 'credits.md.rendered': 'credits', + '.flowconfig': 'flow', + 'favicon.ico': 'favicon', + 'karma.conf.js': 'karma', + 'karma.conf.ts': 'karma', + 'karma.conf.coffee': 'karma', + 'karma.config.js': 'karma', + 'karma.config.ts': 'karma', + 'karma-main.js': 'karma', + 'karma-main.ts': 'karma', + '.bithoundrc': 'bithound', + 'appveyor.yml': 'appveyor', + '.travis.yml': 'travis', + 'protractor.conf.js': 'protractor', + 'protractor.conf.ts': 'protractor', + 'protractor.conf.coffee': 'protractor', + 'protractor.config.js': 'protractor', + 'protractor.config.ts': 'protractor', + 'fuse.js': 'fusebox', + procfile: 'heroku', + '.editorconfig': 'editorconfig', + '.gitlab-ci.yml': 'gitlab', + '.bowerrc': 'bower', + 'bower.json': 'bower', + '.eslintrc.js': 'eslint', + '.eslintrc.yaml': 'eslint', + '.eslintrc.yml': 'eslint', + '.eslintrc.json': 'eslint', + '.eslintrc': 'eslint', + '.eslintignore': 'eslint', + 'code_of_conduct.md': 'conduct', + 'code_of_conduct.md.rendered': 'conduct', + '.watchmanconfig': 'watchman', + 'aurelia.json': 'aurelia', + 'mocha.opts': 'mocha', + jenkinsfile: 'jenkins', + 'firebase.json': 'firebase', + '.firebaserc': 'firebase', + 'rollup.config.js': 'rollup', + 'rollup.config.ts': 'rollup', + 'rollup-config.js': 'rollup', + 'rollup-config.ts': 'rollup', + 'rollup.config.prod.js': 'rollup', + 'rollup.config.prod.ts': 'rollup', + 'rollup.config.dev.js': 'rollup', + 'rollup.config.dev.ts': 'rollup', + 'rollup.config.prod.vendor.js': 'rollup', + 'rollup.config.prod.vendor.ts': 'rollup', + '.hhconfig': 'hack', + '.stylelintrc': 'stylelint', + 'stylelint.config.js': 'stylelint', + '.stylelintrc.json': 'stylelint', + '.stylelintrc.yaml': 'stylelint', + '.stylelintrc.yml': 'stylelint', + '.stylelintrc.js': 'stylelint', + '.stylelintignore': 'stylelint', + '.codeclimate.yml': 'code-climate', + '.prettierrc': 'prettier', + 'prettier.config.js': 'prettier', + '.prettierrc.js': 'prettier', + '.prettierrc.json': 'prettier', + '.prettierrc.yaml': 'prettier', + '.prettierrc.yml': 'prettier', + 'nodemon.json': 'nodemon', + '.sonarrc': 'sonar', + browserslist: 'browserlist', + '.browserslistrc': 'browserlist', + '.snyk': 'snyk', + '.drone.yml': 'drone', +}; + +export default function getIconForFile(name) { + return fileNameIcons[name] || + fileExtensionIcons[name ? name.split('.').pop() : ''] || + ''; +} 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 d305bd6acdc..1f72dea1b33 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -1,75 +1,78 @@ <script> -import ciIconBadge from './ci_badge_link.vue'; -import loadingIcon from './loading_icon.vue'; -import timeagoTooltip from './time_ago_tooltip.vue'; -import tooltip from '../directives/tooltip'; -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 - */ -export default { - props: { - status: { - type: Object, - required: true, - }, - itemName: { - type: String, - required: true, - }, - itemId: { - type: Number, - required: true, + import ciIconBadge from './ci_badge_link.vue'; + import loadingIcon from './loading_icon.vue'; + import timeagoTooltip from './time_ago_tooltip.vue'; + import tooltip from '../directives/tooltip'; + 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 + */ + export default { + components: { + ciIconBadge, + loadingIcon, + timeagoTooltip, + userAvatarImage, }, - time: { - type: String, - required: true, + directives: { + tooltip, }, - user: { - type: Object, - required: false, - default: () => ({}), + props: { + status: { + type: Object, + required: true, + }, + itemName: { + type: String, + required: true, + }, + itemId: { + type: Number, + required: true, + }, + time: { + type: String, + required: true, + }, + user: { + type: Object, + required: false, + default: () => ({}), + }, + actions: { + type: Array, + required: false, + default: () => [], + }, + hasSidebarButton: { + type: Boolean, + required: false, + default: false, + }, + shouldRenderTriggeredLabel: { + type: Boolean, + required: false, + default: true, + }, }, - actions: { - type: Array, - required: false, - default: () => [], - }, - hasSidebarButton: { - type: Boolean, - required: false, - default: false, - }, - }, - - directives: { - tooltip, - }, - components: { - ciIconBadge, - loadingIcon, - timeagoTooltip, - userAvatarImage, - }, - - computed: { - userAvatarAltText() { - return `${this.user.name}'s avatar`; + computed: { + userAvatarAltText() { + return `${this.user.name}'s avatar`; + }, }, - }, - methods: { - onClickAction(action) { - this.$emit('actionClicked', action); + methods: { + onClickAction(action) { + this.$emit('actionClicked', action); + }, }, - }, -}; + }; </script> <template> @@ -79,10 +82,15 @@ export default { <ci-icon-badge :status="status" /> <strong> - {{itemName}} #{{itemId}} + {{ itemName }} #{{ itemId }} </strong> - triggered + <template v-if="shouldRenderTriggeredLabel"> + triggered + </template> + <template v-else> + created + </template> <timeago-tooltip :time="time" /> @@ -93,16 +101,17 @@ export default { v-tooltip :href="user.path" :title="user.email" - class="js-user-link commit-committer-link"> + class="js-user-link commit-committer-link" + > <user-avatar-image :img-src="user.avatar_url" :img-alt="userAvatarAltText" :tooltip-text="user.name" :img-size="24" - /> + /> - {{user.name}} + {{ user.name }} </a> </template> </section> @@ -111,12 +120,15 @@ export default { class="header-action-buttons" v-if="actions.length"> <template - v-for="action in actions"> + v-for="(action, i) in actions" + > <a v-if="action.type === 'link'" :href="action.path" - :class="action.cssClass"> - {{action.label}} + :class="action.cssClass" + :key="i" + > + {{ action.label }} </a> <a @@ -124,8 +136,10 @@ export default { :href="action.path" data-method="post" rel="nofollow" - :class="action.cssClass"> - {{action.label}} + :class="action.cssClass" + :key="i" + > + {{ action.label }} </a> <button @@ -133,25 +147,31 @@ export default { @click="onClickAction(action)" :disabled="action.isLoading" :class="action.cssClass" - type="button"> - {{action.label}} + type="button" + :key="i" + > + {{ action.label }} <i v-show="action.isLoading" class="fa fa-spin fa-spinner" - aria-hidden="true"> + aria-hidden="true" + > </i> </button> </template> <button v-if="hasSidebarButton" type="button" - class="btn btn-default visible-xs-block visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header" + class="btn btn-default visible-xs-block +visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header" aria-label="Toggle Sidebar" - id="toggleSidebar"> + id="toggleSidebar" + > <i class="fa fa-angle-double-left" aria-hidden="true" - aria-labelledby="toggleSidebar"> + aria-labelledby="toggleSidebar" + > </i> </button> </section> diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue index 365229ea274..6a2e05000e1 100644 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -1,17 +1,17 @@ <script> -/* This is a re-usable vue component for rendering a svg sprite - icon + /* This is a re-usable vue component for rendering a svg sprite + icon - Sample configuration: + Sample configuration: - <icon - name="retry" - :size="32" - css-classes="top" - /> + <icon + name="retry" + :size="32" + css-classes="top" + /> -*/ + */ // only allow classes in images.scss e.g. s12 const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; @@ -80,7 +80,6 @@ :height="height" :x="x" :y="y"> - <use - v-bind="{'xlink:href':spriteHref}"/> + <use v-bind="{ 'xlink:href':spriteHref }" /> </svg> </template> diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue index 7cf2e029cf6..0a30f467b08 100644 --- a/app/assets/javascripts/vue_shared/components/identicon.vue +++ b/app/assets/javascripts/vue_shared/components/identicon.vue @@ -46,6 +46,6 @@ export default { class="avatar identicon" :class="sizeClass" :style="identiconStyles"> - {{identiconTitle}} + {{ identiconTitle }} </div> </template> 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 564fc5029af..b48828ae81f 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue @@ -1,7 +1,10 @@ <script> - import Icon from '../../../vue_shared/components/icon.vue'; + import icon from '../../../vue_shared/components/icon.vue'; export default { + components: { + icon, + }, props: { isLocked: { type: Boolean, @@ -16,10 +19,6 @@ }, }, - components: { - Icon, - }, - computed: { warningIcon() { if (this.isConfidential) return 'eye-slash'; @@ -37,16 +36,17 @@ <template> <div class="issuable-note-warning"> <icon - :name="warningIcon" - :size="16" - class="icon inline" - aria-hidden="true" - v-if="!isLockedAndConfidential"> - </icon> + :name="warningIcon" + :size="16" + class="icon inline" + aria-hidden="true" + v-if="!isLockedAndConfidential" + /> <span v-if="isLockedAndConfidential"> {{ __('This issue is confidential and locked.') }} - {{ __('People without permission will never get a notification and won\'t be able to comment.') }} + {{ __(`People without permission will never +get a notification and won't be able to comment.`) }} </span> <span v-else-if="isConfidential"> diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index 247943f83e6..ff8c0f7c1d2 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -1,55 +1,56 @@ <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. - This can also be used for initial page load when you don't - know the action of the button yet by setting - `loading: true, label: undefined`. + /* 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. + This can also be used for initial page load when you don't + know the action of the button yet by setting + `loading: true, label: undefined`. - Sample configuration: + Sample configuration: - <loading-button - :loading="true" - :label="Hello" - @click="..." - /> + <loading-button + :loading="true" + :label="Hello" + @click="..." + /> -*/ + */ -import loadingIcon from './loading_icon.vue'; + import loadingIcon from './loading_icon.vue'; -export default { - props: { - loading: { - type: Boolean, - required: false, - default: false, + export default { + components: { + loadingIcon, }, - disabled: { - type: Boolean, - required: false, - default: false, + props: { + loading: { + type: Boolean, + required: false, + default: false, + }, + disabled: { + type: Boolean, + required: false, + default: false, + }, + label: { + type: String, + required: false, + }, + containerClass: { + type: String, + required: false, + default: 'btn btn-align-content', + }, }, - label: { - type: String, - required: false, + methods: { + onClick(e) { + this.$emit('click', e); + }, }, - containerClass: { - type: String, - required: false, - default: 'btn btn-align-content', - }, - }, - components: { - loadingIcon, - }, - methods: { - onClick(e) { - this.$emit('click', e); - }, - }, -}; + }; </script> <template> @@ -59,23 +60,23 @@ export default { :class="containerClass" :disabled="loading || disabled" > - <transition name="fade"> - <loading-icon - v-if="loading" - :inline="true" - class="js-loading-button-icon" - :class="{ - 'append-right-5': label - }" - /> - </transition> - <transition name="fade"> - <span - v-if="label" - class="js-loading-button-label" - > - {{ label }} - </span> - </transition> + <transition name="fade"> + <loading-icon + v-if="loading" + :inline="true" + class="js-loading-button-icon" + :class="{ + 'append-right-5': label + }" + /> + </transition> + <transition name="fade"> + <span + v-if="label" + class="js-loading-button-label" + > + {{ label }} + </span> + </transition> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/loading_icon.vue b/app/assets/javascripts/vue_shared/components/loading_icon.vue index 15581d5c2a0..12a75e016d7 100644 --- a/app/assets/javascripts/vue_shared/components/loading_icon.vue +++ b/app/assets/javascripts/vue_shared/components/loading_icon.vue @@ -32,13 +32,14 @@ </script> <template> <component - :is="this.rootElementType" - class="text-center"> + :is="rootElementType" + class="loading-container text-center"> <i class="fa fa-spin fa-spinner" :class="cssClass" aria-hidden="true" - :aria-label="label"> + :aria-label="label" + > </i> </component> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 15e3d713448..1371dca0c35 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -6,6 +6,11 @@ import icon from '../icon.vue'; export default { + components: { + markdownHeader, + markdownToolbar, + icon, + }, props: { markdownPreviewPath: { type: String, @@ -24,6 +29,7 @@ quickActionsDocsPath: { type: String, required: false, + default: '', }, canAttachFile: { type: Boolean, @@ -45,17 +51,24 @@ previewMarkdown: false, }; }, - components: { - markdownHeader, - markdownToolbar, - icon, - }, computed: { shouldShowReferencedUsers() { const referencedUsersThreshold = 10; return this.referencedUsers.length >= referencedUsersThreshold; }, }, + mounted() { + /* + GLForm class handles all the toolbar buttons + */ + return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete); + }, + beforeDestroy() { + const glForm = $(this.$refs['gl-form']).data('gl-form'); + if (glForm) { + glForm.destroy(); + } + }, methods: { showPreviewTab() { if (this.previewMarkdown) return; @@ -98,18 +111,6 @@ }); }, }, - mounted() { - /* - GLForm class handles all the toolbar buttons - */ - return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete); - }, - beforeDestroy() { - const glForm = $(this.$refs['gl-form']).data('gl-form'); - if (glForm) { - glForm.destroy(); - } - }, }; </script> @@ -121,34 +122,39 @@ <markdown-header :preview-markdown="previewMarkdown" @preview-markdown="showPreviewTab" - @write-markdown="showWriteTab" /> + @write-markdown="showWriteTab" + /> <div class="md-write-holder" - v-show="!previewMarkdown"> + v-show="!previewMarkdown" + > <div class="zen-backdrop"> <slot name="textarea"></slot> <a class="zen-control zen-control-leave js-zen-leave" href="#" - aria-label="Enter zen mode"> + aria-label="Enter zen mode" + > <icon name="screen-normal" - :size="32"> - </icon> + :size="32" + /> </a> <markdown-toolbar :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" :can-attach-file="canAttachFile" - /> + /> </div> </div> <div class="md md-preview-holder md-preview" - v-show="previewMarkdown"> + v-show="previewMarkdown" + > <div ref="markdown-preview" - v-html="markdownPreview"> + v-html="markdownPreview" + > </div> <span v-if="markdownPreviewLoading"> Loading... @@ -158,23 +164,27 @@ <div v-if="referencedCommands" v-html="referencedCommands" - class="referenced-commands"></div> + class="referenced-commands" + > + </div> <div v-if="shouldShowReferencedUsers" - class="referenced-users"> - <span> - <i - class="fa fa-exclamation-triangle" - aria-hidden="true"> - </i> - You are about to add - <strong> - <span class="js-referenced-users-count"> - {{referencedUsers.length}} - </span> - </strong> people to the discussion. Proceed with caution. - </span> - </div> + class="referenced-users" + > + <span> + <i + class="fa fa-exclamation-triangle" + aria-hidden="true" + > + </i> + You are about to add + <strong> + <span class="js-referenced-users-count"> + {{ referencedUsers.length }} + </span> + </strong> people to the discussion. Proceed with caution. + </span> + </div> </template> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 36d2d1dc164..f65eab11a27 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -4,18 +4,26 @@ import icon from '../icon.vue'; export default { + directives: { + tooltip, + }, + components: { + toolbarButton, + icon, + }, props: { previewMarkdown: { type: Boolean, required: true, }, }, - directives: { - tooltip, + mounted() { + $(document).on('markdown-preview:show.vue', this.previewMarkdownTab); + $(document).on('markdown-preview:hide.vue', this.writeMarkdownTab); }, - components: { - toolbarButton, - icon, + beforeDestroy() { + $(document).off('markdown-preview:show.vue', this.previewMarkdownTab); + $(document).off('markdown-preview:hide.vue', this.writeMarkdownTab); }, methods: { isMarkdownForm(form) { @@ -36,14 +44,6 @@ this.$emit('write-markdown'); }, }, - mounted() { - $(document).on('markdown-preview:show.vue', this.previewMarkdownTab); - $(document).on('markdown-preview:hide.vue', this.writeMarkdownTab); - }, - beforeDestroy() { - $(document).off('markdown-preview:show.vue', this.previewMarkdownTab); - $(document).off('markdown-preview:hide.vue', this.writeMarkdownTab); - }, }; </script> @@ -52,12 +52,14 @@ <ul class="nav-links clearfix"> <li class="md-header-tab" - :class="{ active: !previewMarkdown }"> + :class="{ active: !previewMarkdown }" + > <a class="js-write-link" href="#md-write-holder" tabindex="-1" - @click.prevent="writeMarkdownTab($event)"> + @click.prevent="writeMarkdownTab($event)" + > Write </a> </li> @@ -68,46 +70,55 @@ class="js-preview-link" href="#md-preview-holder" tabindex="-1" - @click.prevent="previewMarkdownTab($event)"> + @click.prevent="previewMarkdownTab($event)" + > Preview </a> </li> <li class="md-header-toolbar" - :class="{ active: !previewMarkdown }"> + :class="{ active: !previewMarkdown }" + > <toolbar-button tag="**" button-title="Add bold text" - icon="bold" /> + icon="bold" + /> <toolbar-button tag="*" button-title="Add italic text" - icon="italic" /> + icon="italic" + /> <toolbar-button tag="> " :prepend="true" button-title="Insert a quote" - icon="quote" /> + icon="quote" + /> <toolbar-button tag="`" tag-block="```" button-title="Insert code" - icon="code" /> + icon="code" + /> <toolbar-button tag="* " :prepend="true" button-title="Add a bullet list" - icon="list-bulleted" /> + icon="list-bulleted" + /> <toolbar-button tag="1. " :prepend="true" button-title="Add a numbered list" - icon="list-numbered" /> + icon="list-numbered" + /> <toolbar-button tag="* [ ] " :prepend="true" button-title="Add a task list" - icon="task-done" /> + icon="task-done" + /> <button v-tooltip aria-label="Go full screen" @@ -115,10 +126,11 @@ data-container="body" tabindex="-1" title="Go full screen" - type="button"> + type="button" + > <icon - name="screen-full"> - </icon> + name="screen-full" + /> </button> </li> </ul> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index ea2509d2839..c0ee88bbf72 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -8,6 +8,7 @@ quickActionsDocsPath: { type: String, required: false, + default: '', }, canAttachFile: { type: Boolean, @@ -15,32 +16,40 @@ default: true, }, }, + computed: { + hasQuickActionsDocsPath() { + return this.quickActionsDocsPath !== ''; + }, + }, }; </script> <template> <div class="comment-toolbar clearfix"> <div class="toolbar-text"> - <template v-if="!quickActionsDocsPath && markdownDocsPath"> + <template v-if="!hasQuickActionsDocsPath && markdownDocsPath"> <a :href="markdownDocsPath" target="_blank" - tabindex="-1"> + tabindex="-1" + > Markdown is supported </a> </template> - <template v-if="quickActionsDocsPath && markdownDocsPath"> - <a + <template v-if="hasQuickActionsDocsPath && markdownDocsPath"> + <a :href="markdownDocsPath" target="_blank" - tabindex="-1"> + tabindex="-1" + > Markdown </a> and - <a + <a :href="quickActionsDocsPath" target="_blank" - tabindex="-1"> + tabindex="-1" + > quick actions </a> are supported @@ -53,46 +62,58 @@ <span class="uploading-progress-container hide"> <i class="fa fa-file-image-o toolbar-button-icon" - aria-hidden="true"></i> + aria-hidden="true" + > + </i> <span class="attaching-file-message"></span> <span class="uploading-progress">0%</span> <span class="uploading-spinner"> <i class="fa fa-spinner fa-spin toolbar-button-icon" - aria-hidden="true"></i> + aria-hidden="true" + > + </i> </span> </span> <span class="uploading-error-container hide"> <span class="uploading-error-icon"> <i class="fa fa-file-image-o toolbar-button-icon" - aria-hidden="true"></i> + aria-hidden="true" + > + </i> </span> <span class="uploading-error-message"></span> <button class="retry-uploading-link" - type="button"> - Try again + type="button" + > + Try again </button> or <button class="attach-new-file markdown-selector" - type="button"> + type="button" + > attach a new file </button> </span> <button class="markdown-selector button-attach-file" tabindex="-1" - type="button"> + type="button" + > <i class="fa fa-file-image-o toolbar-button-icon" - aria-hidden="true"></i> + aria-hidden="true" + > + </i> Attach a file </button> <button class="btn btn-default btn-xs hide button-cancel-uploading-files" - type="button"> + type="button" + > Cancel </button> </span> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index e3e41f8f0ca..2d2d69ebeb2 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -3,6 +3,12 @@ import icon from '../icon.vue'; export default { + components: { + icon, + }, + directives: { + tooltip, + }, props: { buttonTitle: { type: String, @@ -27,12 +33,6 @@ default: false, }, }, - components: { - icon, - }, - directives: { - tooltip, - }, }; </script> @@ -47,9 +47,10 @@ :data-md-block="tagBlock" :data-md-prepend="prepend" :title="buttonTitle" - :aria-label="buttonTitle"> + :aria-label="buttonTitle" + > <icon - :name="icon"> - </icon> + :name="icon" + /> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/modal.vue b/app/assets/javascripts/vue_shared/components/modal.vue index 55f466b7b41..8227428d8ba 100644 --- a/app/assets/javascripts/vue_shared/components/modal.vue +++ b/app/assets/javascripts/vue_shared/components/modal.vue @@ -1,131 +1,153 @@ <script> -export default { - name: 'modal', + /* eslint-disable vue/require-default-prop */ + export default { + name: 'Modal', - props: { - title: { - type: String, - required: false, + props: { + id: { + type: String, + required: false, + }, + title: { + type: String, + required: false, + }, + text: { + type: String, + required: false, + }, + hideFooter: { + type: Boolean, + required: false, + default: false, + }, + kind: { + type: String, + required: false, + default: 'primary', + }, + modalDialogClass: { + type: String, + required: false, + default: '', + }, + closeKind: { + type: String, + required: false, + default: 'default', + }, + closeButtonLabel: { + type: String, + required: false, + default: 'Cancel', + }, + primaryButtonLabel: { + type: String, + required: false, + default: '', + }, + submitDisabled: { + type: Boolean, + required: false, + default: false, + }, }, - text: { - type: String, - required: false, - }, - hideFooter: { - type: Boolean, - required: false, - default: false, - }, - kind: { - type: String, - required: false, - default: 'primary', - }, - modalDialogClass: { - type: String, - required: false, - default: '', - }, - closeKind: { - type: String, - required: false, - default: 'default', - }, - closeButtonLabel: { - type: String, - required: false, - default: 'Cancel', - }, - primaryButtonLabel: { - type: String, - required: false, - default: '', - }, - submitDisabled: { - type: Boolean, - required: false, - default: false, - }, - }, - computed: { - btnKindClass() { - return { - [`btn-${this.kind}`]: true, - }; + computed: { + btnKindClass() { + return { + [`btn-${this.kind}`]: true, + }; + }, + btnCancelKindClass() { + return { + [`btn-${this.closeKind}`]: true, + }; + }, }, - btnCancelKindClass() { - return { - [`btn-${this.closeKind}`]: true, - }; - }, - }, - methods: { - close() { - this.$emit('toggle', false); - }, - emitSubmit(status) { - this.$emit('submit', status); + methods: { + emitCancel(event) { + this.$emit('cancel', event); + }, + emitSubmit(event) { + this.$emit('submit', event); + }, }, - }, -}; + }; </script> <template> -<div class="modal-open"> - <div - class="modal show" - role="dialog" - tabindex="-1" - > + <div class="modal-open"> <div - :class="modalDialogClass" - class="modal-dialog" - role="document" + :id="id" + class="modal" + :class="id ? '' : 'show'" + role="dialog" + tabindex="-1" > - <div class="modal-content"> - <div class="modal-header"> - <slot name="header"> - <h4 class="modal-title pull-left"> - {{this.title}} - </h4> + <div + :class="modalDialogClass" + class="modal-dialog" + role="document" + > + <div class="modal-content"> + <div class="modal-header"> + <slot name="header"> + <h4 class="modal-title pull-left"> + {{ title }} + </h4> + <button + type="button" + class="close pull-right" + @click="emitCancel($event)" + data-dismiss="modal" + aria-label="Close" + > + <span aria-hidden="true">×</span> + </button> + </slot> + </div> + <div class="modal-body"> + <slot + name="body" + :text="text" + > + <p>{{ text }}</p> + </slot> + </div> + <div + class="modal-footer" + v-if="!hideFooter" + > <button type="button" - class="close pull-right" - @click="close" - aria-label="Close" + class="btn" + :class="btnCancelKindClass" + @click="emitCancel($event)" + data-dismiss="modal" > - <span aria-hidden="true">×</span> + {{ closeButtonLabel }} </button> - </slot> - </div> - <div class="modal-body"> - <slot name="body" :text="text"> - <p>{{this.text}}</p> - </slot> - </div> - <div class="modal-footer" v-if="!hideFooter"> - <button - type="button" - class="btn pull-left" - :class="btnCancelKindClass" - @click="close"> - {{ closeButtonLabel }} - </button> - <button - v-if="primaryButtonLabel" - type="button" - class="btn pull-right js-primary-button" - :disabled="submitDisabled" - :class="btnKindClass" - @click="emitSubmit(true)"> - {{ primaryButtonLabel }} - </button> + <button + v-if="primaryButtonLabel" + type="button" + class="btn js-primary-button" + :disabled="submitDisabled" + :class="btnKindClass" + @click="emitSubmit($event)" + data-dismiss="modal" + > + {{ primaryButtonLabel }} + </button> + </div> </div> </div> </div> + <div + v-if="!id" + class="modal-backdrop fade in" + > + </div> </div> - <div class="modal-backdrop fade in" /> -</div> </template> diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue index a2ddd565170..63d8329e495 100644 --- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue @@ -45,31 +45,30 @@ this.$emit('onChangeTab', tab.scope); }, }, -}; + }; </script> <template> - <ul class="nav-links scrolling-tabs"> + <ul class="nav-links scrolling-tabs separator"> <li v-for="(tab, i) in tabs" :key="i" :class="{ active: tab.isActive, }" - > + > <a role="button" @click="onTabClick(tab)" :class="`js-${scope}-tab-${tab.scope}`" - > + > {{ tab.name }} <span v-if="shouldRenderBadge(tab.count)" class="badge" - > - {{tab.count}} + > + {{ tab.count }} </span> - </a> </li> </ul> diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index e467ca56704..50b1508691b 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -20,16 +20,16 @@ import userAvatarLink from '../user_avatar/user_avatar_link.vue'; export default { - name: 'placeholderNote', + name: 'PlaceholderNote', + components: { + userAvatarLink, + }, props: { note: { type: Object, required: true, }, }, - components: { - userAvatarLink, - }, computed: { ...mapGetters([ 'getUserData', @@ -46,7 +46,7 @@ :link-href="getUserData.path" :img-src="getUserData.avatar_url" :img-size="40" - /> + /> </div> <div :class="{ discussion: !note.individual_note }" @@ -54,14 +54,14 @@ <div class="note-header"> <div class="note-header-info"> <a :href="getUserData.path"> - <span class="hidden-xs">{{getUserData.name}}</span> - <span class="note-headline-light">@{{getUserData.username}}</span> + <span class="hidden-xs">{{ getUserData.name }}</span> + <span class="note-headline-light">@{{ getUserData.username }}</span> </a> </div> </div> <div class="note-body"> <div class="note-text"> - <p>{{note.body}}</p> + <p>{{ note.body }}</p> </div> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue index d805fea8006..95e2b38e292 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue @@ -8,7 +8,7 @@ * /> */ export default { - name: 'placeholderSystemNote', + name: 'PlaceholderSystemNote', props: { note: { type: Object, @@ -20,10 +20,10 @@ <template> <li class="note system-note timeline-entry being-posted fade-in-half"> - <div class="timeline-entry-inner"> - <div class="timeline-content"> - <em>{{note.body}}</em> - </div> - </div> + <div class="timeline-entry-inner"> + <div class="timeline-content"> + <em>{{ note.body }}</em> + </div> + </div> </li> </template> diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 2248699c399..aac10f84087 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -21,16 +21,16 @@ import { spriteIcon } from '../../../lib/utils/common_utils'; export default { - name: 'systemNote', + name: 'SystemNote', + components: { + noteHeader, + }, props: { note: { type: Object, required: true, }, }, - components: { - noteHeader, - }, computed: { ...mapGetters([ 'targetNoteHash', diff --git a/app/assets/javascripts/vue_shared/components/panel_resizer.vue b/app/assets/javascripts/vue_shared/components/panel_resizer.vue new file mode 100644 index 00000000000..abbe9a22717 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/panel_resizer.vue @@ -0,0 +1,91 @@ +<script> + export default { + props: { + startSize: { + type: Number, + required: true, + }, + side: { + type: String, + required: true, + }, + minSize: { + type: Number, + required: false, + default: 0, + }, + maxSize: { + type: Number, + required: false, + default: Number.MAX_VALUE, + }, + enabled: { + type: Boolean, + required: false, + default: true, + }, + }, + data() { + return { + size: this.startSize, + }; + }, + computed: { + className() { + return `drag${this.side}`; + }, + cursorStyle() { + if (this.enabled) { + return { cursor: 'ew-resize' }; + } + return {}; + }, + }, + methods: { + resetSize(e) { + e.preventDefault(); + this.size = this.startSize; + this.$emit('update:size', this.size); + }, + startDrag(e) { + if (this.enabled) { + e.preventDefault(); + this.startPos = e.clientX; + this.currentStartSize = this.size; + document.addEventListener('mousemove', this.drag); + document.addEventListener('mouseup', this.endDrag, { once: true }); + this.$emit('resize-start', this.size); + } + }, + drag(e) { + e.preventDefault(); + let moved = e.clientX - this.startPos; + if (this.side === 'left') moved = -moved; + let newSize = this.currentStartSize + moved; + if (newSize < this.minSize) { + newSize = this.minSize; + } else if (newSize > this.maxSize) { + newSize = this.maxSize; + } + this.size = newSize; + + this.$emit('update:size', newSize); + }, + endDrag(e) { + e.preventDefault(); + document.removeEventListener('mousemove', this.drag); + this.$emit('resize-end', this.size); + }, + }, + }; +</script> + +<template> + <div + class="dragHandle" + :class="className" + :style="cursorStyle" + @mousedown="startDrag" + @dblclick="resetSize" + ></div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue index d8d974a2ff7..bfeece12077 100644 --- a/app/assets/javascripts/vue_shared/components/pikaday.vue +++ b/app/assets/javascripts/vue_shared/components/pikaday.vue @@ -3,7 +3,7 @@ import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix'; export default { - name: 'datePicker', + name: 'DatePicker', props: { label: { type: String, @@ -13,22 +13,17 @@ selectedDate: { type: Date, required: false, + default: null, }, minDate: { type: Date, required: false, + default: null, }, maxDate: { type: Date, required: false, - }, - }, - methods: { - selected(dateText) { - this.$emit('newDateSelected', this.calendar.toString(dateText)); - }, - toggled() { - this.$emit('hidePicker'); + default: null, }, }, mounted() { @@ -53,6 +48,14 @@ beforeDestroy() { this.calendar.destroy(); }, + methods: { + selected(dateText) { + this.$emit('newDateSelected', this.calendar.toString(dateText)); + }, + toggled() { + this.$emit('hidePicker'); + }, + }, }; </script> @@ -66,7 +69,7 @@ @click="toggled" > <span class="dropdown-toggle-text"> - {{label}} + {{ label }} </span> <i class="fa fa-chevron-down" diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue index dce23bd65f6..279cc1de5bb 100644 --- a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue +++ b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue @@ -1,85 +1,85 @@ <script> -/* This is a re-usable vue component for rendering a project avatar that - does not need to link to the project's profile. The image and an optional - tooltip can be configured by props passed to this component. + /* This is a re-usable vue component for rendering a project avatar that + does not need to link to the project's profile. The image and an optional + tooltip can be configured by props passed to this component. - Sample configuration: + Sample configuration: - <project-avatar-image - :lazy="true" - :img-src="projectAvatarSrc" - :img-alt="tooltipText" - :tooltip-text="tooltipText" - tooltip-placement="top" - /> + <project-avatar-image + :lazy="true" + :img-src="projectAvatarSrc" + :img-alt="tooltipText" + :tooltip-text="tooltipText" + tooltip-placement="top" + /> -*/ + */ -import defaultAvatarUrl from 'images/no_avatar.png'; -import { placeholderImage } from '../../../lazy_loader'; -import tooltip from '../../directives/tooltip'; + import defaultAvatarUrl from 'images/no_avatar.png'; + import { placeholderImage } from '../../../lazy_loader'; + import tooltip from '../../directives/tooltip'; -export default { - name: 'ProjectAvatarImage', - props: { - lazy: { - type: Boolean, - required: false, - default: false, - }, - imgSrc: { - type: String, - required: false, - default: defaultAvatarUrl, - }, - cssClasses: { - type: String, - required: false, - default: '', - }, - imgAlt: { - type: String, - required: false, - default: 'project avatar', - }, - size: { - type: Number, - required: false, - default: 20, - }, - tooltipText: { - type: String, - required: false, - default: '', - }, - tooltipPlacement: { - type: String, - required: false, - default: 'top', - }, - }, - directives: { - tooltip, - }, - computed: { - // API response sends null when gravatar is disabled and - // we provide an empty string when we use it inside project avatar link. - // In both cases we should render the defaultAvatarUrl - sanitizedSource() { - return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc; - }, - resultantSrcAttribute() { - return this.lazy ? placeholderImage : this.sanitizedSource; + export default { + name: 'ProjectAvatarImage', + directives: { + tooltip, }, - tooltipContainer() { - return this.tooltipText ? 'body' : null; + props: { + lazy: { + type: Boolean, + required: false, + default: false, + }, + imgSrc: { + type: String, + required: false, + default: defaultAvatarUrl, + }, + cssClasses: { + type: String, + required: false, + default: '', + }, + imgAlt: { + type: String, + required: false, + default: 'project avatar', + }, + size: { + type: Number, + required: false, + default: 20, + }, + tooltipText: { + type: String, + required: false, + default: '', + }, + tooltipPlacement: { + type: String, + required: false, + default: 'top', + }, }, - avatarSizeClass() { - return `s${this.size}`; + computed: { + // API response sends null when gravatar is disabled and + // we provide an empty string when we use it inside project avatar link. + // In both cases we should render the defaultAvatarUrl + sanitizedSource() { + return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc; + }, + resultantSrcAttribute() { + return this.lazy ? placeholderImage : this.sanitizedSource; + }, + tooltipContainer() { + return this.tooltipText ? 'body' : null; + }, + avatarSizeClass() { + return `s${this.size}`; + }, }, - }, -}; + }; </script> <template> @@ -87,7 +87,7 @@ export default { v-tooltip class="avatar" :class="{ - lazy, + lazy: lazy, [avatarSizeClass]: true, [cssClasses]: true }" diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index 8053c65d498..c35621c9ef3 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -1,85 +1,86 @@ <script> -import modal from './modal.vue'; + import modal from './modal.vue'; -export default { - name: 'recaptcha-modal', + export default { + name: 'RecaptchaModal', - props: { - html: { - type: String, - required: false, - default: '', + components: { + modal, }, - }, - data() { - return { - script: {}, - scriptSrc: 'https://www.google.com/recaptcha/api.js', - }; - }, + props: { + html: { + type: String, + required: false, + default: '', + }, + }, - components: { - modal, - }, + data() { + return { + script: {}, + scriptSrc: 'https://www.google.com/recaptcha/api.js', + }; + }, - methods: { - appendRecaptchaScript() { - this.removeRecaptchaScript(); + watch: { + html() { + this.appendRecaptchaScript(); + }, + }, - const script = document.createElement('script'); - script.src = this.scriptSrc; - script.classList.add('js-recaptcha-script'); - script.async = true; - script.defer = true; + mounted() { + window.recaptchaDialogCallback = this.submit.bind(this); + }, - this.script = script; + methods: { + appendRecaptchaScript() { + this.removeRecaptchaScript(); - document.body.appendChild(script); - }, + const script = document.createElement('script'); + script.src = this.scriptSrc; + script.classList.add('js-recaptcha-script'); + script.async = true; + script.defer = true; - removeRecaptchaScript() { - if (this.script instanceof Element) this.script.remove(); - }, + this.script = script; - close() { - this.removeRecaptchaScript(); - this.$emit('close'); - }, + document.body.appendChild(script); + }, - submit() { - this.$el.querySelector('form').submit(); - }, - }, + removeRecaptchaScript() { + if (this.script instanceof Element) this.script.remove(); + }, - watch: { - html() { - this.appendRecaptchaScript(); - }, - }, + close() { + this.removeRecaptchaScript(); + this.$emit('close'); + }, - mounted() { - window.recaptchaDialogCallback = this.submit.bind(this); - }, -}; + submit() { + this.$el.querySelector('form').submit(); + }, + }, + }; </script> <template> -<modal - kind="warning" - class="recaptcha-modal js-recaptcha-modal" - :hide-footer="true" - :title="__('Please solve the reCAPTCHA')" - @toggle="close" -> - <div slot="body"> - <p> - {{__('We want to be sure it is you, please confirm you are not a robot.')}} - </p> - <div - ref="recaptcha" - v-html="html" - ></div> - </div> -</modal> + <modal + kind="warning" + class="recaptcha-modal js-recaptcha-modal" + :hide-footer="true" + :title="__('Please solve the reCAPTCHA')" + @cancel="close" + > + <div slot="body"> + <p> + {{ __('We want to be sure it is you, please confirm you are not a robot.') }} + </p> + <div + ref="recaptcha" + v-html="html" + > + </div> + </div> + </modal> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue index a88e1310131..7f1eb6bcec4 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue @@ -1,6 +1,6 @@ <script> export default { - name: 'collapsedCalendarIcon', + name: 'CollapsedCalendarIcon', props: { containerClass: { type: String, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue index 9ede5553bc5..dac438a702d 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue @@ -4,7 +4,11 @@ import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; export default { - name: 'sidebarCollapsedGroupedDatePicker', + name: 'SidebarCollapsedGroupedDatePicker', + components: { + toggleSidebar, + collapsedCalendarIcon, + }, props: { collapsed: { type: Boolean, @@ -19,10 +23,12 @@ minDate: { type: Date, required: false, + default: null, }, maxDate: { type: Date, required: false, + default: null, }, disableClickableIcons: { type: Boolean, @@ -30,10 +36,6 @@ default: false, }, }, - components: { - toggleSidebar, - collapsedCalendarIcon, - }, computed: { hasMinAndMaxDates() { return this.minDate && this.maxDate; 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 9c3413377a3..1413dd69f24 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -6,7 +6,13 @@ import { dateInWords } from '../../../lib/utils/datetime_utility'; export default { - name: 'sidebarDatePicker', + name: 'SidebarDatePicker', + components: { + datePicker, + toggleSidebar, + loadingIcon, + collapsedCalendarIcon, + }, props: { collapsed: { type: Boolean, @@ -36,14 +42,17 @@ selectedDate: { type: Date, required: false, + default: null, }, minDate: { type: Date, required: false, + default: null, }, maxDate: { type: Date, required: false, + default: null, }, }, data() { @@ -51,12 +60,6 @@ editing: false, }; }, - components: { - datePicker, - toggleSidebar, - loadingIcon, - collapsedCalendarIcon, - }, computed: { selectedAndEditable() { return this.selectedDate && this.editable; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue index 5ae76adad71..8211d425b1f 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue @@ -1,6 +1,6 @@ <script> export default { - name: 'toggleSidebar', + name: 'ToggleSidebar', props: { collapsed: { type: Boolean, @@ -24,7 +24,11 @@ <i aria-label="toggle collapse" class="fa" - :class="{ 'fa-angle-double-right': !collapsed, 'fa-angle-double-left': collapsed }" - ></i> + :class="{ + 'fa-angle-double-right': !collapsed, + 'fa-angle-double-left': collapsed + }" + > + </i> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue new file mode 100644 index 00000000000..86f06c8d266 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue @@ -0,0 +1,127 @@ +<script> +import tooltip from '~/vue_shared/directives/tooltip'; + +export default { + directives: { + tooltip, + }, + props: { + cssClass: { + type: String, + required: false, + default: '', + }, + successLabel: { + type: String, + required: true, + }, + failureLabel: { + type: String, + required: true, + }, + neutralLabel: { + type: String, + required: true, + }, + successCount: { + type: Number, + required: true, + }, + failureCount: { + type: Number, + required: true, + }, + totalCount: { + type: Number, + required: true, + }, + }, + computed: { + neutralCount() { + return this.totalCount - this.successCount - this.failureCount; + }, + successPercent() { + return this.getPercent(this.successCount); + }, + successBarStyle() { + return this.barStyle(this.successPercent); + }, + successTooltip() { + return this.getTooltip(this.successLabel, this.successCount); + }, + failurePercent() { + return this.getPercent(this.failureCount); + }, + failureBarStyle() { + return this.barStyle(this.failurePercent); + }, + failureTooltip() { + return this.getTooltip(this.failureLabel, this.failureCount); + }, + neutralPercent() { + return this.getPercent(this.neutralCount); + }, + neutralBarStyle() { + return this.barStyle(this.neutralPercent); + }, + neutralTooltip() { + return this.getTooltip(this.neutralLabel, this.neutralCount); + }, + }, + methods: { + getPercent(count) { + return Math.ceil((count / this.totalCount) * 100); + }, + barStyle(percent) { + return `width: ${percent}%;`; + }, + getTooltip(label, count) { + return `${label}: ${count}`; + }, + }, +}; +</script> + +<template> + <div + class="stacked-progress-bar" + :class="cssClass" + > + <span + v-if="!totalCount" + class="status-unavailable" + > + {{ __("Not available") }} + </span> + <span + v-tooltip + v-if="successPercent" + class="status-green" + data-placement="bottom" + :title="successTooltip" + :style="successBarStyle" + > + {{ successPercent }}% + </span> + <span + v-tooltip + v-if="neutralPercent" + class="status-neutral" + data-placement="bottom" + :title="neutralTooltip" + :style="neutralBarStyle" + > + {{ neutralPercent }}% + </span> + <span + v-tooltip + v-if="failurePercent" + class="status-red" + data-placement="bottom" + :title="failureTooltip" + :style="failureBarStyle" + > + {{ failurePercent }}% + </span> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue index 710452bb3d3..c44c606a8b2 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.vue +++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue @@ -1,135 +1,135 @@ <script> -import { s__ } from '../../locale'; - -const PAGINATION_UI_BUTTON_LIMIT = 4; -const UI_LIMIT = 6; -const SPREAD = '...'; -const PREV = s__('Pagination|Prev'); -const NEXT = s__('Pagination|Next'); -const FIRST = s__('Pagination|« First'); -const LAST = s__('Pagination|Last »'); - -export default { - props: { - /** - This function will take the information given by the pagination component - - Here is an example `change` method: - - change(pagenum) { - gl.utils.visitUrl(`?page=${pagenum}`); + import { s__ } from '../../locale'; + + const PAGINATION_UI_BUTTON_LIMIT = 4; + const UI_LIMIT = 6; + const SPREAD = '...'; + const PREV = s__('Pagination|Prev'); + const NEXT = s__('Pagination|Next'); + const FIRST = s__('Pagination|« First'); + const LAST = s__('Pagination|Last »'); + + export default { + props: { + /** + This function will take the information given by the pagination component + */ + change: { + type: Function, + required: true, }, - */ - change: { - type: Function, - required: true, - }, - /** - pageInfo will come from the headers of the API call - in the `.then` clause of the VueResource API call - there should be a function that contructs the pageInfo for this component - - This is an example: - - const pageInfo = headers => ({ - perPage: +headers['X-Per-Page'], - page: +headers['X-Page'], - total: +headers['X-Total'], - totalPages: +headers['X-Total-Pages'], - nextPage: +headers['X-Next-Page'], - previousPage: +headers['X-Prev-Page'], - }); - */ - pageInfo: { - type: Object, - required: true, - }, - }, - methods: { - changePage(e) { - if (e.target.parentElement.classList.contains('disabled')) return; - - const text = e.target.innerText; - const { totalPages, nextPage, previousPage } = this.pageInfo; - - switch (text) { - case SPREAD: - break; - case LAST: - this.change(totalPages); - break; - case NEXT: - this.change(nextPage); - break; - case PREV: - this.change(previousPage); - break; - case FIRST: - this.change(1); - break; - default: - this.change(+text); - break; - } - }, - }, - computed: { - prev() { - return this.pageInfo.previousPage; + /** + pageInfo will come from the headers of the API call + in the `.then` clause of the VueResource API call + there should be a function that contructs the pageInfo for this component + + This is an example: + + const pageInfo = headers => ({ + perPage: +headers['X-Per-Page'], + page: +headers['X-Page'], + total: +headers['X-Total'], + totalPages: +headers['X-Total-Pages'], + nextPage: +headers['X-Next-Page'], + previousPage: +headers['X-Prev-Page'], + }); + */ + pageInfo: { + type: Object, + required: true, + }, }, - next() { - return this.pageInfo.nextPage; + computed: { + prev() { + return this.pageInfo.previousPage; + }, + next() { + return this.pageInfo.nextPage; + }, + getItems() { + const total = this.pageInfo.totalPages; + const page = this.pageInfo.page; + const items = []; + + if (page > 1) { + items.push({ title: FIRST, first: true }); + } + + if (page > 1) { + items.push({ title: PREV, prev: true }); + } else { + items.push({ title: PREV, disabled: true, prev: true }); + } + + if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); + + const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); + const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); + + for (let i = start; i <= end; i += 1) { + const isActive = i === page; + items.push({ title: i, active: isActive, page: true }); + } + + if (total - page > PAGINATION_UI_BUTTON_LIMIT) { + items.push({ title: SPREAD, separator: true, page: true }); + } + + if (page === total) { + items.push({ title: NEXT, disabled: true, next: true }); + } else if (total - page >= 1) { + items.push({ title: NEXT, next: true }); + } + + if (total - page >= 1) { + items.push({ title: LAST, last: true }); + } + + return items; + }, + showPagination() { + return this.pageInfo.totalPages > 1; + }, }, - getItems() { - const total = this.pageInfo.totalPages; - const page = this.pageInfo.page; - const items = []; - - if (page > 1) { - items.push({ title: FIRST, first: true }); - } - - if (page > 1) { - items.push({ title: PREV, prev: true }); - } else { - items.push({ title: PREV, disabled: true, prev: true }); - } - - if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); - - const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); - const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); - - for (let i = start; i <= end; i += 1) { - const isActive = i === page; - items.push({ title: i, active: isActive, page: true }); - } - - if (total - page > PAGINATION_UI_BUTTON_LIMIT) { - items.push({ title: SPREAD, separator: true, page: true }); - } - - if (page === total) { - items.push({ title: NEXT, disabled: true, next: true }); - } else if (total - page >= 1) { - items.push({ title: NEXT, next: true }); - } - - if (total - page >= 1) { - items.push({ title: LAST, last: true }); - } - - return items; + methods: { + changePage(text, isDisabled) { + if (isDisabled) return; + + const { totalPages, nextPage, previousPage } = this.pageInfo; + + switch (text) { + case SPREAD: + break; + case LAST: + this.change(totalPages); + break; + case NEXT: + this.change(nextPage); + break; + case PREV: + this.change(previousPage); + break; + case FIRST: + this.change(1); + break; + default: + this.change(+text); + break; + } + }, }, - }, -}; + }; </script> <template> - <div class="gl-pagination"> + <div + v-if="showPagination" + class="gl-pagination" + > <ul class="pagination clearfix"> <li - v-for="item in getItems" + v-for="(item, index) in getItems" + :key="index" :class="{ page: item.page, 'js-previous-button': item.prev, @@ -139,8 +139,11 @@ export default { separator: item.separator, active: item.active, disabled: item.disabled - }"> - <a @click.prevent="changePage($event)">{{item.title}}</a> + }" + > + <a @click.prevent="changePage(item.title, item.disabled)"> + {{ item.title }} + </a> </li> </ul> </div> diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue index 3ff7f6e2c4e..bec4e7c99b6 100644 --- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue +++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue @@ -8,6 +8,12 @@ import '../../lib/utils/datetime_utility'; */ export default { + directives: { + tooltip, + }, + mixins: [ + timeagoMixin, + ], props: { time: { type: String, @@ -26,14 +32,6 @@ export default { default: '', }, }, - - mixins: [ - timeagoMixin, - ], - - directives: { - tooltip, - }, }; </script> <template> @@ -43,6 +41,6 @@ export default { :title="tooltipTitle(time)" :data-placement="tooltipPlacement" data-container="body"> - {{timeFormated(time)}} + {{ timeFormated(time) }} </time> </template> diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue index 4277d9281a0..09031d3ffa1 100644 --- a/app/assets/javascripts/vue_shared/components/toggle_button.vue +++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue @@ -9,15 +9,26 @@ const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF'); export default { + components: { + icon, + loadingIcon, + }, + + model: { + prop: 'value', + event: 'change', + }, + props: { name: { type: String, required: false, - default: '', + default: null, }, value: { type: Boolean, - required: true, + required: false, + default: null, }, disabledInput: { type: Boolean, @@ -31,16 +42,6 @@ }, }, - components: { - icon, - loadingIcon, - }, - - model: { - prop: 'value', - event: 'change', - }, - computed: { toggleIcon() { return this.value ? ICON_ON : ICON_OFF; @@ -61,6 +62,7 @@ <template> <label class="toggle-wrapper"> <input + v-if="name" type="hidden" :name="name" :value="value" diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue index 1ac61a3c39b..cc9cc46bb4c 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue @@ -22,6 +22,9 @@ import tooltip from '../../directives/tooltip'; export default { name: 'UserAvatarImage', + directives: { + tooltip, + }, props: { lazy: { type: Boolean, @@ -59,9 +62,6 @@ export default { default: 'top', }, }, - directives: { - tooltip, - }, computed: { // API response sends null when gravatar is disabled and // we provide an empty string when we use it inside user avatar link. @@ -87,7 +87,7 @@ export default { v-tooltip class="avatar" :class="{ - lazy, + lazy: lazy, [avatarSizeClass]: true, [cssClasses]: true }" diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue index dc32e783258..6955d164def 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -26,6 +26,9 @@ export default { components: { userAvatarImage, }, + directives: { + tooltip, + }, props: { linkHref: { type: String, @@ -76,9 +79,6 @@ export default { return this.shouldShowUsername ? '' : this.tooltipText; }, }, - directives: { - tooltip, - }, }; </script> @@ -98,6 +98,6 @@ export default { v-tooltip :title="tooltipText" :tooltip-placement="tooltipPlacement" - >{{username}}</span> + >{{ username }}</span> </a> </template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue index d2ff2ac006e..ef3b16edf5f 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue @@ -39,7 +39,7 @@ export default { :class="avatarSizeClass" :height="size" :width="size" - v-html="svg"> - </svg> + v-html="svg" + /> </template> diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index 06a86f3b94a..4592003f57e 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,5 +1,4 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len, class-methods-use-this */ -/* global Mousetrap */ // Zen Mode (full screen) textarea // @@ -8,9 +7,11 @@ import 'vendor/jquery.scrollTo'; import Dropzone from 'dropzone'; -import 'mousetrap'; +import Mousetrap from 'mousetrap'; import 'mousetrap/plugins/pause/mousetrap-pause'; +Dropzone.autoDiscover = false; + // // ### Events // |