diff options
author | Stan Hu <stanhu@gmail.com> | 2018-07-03 14:19:34 -0700 |
---|---|---|
committer | Stan Hu <stanhu@gmail.com> | 2018-07-03 14:19:34 -0700 |
commit | 5f362686ca20f2ff81f5c86a6f9be9b31177c62b (patch) | |
tree | aeacd872b39a9bf52856f328db6bdc04e5f60f01 /app | |
parent | f4f4e02564dcfa94ebf25e680a1778a5239d150d (diff) | |
parent | cd5789415b6e561564073693243e890e79596ed2 (diff) | |
download | gitlab-ce-5f362686ca20f2ff81f5c86a6f9be9b31177c62b.tar.gz |
Merge branch 'master' into sh-support-bitbucket-server-import
Diffstat (limited to 'app')
115 files changed, 1590 insertions, 1419 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index d62fae99c6b..0ca0e8f35dd 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -150,14 +150,15 @@ const Api = { }, // Return group projects list. Filtered by query - groupProjects(groupId, query, callback) { + groupProjects(groupId, query, options, callback) { const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId); + const defaults = { + search: query, + per_page: 20, + }; return axios .get(url, { - params: { - search: query, - per_page: 20, - }, + params: Object.assign({}, defaults, options), }) .then(({ data }) => callback(data)); }, diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js b/app/assets/javascripts/boards/filters/due_date_filters.js index 70132dbfa6f..9eaa0cd227d 100644 --- a/app/assets/javascripts/boards/filters/due_date_filters.js +++ b/app/assets/javascripts/boards/filters/due_date_filters.js @@ -1,8 +1,7 @@ -/* global dateFormat */ - import Vue from 'vue'; +import dateFormat from 'dateformat'; -Vue.filter('due-date', (value) => { +Vue.filter('due-date', value => { const date = new Date(value); return dateFormat(date, 'mmm d, yyyy', true); }); diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index deddb61ca31..eb0985e5603 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -3,6 +3,7 @@ import { mapState, mapGetters, mapActions } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; import { __ } from '~/locale'; import createFlash from '~/flash'; +import eventHub from '../../notes/event_hub'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import CompareVersions from './compare_versions.vue'; import ChangedFiles from './changed_files.vue'; @@ -62,7 +63,7 @@ export default { plainDiffPath: state => state.diffs.plainDiffPath, emailPatchPath: state => state.diffs.emailPatchPath, }), - ...mapGetters(['isParallelView']), + ...mapGetters(['isParallelView', 'isNotesFetched']), targetBranch() { return { branchName: this.targetBranchName, @@ -94,20 +95,36 @@ export default { this.adjustView(); }, shouldShow() { + // When the shouldShow property changed to true, the route is rendered for the first time + // and if we have the isLoading as true this means we didn't fetch the data + if (this.isLoading) { + this.fetchData(); + } + this.adjustView(); }, }, mounted() { this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath }); - this.fetchDiffFiles().catch(() => { - createFlash(__('Fetching diff files failed. Please reload the page to try again!')); - }); + + if (this.shouldShow) { + this.fetchData(); + } }, created() { this.adjustView(); }, methods: { ...mapActions(['setBaseConfig', 'fetchDiffFiles']), + fetchData() { + this.fetchDiffFiles().catch(() => { + createFlash(__('Something went wrong on our end. Please try again!')); + }); + + if (!this.isNotesFetched) { + eventHub.$emit('fetchNotesData'); + } + }, setActive(filePath) { this.activeFile = filePath; }, @@ -128,7 +145,7 @@ export default { </script> <template> - <div v-if="shouldShow"> + <div v-show="shouldShow"> <div v-if="isLoading" class="loading" diff --git a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue index f224b9dd246..b38d217fbe3 100644 --- a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue +++ b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue @@ -66,59 +66,61 @@ export default { @click="clearSearch" ></i> </div> - <ul> - <li - v-for="diffFile in filteredDiffFiles" - :key="diffFile.name" - > - <a - :href="`#${diffFile.fileHash}`" - :title="diffFile.newPath" - class="diff-changed-file" + <div class="dropdown-content"> + <ul> + <li + v-for="diffFile in filteredDiffFiles" + :key="diffFile.name" > - <icon - :name="fileChangedIcon(diffFile)" - :size="16" - :class="fileChangedClass(diffFile)" - class="diff-file-changed-icon append-right-8" - /> - <span class="diff-changed-file-content append-right-8"> - <strong - v-if="diffFile.blob && diffFile.blob.name" - class="diff-changed-file-name" - > - {{ diffFile.blob.name }} - </strong> - <strong - v-else - class="diff-changed-blank-file-name" - > - {{ s__('Diffs|No file name available') }} - </strong> - <span class="diff-changed-file-path prepend-top-5"> - {{ truncatedDiffPath(diffFile.blob.path) }} + <a + :href="`#${diffFile.fileHash}`" + :title="diffFile.newPath" + class="diff-changed-file" + > + <icon + :name="fileChangedIcon(diffFile)" + :size="16" + :class="fileChangedClass(diffFile)" + class="diff-file-changed-icon append-right-8" + /> + <span class="diff-changed-file-content append-right-8"> + <strong + v-if="diffFile.blob && diffFile.blob.name" + class="diff-changed-file-name" + > + {{ diffFile.blob.name }} + </strong> + <strong + v-else + class="diff-changed-blank-file-name" + > + {{ s__('Diffs|No file name available') }} + </strong> + <span class="diff-changed-file-path prepend-top-5"> + {{ truncatedDiffPath(diffFile.blob.path) }} + </span> </span> - </span> - <span class="diff-changed-stats"> - <span class="cgreen"> - +{{ diffFile.addedLines }} + <span class="diff-changed-stats"> + <span class="cgreen"> + +{{ diffFile.addedLines }} + </span> + <span class="cred"> + -{{ diffFile.removedLines }} + </span> </span> - <span class="cred"> - -{{ diffFile.removedLines }} - </span> - </span> - </a> - </li> + </a> + </li> - <li - v-show="filteredDiffFiles.length === 0" - class="dropdown-menu-empty-item" - > - <a> - {{ __('No files found') }} - </a> - </li> - </ul> + <li + v-show="filteredDiffFiles.length === 0" + class="dropdown-menu-empty-item" + > + <a> + {{ __('No files found') }} + </a> + </li> + </ul> + </div> </div> </span> </template> diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 6bad389f778..fba1d1af7cd 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -112,7 +112,11 @@ export default { }, methods: { handleToggle(e, checkTarget) { - if (!checkTarget || e.target === this.$refs.header) { + if ( + !checkTarget || + e.target === this.$refs.header || + (e.target.classList && e.target.classList.contains('diff-toggle-caret')) + ) { this.$emit('toggleFile'); } }, @@ -201,7 +205,7 @@ export default { <div v-if="!diffFile.submodule && addMergeRequestButtons" - class="file-actions d-none d-md-block" + class="file-actions d-none d-sm-block" > <template v-if="diffFile.blob && diffFile.blob.readableText" diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index bf188a44022..5e0fd5109bb 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -15,10 +15,6 @@ export const setBaseConfig = ({ commit }, options) => { commit(types.SET_BASE_CONFIG, { endpoint, projectPath }); }; -export const setLoadingState = ({ commit }, state) => { - commit(types.SET_LOADING, state); -}; - export const fetchDiffFiles = ({ state, commit }) => { commit(types.SET_LOADING, true); @@ -88,7 +84,6 @@ export const expandAllFiles = ({ commit }) => { export default { setBaseConfig, - setLoadingState, fetchDiffFiles, setInlineDiffViewType, setParallelDiffViewType, diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index b755458aa4b..a5af37e80b6 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,12 +1,12 @@ /* eslint-disable consistent-return, no-new */ import $ from 'jquery'; -import Flash from './flash'; import GfmAutoComplete from './gfm_auto_complete'; import { convertPermissionToBoolean } from './lib/utils/common_utils'; import GlFieldErrors from './gl_field_errors'; import Shortcuts from './shortcuts'; import SearchAutocomplete from './search_autocomplete'; +import performanceBar from './performance_bar'; function initSearch() { // Only when search form is present @@ -72,9 +72,7 @@ function initGFMInput() { function initPerformanceBar() { if (document.querySelector('#js-peek')) { - import('./performance_bar') - .then(m => new m.default({ container: '#js-peek' })) // eslint-disable-line new-cap - .catch(() => Flash('Error loading performance bar module')); + performanceBar({ container: '#js-peek' }); } } diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index 4164149dd06..17ea3bdb179 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -1,7 +1,6 @@ -/* global dateFormat */ - import $ from 'jquery'; import Pikaday from 'pikaday'; +import dateFormat from 'dateformat'; import { __ } from '~/locale'; import axios from './lib/utils/axios_utils'; import { timeFor } from './lib/utils/datetime_utility'; @@ -55,7 +54,7 @@ class DueDateSelect { format: 'yyyy-mm-dd', parse: dateString => parsePikadayDate(dateString), toString: date => pikadayToString(date), - onSelect: (dateText) => { + onSelect: dateText => { $dueDateInput.val(calendar.toString(dateText)); if (this.$dropdown.hasClass('js-issue-boards-due-date')) { @@ -73,7 +72,7 @@ class DueDateSelect { } initRemoveDueDate() { - this.$block.on('click', '.js-remove-due-date', (e) => { + this.$block.on('click', '.js-remove-due-date', e => { const calendar = this.$datePicker.data('pikaday'); e.preventDefault(); @@ -124,7 +123,8 @@ class DueDateSelect { this.$loading.fadeOut(); }; - gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update')) + gl.issueBoards.BoardsStore.detail.issue + .update(this.$dropdown.attr('data-issue-update')) .then(fadeOutLoader) .catch(fadeOutLoader); } @@ -147,17 +147,18 @@ class DueDateSelect { $('.js-remove-due-date-holder').toggleClass('hidden', selectedDateValue.length); - return axios.put(this.issueUpdateURL, this.datePayload) - .then(() => { - const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date'); - if (isDropdown) { - this.$dropdown.trigger('loaded.gl.dropdown'); - this.$dropdown.dropdown('toggle'); - } - this.$sidebarCollapsedValue.attr('data-original-title', tooltipText); + return axios.put(this.issueUpdateURL, this.datePayload).then(() => { + const tooltipText = hasDueDate + ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` + : __('Due date'); + if (isDropdown) { + this.$dropdown.trigger('loaded.gl.dropdown'); + this.$dropdown.dropdown('toggle'); + } + this.$sidebarCollapsedValue.attr('data-original-title', tooltipText); - return this.$loading.fadeOut(); - }); + return this.$loading.fadeOut(); + }); } } @@ -187,15 +188,19 @@ export default class DueDateSelectors { $datePicker.data('pikaday', calendar); }); - $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => { + $('.js-clear-due-date,.js-clear-start-date').on('click', e => { e.preventDefault(); - const calendar = $(e.target).siblings('.datepicker').data('pikaday'); + const calendar = $(e.target) + .siblings('.datepicker') + .data('pikaday'); calendar.setDate(null); }); } // eslint-disable-next-line class-methods-use-this initIssuableSelect() { - const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide(); + const $loading = $('.js-issuable-update .due_date') + .find('.block-loading') + .hide(); $('.js-due-date-select').each((i, dropdown) => { const $dropdown = $(dropdown); diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 09186a865e4..73b2cd0b2c7 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -12,7 +12,7 @@ export const defaultAutocompleteConfig = { members: true, issues: true, mergeRequests: true, - epics: false, + epics: true, milestones: true, labels: true, }; @@ -493,6 +493,7 @@ GfmAutoComplete.atTypeMap = { '@': 'members', '#': 'issues', '!': 'mergeRequests', + '&': 'epics', '~': 'labels', '%': 'milestones', '/': 'commands', diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index f802971a3ca..c74de7ac34d 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -9,6 +9,13 @@ export default class GLForm { this.form = form; this.textarea = this.form.find('textarea.js-gfm-input'); this.enableGFM = Object.assign({}, GFMConfig.defaultAutocompleteConfig, enableGFM); + // Disable autocomplete for keywords which do not have dataSources available + const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {}; + Object.keys(this.enableGFM).forEach(item => { + if (item !== 'emojis') { + this.enableGFM[item] = !!dataSources[item]; + } + }); // Before we start, we should clean up any previous data for this form this.destroy(); // Setup the form diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue index e7408264c80..acbc98b7a7b 100644 --- a/app/assets/javascripts/ide/components/error_message.vue +++ b/app/assets/javascripts/ide/components/error_message.vue @@ -24,8 +24,8 @@ export default { this.isLoading = true; - this.$store - .dispatch(this.message.action, this.message.actionPayload) + this.message + .action(this.message.actionPayload) .then(() => { this.isLoading = false; }) diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index 5e642067141..3e939f0c1a3 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -1,16 +1,11 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; import axios from '~/lib/utils/axios_utils'; import Api from '~/api'; -Vue.use(VueResource); - export default { - getTreeData(endpoint) { - return Vue.http.get(endpoint, { params: { format: 'json' } }); - }, getFileData(endpoint) { - return Vue.http.get(endpoint, { params: { format: 'json', viewer: 'none' } }); + return axios.get(endpoint, { + params: { format: 'json', viewer: 'none' }, + }); }, getRawFileData(file) { if (file.tempFile) { @@ -21,7 +16,11 @@ export default { return Promise.resolve(file.raw); } - return Vue.http.get(file.rawPath, { params: { format: 'json' } }).then(res => res.text()); + return axios + .get(file.rawPath, { + params: { format: 'json' }, + }) + .then(({ data }) => data); }, getBaseRawFileData(file, sha) { if (file.tempFile) { @@ -32,11 +31,11 @@ export default { return Promise.resolve(file.baseRaw); } - return Vue.http + return axios .get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), { params: { format: 'json' }, }) - .then(res => res.text()); + .then(({ data }) => data); }, getProjectData(namespace, project) { return Api.project(`${namespace}/${project}`); @@ -53,21 +52,9 @@ export default { getBranchData(projectId, currentBranchId) { return Api.branchSingle(projectId, currentBranchId); }, - createBranch(projectId, payload) { - const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId); - - return Vue.http.post(url, payload); - }, commit(projectId, payload) { return Api.commitMultiple(projectId, payload); }, - getTreeLastCommit(endpoint) { - return Vue.http.get(endpoint, { - params: { - format: 'json', - }, - }); - }, getFiles(projectUrl, branchId) { const url = `${projectUrl}/files/${branchId}`; return axios.get(url, { params: { format: 'json' } }); diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 29995a29d1a..6c0887e11ee 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -1,5 +1,5 @@ -import { normalizeHeaders } from '~/lib/utils/common_utils'; -import flash from '~/flash'; +import { __ } from '../../../locale'; +import { normalizeHeaders } from '../../../lib/utils/common_utils'; import eventHub from '../../eventhub'; import service from '../../services'; import * as types from '../mutation_types'; @@ -66,13 +66,10 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive .getFileData( `${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`, ) - .then(res => { - const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); - setPageTitle(pageTitle); + .then(({ data, headers }) => { + const normalizedHeaders = normalizeHeaders(headers); + setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE'])); - return res.json(); - }) - .then(data => { commit(types.SET_FILE_DATA, { data, file }); commit(types.TOGGLE_FILE_OPEN, path); if (makeFileActive) dispatch('setFileActive', path); @@ -80,7 +77,13 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive }) .catch(() => { commit(types.TOGGLE_LOADING, { entry: file }); - flash('Error loading file data. Please try again.', 'alert', document, null, false, true); + dispatch('setErrorMessage', { + text: __('An error occured whilst loading the file.'), + action: payload => + dispatch('getFileData', payload).then(() => dispatch('setErrorMessage', null)), + actionText: __('Please try again'), + actionPayload: { path, makeFileActive }, + }); }); }; @@ -88,7 +91,7 @@ export const setFileMrChange = ({ commit }, { file, mrChange }) => { commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange }); }; -export const getRawFileData = ({ state, commit }, { path, baseSha }) => { +export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => { const file = state.entries[path]; return new Promise((resolve, reject) => { service @@ -113,7 +116,13 @@ export const getRawFileData = ({ state, commit }, { path, baseSha }) => { } }) .catch(() => { - flash('Error loading file content. Please try again.'); + dispatch('setErrorMessage', { + text: __('An error occured whilst loading the file content.'), + action: payload => + dispatch('getRawFileData', payload).then(() => dispatch('setErrorMessage', null)), + actionText: __('Please try again'), + actionPayload: { path, baseSha }, + }); reject(); }); }); diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index edb20ff96fc..4aa151abcb7 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -1,17 +1,16 @@ -import flash from '~/flash'; +import { __ } from '../../../locale'; import service from '../../services'; import * as types from '../mutation_types'; export const getMergeRequestData = ( - { commit, state }, + { commit, dispatch, state }, { projectId, mergeRequestId, force = false } = {}, ) => new Promise((resolve, reject) => { if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) { service .getProjectMergeRequestData(projectId, mergeRequestId) - .then(res => res.data) - .then(data => { + .then(({ data }) => { commit(types.SET_MERGE_REQUEST, { projectPath: projectId, mergeRequestId, @@ -21,7 +20,15 @@ export const getMergeRequestData = ( resolve(data); }) .catch(() => { - flash('Error loading merge request data. Please try again.'); + dispatch('setErrorMessage', { + text: __('An error occured whilst loading the merge request.'), + action: payload => + dispatch('getMergeRequestData', payload).then(() => + dispatch('setErrorMessage', null), + ), + actionText: __('Please try again'), + actionPayload: { projectId, mergeRequestId, force }, + }); reject(new Error(`Merge Request not loaded ${projectId}`)); }); } else { @@ -30,15 +37,14 @@ export const getMergeRequestData = ( }); export const getMergeRequestChanges = ( - { commit, state }, + { commit, dispatch, state }, { projectId, mergeRequestId, force = false } = {}, ) => new Promise((resolve, reject) => { if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) { service .getProjectMergeRequestChanges(projectId, mergeRequestId) - .then(res => res.data) - .then(data => { + .then(({ data }) => { commit(types.SET_MERGE_REQUEST_CHANGES, { projectPath: projectId, mergeRequestId, @@ -47,7 +53,15 @@ export const getMergeRequestChanges = ( resolve(data); }) .catch(() => { - flash('Error loading merge request changes. Please try again.'); + dispatch('setErrorMessage', { + text: __('An error occured whilst loading the merge request changes.'), + action: payload => + dispatch('getMergeRequestChanges', payload).then(() => + dispatch('setErrorMessage', null), + ), + actionText: __('Please try again'), + actionPayload: { projectId, mergeRequestId, force }, + }); reject(new Error(`Merge Request Changes not loaded ${projectId}`)); }); } else { @@ -56,7 +70,7 @@ export const getMergeRequestChanges = ( }); export const getMergeRequestVersions = ( - { commit, state }, + { commit, dispatch, state }, { projectId, mergeRequestId, force = false } = {}, ) => new Promise((resolve, reject) => { @@ -73,7 +87,15 @@ export const getMergeRequestVersions = ( resolve(data); }) .catch(() => { - flash('Error loading merge request versions. Please try again.'); + dispatch('setErrorMessage', { + text: __('An error occured whilst loading the merge request version data.'), + action: payload => + dispatch('getMergeRequestVersions', payload).then(() => + dispatch('setErrorMessage', null), + ), + actionText: __('Please try again'), + actionPayload: { projectId, mergeRequestId, force }, + }); reject(new Error(`Merge Request Versions not loaded ${projectId}`)); }); } else { diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index ab5cd8e4742..501e25d452b 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -104,7 +104,7 @@ export const createNewBranchFromDefault = ({ state, dispatch, getters }, branch) .catch(() => { dispatch('setErrorMessage', { text: __('An error occured creating the new branch.'), - action: 'createNewBranchFromDefault', + action: payload => dispatch('createNewBranchFromDefault', payload), actionText: __('Please try again'), actionPayload: branch, }); @@ -119,7 +119,7 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => { }, false, ), - action: 'createNewBranchFromDefault', + action: payload => dispatch('createNewBranchFromDefault', payload), actionText: __('Create branch'), actionPayload: branchId, }); diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index dcdd900fc7e..ffaaaabff17 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -1,9 +1,6 @@ -import { normalizeHeaders } from '~/lib/utils/common_utils'; -import flash from '~/flash'; import { __ } from '../../../locale'; import service from '../../services'; import * as types from '../mutation_types'; -import { findEntry } from '../utils'; import FilesDecoratorWorker from '../workers/files_decorator_worker'; export const toggleTreeOpen = ({ commit }, path) => { @@ -37,32 +34,6 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => { dispatch('showTreeEntry', row.path); }; -export const getLastCommitData = ({ state, commit, dispatch }, tree = state) => { - if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return; - - service - .getTreeLastCommit(tree.lastCommitPath) - .then(res => { - const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null; - - commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath }); - - return res.json(); - }) - .then(data => { - data.forEach(lastCommit => { - const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name); - - if (entry) { - commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit }); - } - }); - - dispatch('getLastCommitData', tree); - }) - .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true)); -}; - export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) => new Promise((resolve, reject) => { if ( @@ -106,14 +77,13 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = if (e.response.status === 404) { dispatch('showBranchNotFoundError', branchId); } else { - flash( - __('Error loading tree data. Please try again.'), - 'alert', - document, - null, - false, - true, - ); + dispatch('setErrorMessage', { + text: __('An error occured whilst loading all the files.'), + action: payload => + dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)), + actionText: __('Please try again'), + actionPayload: { projectId, branchId }, + }); } reject(e); }); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 7cca32dc6fa..1f66fa811ea 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -1,11 +1,10 @@ import $ from 'jquery'; import timeago from 'timeago.js'; -import dateFormat from 'vendor/date.format'; +import dateFormat from 'dateformat'; import { pluralize } from './text_utility'; import { languageCode, s__ } from '../../locale'; window.timeago = timeago; -window.dateFormat = dateFormat; /** * Returns i18n month names array. @@ -143,7 +142,8 @@ export const localTimeAgo = ($timeagoEls, setTimeago = true) => { if (setTimeago) { // Recreate with custom template $(el).tooltip({ - template: '<div class="tooltip local-timeago" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>', + template: + '<div class="tooltip local-timeago" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>', }); } @@ -275,10 +275,8 @@ export const totalDaysInMonth = date => { * * @param {Array} quarter */ -export const totalDaysInQuarter = quarter => quarter.reduce( - (acc, month) => acc + totalDaysInMonth(month), - 0, -); +export const totalDaysInQuarter = quarter => + quarter.reduce((acc, month) => acc + totalDaysInMonth(month), 0); /** * Returns list of Dates referring to Sundays of the month @@ -333,14 +331,8 @@ export const getTimeframeWindowFrom = (startDate, length) => { // Iterate and set date for the size of length // and push date reference to timeframe list const timeframe = new Array(length) - .fill() - .map( - (val, i) => new Date( - startDate.getFullYear(), - startDate.getMonth() + i, - 1, - ), - ); + .fill() + .map((val, i) => new Date(startDate.getFullYear(), startDate.getMonth() + i, 1)); // Change date of last timeframe item to last date of the month timeframe[length - 1].setDate(totalDaysInMonth(timeframe[length - 1])); @@ -362,14 +354,15 @@ export const getTimeframeWindowFrom = (startDate, length) => { * @param {Date} date * @param {Array} quarter */ -export const dayInQuarter = (date, quarter) => quarter.reduce((acc, month) => { - if (date.getMonth() > month.getMonth()) { - return acc + totalDaysInMonth(month); - } else if (date.getMonth() === month.getMonth()) { - return acc + date.getDate(); - } - return acc + 0; -}, 0); +export const dayInQuarter = (date, quarter) => + quarter.reduce((acc, month) => { + if (date.getMonth() > month.getMonth()) { + return acc + totalDaysInMonth(month); + } else if (date.getMonth() === month.getMonth()) { + return acc + date.getDate(); + } + return acc + 0; + }, 0); window.gl = window.gl || {}; window.gl.utils = { diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 329d4303132..53d7504de35 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -16,6 +16,7 @@ import Diff from './diff'; import { localTimeAgo } from './lib/utils/datetime_utility'; import syntaxHighlight from './syntax_highlight'; import Notes from './notes'; +import { polyfillSticky } from './lib/utils/sticky'; /* eslint-disable max-len */ // MergeRequestTabs @@ -68,12 +69,23 @@ let { location } = window; export default class MergeRequestTabs { constructor({ action, setUrl, stubLocation } = {}) { - const mergeRequestTabs = document.querySelector('.js-tabs-affix'); + this.mergeRequestTabs = document.querySelector('.merge-request-tabs-container'); + this.mergeRequestTabsAll = + this.mergeRequestTabs && this.mergeRequestTabs.querySelectorAll + ? this.mergeRequestTabs.querySelectorAll('.merge-request-tabs li') + : null; + this.mergeRequestTabPanes = document.querySelector('#diff-notes-app'); + this.mergeRequestTabPanesAll = + this.mergeRequestTabPanes && this.mergeRequestTabPanes.querySelectorAll + ? this.mergeRequestTabPanes.querySelectorAll('.tab-pane') + : null; const navbar = document.querySelector('.navbar-gitlab'); const peek = document.getElementById('js-peek'); const paddingTop = 16; + this.commitsTab = document.querySelector('.tab-content .commits.tab-pane'); + this.currentTab = null; this.diffsLoaded = false; this.pipelinesLoaded = false; this.commitsLoaded = false; @@ -83,15 +95,15 @@ export default class MergeRequestTabs { this.setUrl = setUrl !== undefined ? setUrl : true; this.setCurrentAction = this.setCurrentAction.bind(this); this.tabShown = this.tabShown.bind(this); - this.showTab = this.showTab.bind(this); + this.clickTab = this.clickTab.bind(this); this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0; if (peek) { this.stickyTop += peek.offsetHeight; } - if (mergeRequestTabs) { - this.stickyTop += mergeRequestTabs.offsetHeight; + if (this.mergeRequestTabs) { + this.stickyTop += this.mergeRequestTabs.offsetHeight; } if (stubLocation) { @@ -99,25 +111,22 @@ export default class MergeRequestTabs { } this.bindEvents(); - this.activateTab(action); + if ( + this.mergeRequestTabs && + this.mergeRequestTabs.querySelector(`a[data-action='${action}']`) && + this.mergeRequestTabs.querySelector(`a[data-action='${action}']`).click + ) + this.mergeRequestTabs.querySelector(`a[data-action='${action}']`).click(); this.initAffix(); } bindEvents() { - $(document) - .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) - .on('click', '.js-show-tab', this.showTab); - - $('.merge-request-tabs a[data-toggle="tab"]').on('click', this.clickTab); + $('.merge-request-tabs a[data-toggle="tabvue"]').on('click', this.clickTab); } // Used in tests unbindEvents() { - $(document) - .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) - .off('click', '.js-show-tab', this.showTab); - - $('.merge-request-tabs a[data-toggle="tab"]').off('click', this.clickTab); + $('.merge-request-tabs a[data-toggle="tabvue"]').off('click', this.clickTab); } destroyPipelinesView() { @@ -129,58 +138,87 @@ export default class MergeRequestTabs { } } - showTab(e) { - e.preventDefault(); - this.activateTab($(e.target).data('action')); - } - clickTab(e) { - if (e.currentTarget && isMetaClick(e)) { - const targetLink = e.currentTarget.getAttribute('href'); + if (e.currentTarget) { e.stopImmediatePropagation(); e.preventDefault(); - window.open(targetLink, '_blank'); + + const { action } = e.currentTarget.dataset; + + if (action) { + const href = e.currentTarget.getAttribute('href'); + this.tabShown(action, href); + } else if (isMetaClick(e)) { + const targetLink = e.currentTarget.getAttribute('href'); + window.open(targetLink, '_blank'); + } } } - tabShown(e) { - const $target = $(e.target); - const action = $target.data('action'); - - if (action === 'commits') { - this.loadCommits($target.attr('href')); - this.expandView(); - this.resetViewContainer(); - this.destroyPipelinesView(); - } else if (this.isDiffAction(action)) { - if (!isInVueNoteablePage()) { - this.loadDiff($target.attr('href')); - } - if (bp.getBreakpointSize() !== 'lg') { - this.shrinkView(); + tabShown(action, href) { + if (action !== this.currentTab && this.mergeRequestTabs) { + this.currentTab = action; + + if (this.mergeRequestTabPanesAll) { + this.mergeRequestTabPanesAll.forEach(el => { + const tabPane = el; + tabPane.style.display = 'none'; + }); } - if (this.diffViewType() === 'parallel') { - this.expandViewContainer(); + + if (this.mergeRequestTabsAll) { + this.mergeRequestTabsAll.forEach(el => { + el.classList.remove('active'); + }); } - this.destroyPipelinesView(); - this.commitsTab.classList.remove('active'); - } else if (action === 'pipelines') { - this.resetViewContainer(); - this.mountPipelinesView(); - } else { - if (bp.getBreakpointSize() !== 'xs') { + + const tabPane = this.mergeRequestTabPanes.querySelector(`#${action}`); + if (tabPane) tabPane.style.display = 'block'; + const tab = this.mergeRequestTabs.querySelector(`.${action}-tab`); + if (tab) tab.classList.add('active'); + + if (action === 'commits') { + this.loadCommits(href); + this.expandView(); + this.resetViewContainer(); + this.destroyPipelinesView(); + } else if (action === 'new') { this.expandView(); + this.resetViewContainer(); + this.destroyPipelinesView(); + } else if (this.isDiffAction(action)) { + if (!isInVueNoteablePage()) { + this.loadDiff(href); + } + if (bp.getBreakpointSize() !== 'lg') { + this.shrinkView(); + } + if (this.diffViewType() === 'parallel') { + this.expandViewContainer(); + } + this.destroyPipelinesView(); + this.commitsTab.classList.remove('active'); + } else if (action === 'pipelines') { + this.resetViewContainer(); + this.mountPipelinesView(); + } else { + this.mergeRequestTabPanes.querySelector('#notes').style.display = 'block'; + this.mergeRequestTabs.querySelector('.notes-tab').classList.add('active'); + + if (bp.getBreakpointSize() !== 'xs') { + this.expandView(); + } + this.resetViewContainer(); + this.destroyPipelinesView(); + + initDiscussionTab(); + } + if (this.setUrl) { + this.setCurrentAction(action); } - this.resetViewContainer(); - this.destroyPipelinesView(); - initDiscussionTab(); - } - if (this.setUrl) { - this.setCurrentAction(action); + this.eventHub.$emit('MergeRequestTabChange', this.getCurrentAction()); } - - this.eventHub.$emit('MergeRequestTabChange', this.getCurrentAction()); } scrollToElement(container) { @@ -193,12 +231,6 @@ export default class MergeRequestTabs { } } - // Activate a tab based on the current action - activateTab(action) { - // important note: the .tab('show') method triggers 'shown.bs.tab' event itself - $(`.merge-request-tabs a[data-action='${action}']`).tab('show'); - } - // Replaces the current Merge Request-specific action in the URL with a new one // // If the action is "notes", the URL is reset to the standard @@ -426,7 +458,6 @@ export default class MergeRequestTabs { initAffix() { const $tabs = $('.js-tabs-affix'); - const $fixedNav = $('.navbar-gitlab'); // Screen space on small screens is usually very sparse // So we dont affix the tabs on these @@ -439,21 +470,6 @@ export default class MergeRequestTabs { */ if ($tabs.css('position') !== 'static') return; - const $diffTabs = $('#diff-notes-app'); - - $tabs - .off('affix.bs.affix affix-top.bs.affix') - .affix({ - offset: { - top: () => $diffTabs.offset().top - $tabs.height() - $fixedNav.height(), - }, - }) - .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() })) - .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' })); - - // Fix bug when reloading the page already scrolling - if ($tabs.hasClass('affix')) { - $tabs.trigger('affix.bs.affix'); - } + polyfillSticky($tabs); } } diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js index 3c0c9995cc2..8aabb840847 100644 --- a/app/assets/javascripts/mr_notes/index.js +++ b/app/assets/javascripts/mr_notes/index.js @@ -45,17 +45,17 @@ export default function initMrNotes() { this.updateDiscussionTabCounter(); }, }, + created() { + this.setActiveTab(window.mrTabs.getCurrentAction()); + }, mounted() { this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge'); - this.setActiveTab(window.mrTabs.getCurrentAction()); - - window.mrTabs.eventHub.$on('MergeRequestTabChange', tab => { - this.setActiveTab(tab); - }); $(document).on('visibilitychange', this.updateDiscussionTabCounter); + window.mrTabs.eventHub.$on('MergeRequestTabChange', this.setActiveTab); }, beforeDestroy() { $(document).off('visibilitychange', this.updateDiscussionTabCounter); + window.mrTabs.eventHub.$off('MergeRequestTabChange', this.setActiveTab); }, methods: { ...mapActions(['setActiveTab']), diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 98f8b9af168..a8995021699 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -3,6 +3,7 @@ import { mapGetters, mapActions } from 'vuex'; import { getLocationHash } from '../../lib/utils/url_utility'; import Flash from '../../flash'; import * as constants from '../constants'; +import eventHub from '../event_hub'; import noteableNote from './noteable_note.vue'; import noteableDiscussion from './noteable_discussion.vue'; import systemNote from '../../vue_shared/components/notes/system_note.vue'; @@ -49,7 +50,7 @@ export default { }; }, computed: { - ...mapGetters(['discussions', 'getNotesDataByProp', 'discussionCount']), + ...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount']), noteableType() { return this.noteableData.noteableType; }, @@ -61,19 +62,30 @@ export default { isSkeletonNote: true, }); } + return this.discussions; }, }, + watch: { + shouldShow() { + if (!this.isNotesFetched) { + this.fetchNotes(); + } + }, + }, created() { this.setNotesData(this.notesData); this.setNoteableData(this.noteableData); this.setUserData(this.userData); this.setTargetNoteHash(getLocationHash()); + eventHub.$once('fetchNotesData', this.fetchNotes); }, mounted() { - this.fetchNotes(); - const { parentElement } = this.$el; + if (this.shouldShow) { + this.fetchNotes(); + } + const { parentElement } = this.$el; if (parentElement && parentElement.classList.contains('js-vue-notes-event')) { parentElement.addEventListener('toggleAward', event => { const { awardName, noteId } = event.detail; @@ -93,6 +105,7 @@ export default { setLastFetchedAt: 'setLastFetchedAt', setTargetNoteHash: 'setTargetNoteHash', toggleDiscussion: 'toggleDiscussion', + setNotesFetchedState: 'setNotesFetchedState', }), getComponentName(discussion) { if (discussion.isSkeletonNote) { @@ -119,11 +132,13 @@ export default { }) .then(() => { this.isLoading = false; + this.setNotesFetchedState(true); }) .then(() => this.$nextTick()) .then(() => this.checkLocationHash()) .catch(() => { this.isLoading = false; + this.setNotesFetchedState(true); Flash('Something went wrong while fetching comments. Please try again.'); }); }, @@ -160,12 +175,13 @@ export default { <template> <div - v-if="shouldShow" - id="notes"> + v-show="shouldShow" + id="notes" + > <ul id="notes-list" - class="notes main-notes-list timeline"> - + class="notes main-notes-list timeline" + > <component v-for="discussion in allDiscussions" :is="getComponentName(discussion)" diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 0a40b48257f..671fa4d7d22 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -28,6 +28,9 @@ export const setInitialNotes = ({ commit }, discussions) => export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data); +export const setNotesFetchedState = ({ commit }, state) => + commit(types.SET_NOTES_FETCHED_STATE, state); + export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data); export const fetchDiscussions = ({ commit }, path) => diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index ab28bb48e9e..a5518383d44 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -8,6 +8,8 @@ export const targetNoteHash = state => state.targetNoteHash; export const getNotesData = state => state.notesData; +export const isNotesFetched = state => state.isNotesFetched; + export const getNotesDataByProp = state => prop => state.notesData[prop]; export const getNoteableData = state => state.noteableData; diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index a978490c009..b4cb9267e0f 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -10,6 +10,7 @@ export default { // View layer isToggleStateButtonLoading: false, + isNotesFetched: false, // holds endpoints and permissions provided through haml notesData: { diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index caead4cb860..a25098fbc06 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -15,6 +15,7 @@ export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION'; export const UPDATE_NOTE = 'UPDATE_NOTE'; export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION'; export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES'; +export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE'; // Issue export const CLOSE_ISSUE = 'CLOSE_ISSUE'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index ea165709e61..e5e40ce07fa 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -205,6 +205,10 @@ export default { Object.assign(state, { isToggleStateButtonLoading: value }); }, + [types.SET_NOTES_FETCHED_STATE](state, value) { + Object.assign(state, { isNotesFetched: value }); + }, + [types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) { const discussion = utils.findNoteObjectById(state.discussions, discussionId); const index = state.discussions.indexOf(discussion); diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js index 2e1fe78b3fa..e3e0ab91993 100644 --- a/app/assets/javascripts/pages/search/show/search.js +++ b/app/assets/javascripts/pages/search/show/search.js @@ -105,7 +105,7 @@ export default class Search { getProjectsData(term) { return new Promise((resolve) => { if (this.groupId) { - Api.groupProjects(this.groupId, term, resolve); + Api.groupProjects(this.groupId, term, {}, resolve); } else { Api.projects(term, { order_by: 'id', diff --git a/app/assets/javascripts/pages/snippets/form.js b/app/assets/javascripts/pages/snippets/form.js index 758bbafead3..f369c7ef9a6 100644 --- a/app/assets/javascripts/pages/snippets/form.js +++ b/app/assets/javascripts/pages/snippets/form.js @@ -8,6 +8,7 @@ export default () => { members: false, issues: false, mergeRequests: false, + epics: false, milestones: false, labels: false, }); diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index 50d042fef29..9892a039941 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import _ from 'underscore'; import { scaleLinear, scaleThreshold } from 'd3-scale'; import { select } from 'd3-selection'; +import dateFormat from 'dateformat'; import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility'; import axios from '~/lib/utils/axios_utils'; import flash from '~/flash'; @@ -26,7 +27,7 @@ function getSystemDate(systemUtcOffsetSeconds) { function formatTooltipText({ date, count }) { const dateObject = new Date(date); const dateDayName = getDayName(dateObject); - const dateText = dateObject.format('mmm d, yyyy'); + const dateText = dateFormat(dateObject, 'mmm d, yyyy'); let contribText = 'No contributions'; if (count > 0) { @@ -84,7 +85,7 @@ export default class ActivityCalendar { date.setDate(date.getDate() + i); const day = date.getDay(); - const count = timestamps[date.format('yyyy-mm-dd')] || 0; + const count = timestamps[dateFormat(date, 'yyyy-mm-dd')] || 0; // Create a new group array if this is the first day of the week // or if is first object diff --git a/app/assets/javascripts/pipelines/components/blank_state.vue b/app/assets/javascripts/pipelines/components/blank_state.vue index f3219b8291c..34360105176 100644 --- a/app/assets/javascripts/pipelines/components/blank_state.vue +++ b/app/assets/javascripts/pipelines/components/blank_state.vue @@ -1,18 +1,18 @@ <script> - export default { - name: 'PipelinesSvgState', - props: { - svgPath: { - type: String, - required: true, - }, +export default { + name: 'PipelinesSvgState', + props: { + svgPath: { + type: String, + required: true, + }, - message: { - type: String, - required: true, - }, + message: { + type: String, + required: true, }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue index 50c27bed9fd..c5a45afc634 100644 --- a/app/assets/javascripts/pipelines/components/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/empty_state.vue @@ -1,21 +1,21 @@ <script> - export default { - name: 'PipelinesEmptyState', - props: { - helpPagePath: { - type: String, - required: true, - }, - emptyStateSvgPath: { - type: String, - required: true, - }, - canSetCi: { - type: Boolean, - required: true, - }, +export default { + name: 'PipelinesEmptyState', + props: { + helpPagePath: { + type: String, + required: true, }, - }; + emptyStateSvgPath: { + type: String, + required: true, + }, + canSetCi: { + type: Boolean, + required: true, + }, + }, +}; </script> <template> <div class="row empty-state js-empty-state"> diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 1f152ed438d..b82e28a0735 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -41,7 +41,6 @@ export default { type: String, required: true, }, - }, data() { return { @@ -67,7 +66,8 @@ export default { this.isDisabled = true; - axios.post(`${this.link}.json`) + axios + .post(`${this.link}.json`) .then(() => { this.isDisabled = false; this.$emit('pipelineActionRequestComplete'); 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 e047d10ac93..c32dc83da8e 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -109,6 +109,7 @@ export default { :key="i" > <job-component + :dropdown-length="job.size" :job="item" css-class-job-name="mini-pipeline-graph-dropdown-item" @pipelineActionRequestComplete="pipelineActionRequestComplete" diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index 886e62ab1a7..8af984ef91a 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -46,6 +46,11 @@ export default { required: false, default: '', }, + dropdownLength: { + type: Number, + required: false, + default: Infinity, + }, }, computed: { status() { @@ -70,6 +75,10 @@ export default { return textBuilder.join(' '); }, + tooltipBoundary() { + return this.dropdownLength < 5 ? 'viewport' : null; + }, + /** * Verifies if the provided job has an action path * @@ -94,9 +103,9 @@ export default { :href="status.details_path" :title="tooltipText" :class="cssClassJobName" + :data-boundary="tooltipBoundary" data-container="body" data-html="true" - data-boundary="viewport" class="js-pipeline-graph-job-link" > 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 14f4964a406..6fdbcc1e049 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue @@ -1,28 +1,28 @@ <script> - import ciIcon from '../../../vue_shared/components/ci_icon.vue'; +import ciIcon from '../../../vue_shared/components/ci_icon.vue'; - /** - * Component that renders both the CI icon status and the job name. - * Used in - * - Badge component - * - Dropdown badge components - */ - export default { - components: { - ciIcon, +/** + * Component that renders both the CI icon status and the job name. + * Used in + * - Badge component + * - Dropdown badge components + */ +export default { + components: { + ciIcon, + }, + props: { + name: { + type: String, + required: true, }, - props: { - name: { - type: String, - required: true, - }, - status: { - type: Object, - required: true, - }, + status: { + type: Object, + required: true, }, - }; + }, +}; </script> <template> <span class="ci-job-name-component"> diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index 5b212ee8931..001eaeaa065 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -1,81 +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', - components: { - ciHeader, - loadingIcon, +export default { + name: 'PipelineHeaderSection', + components: { + ciHeader, + loadingIcon, + }, + props: { + pipeline: { + type: Object, + required: true, }, - props: { - pipeline: { - type: Object, - required: true, - }, - isLoading: { - type: Boolean, - required: true, - }, - }, - data() { - return { - actions: this.getActions(), - }; + isLoading: { + type: Boolean, + required: true, }, + }, + data() { + return { + actions: this.getActions(), + }; + }, - computed: { - status() { - return this.pipeline.details && this.pipeline.details.status; - }, - 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; }, + }, - watch: { - pipeline() { - this.actions = this.getActions(); - }, + watch: { + pipeline() { + this.actions = this.getActions(); }, + }, - methods: { - postAction(action) { - const index = this.actions.indexOf(action); + methods: { + postAction(action) { + const index = this.actions.indexOf(action); - this.$set(this.actions[index], 'isLoading', true); + this.$set(this.actions[index], 'isLoading', true); - eventHub.$emit('headerPostAction', action); - }, + eventHub.$emit('headerPostAction', action); + }, - getActions() { - const actions = []; + getActions() { + const actions = []; - 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, - }); - } + 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, + }); + } - 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.cancel_path) { + actions.push({ + label: 'Cancel running', + path: this.pipeline.cancel_path, + cssClass: 'js-btn-cancel-pipeline btn btn-danger', + type: 'button', + isLoading: false, + }); + } - return actions; - }, + return actions; }, - }; + }, +}; </script> <template> <div class="pipeline-header-container"> diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue index 1fce9f16ee0..9501afb7493 100644 --- a/app/assets/javascripts/pipelines/components/nav_controls.vue +++ b/app/assets/javascripts/pipelines/components/nav_controls.vue @@ -1,42 +1,42 @@ <script> - import LoadingButton from '../../vue_shared/components/loading_button.vue'; +import LoadingButton from '../../vue_shared/components/loading_button.vue'; - export default { - name: 'PipelineNavControls', - components: { - LoadingButton, +export default { + name: 'PipelineNavControls', + components: { + LoadingButton, + }, + props: { + newPipelinePath: { + type: String, + required: false, + default: null, }, - props: { - newPipelinePath: { - type: String, - required: false, - default: null, - }, - resetCachePath: { - type: String, - required: false, - default: null, - }, + resetCachePath: { + type: String, + required: false, + default: null, + }, - ciLintPath: { - type: String, - required: false, - default: null, - }, + ciLintPath: { + type: String, + required: false, + default: null, + }, - isResetCacheButtonLoading: { - type: Boolean, - required: false, - default: false, - }, + isResetCacheButtonLoading: { + type: Boolean, + required: false, + default: false, }, - methods: { - onClickResetCache() { - this.$emit('resetRunnersCache', this.resetCachePath); - }, + }, + methods: { + onClickResetCache() { + this.$emit('resetRunnersCache', this.resetCachePath); }, - }; + }, +}; </script> <template> <div class="nav-controls"> diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index a107e579457..75db1e9ae7c 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -1,49 +1,49 @@ <script> - import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; - import popover from '../../vue_shared/directives/popover'; +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; +import popover from '../../vue_shared/directives/popover'; - export default { - components: { - userAvatarLink, +export default { + components: { + userAvatarLink, + }, + directives: { + tooltip, + popover, + }, + props: { + pipeline: { + type: Object, + required: true, }, - directives: { - tooltip, - popover, + autoDevopsHelpPath: { + type: String, + required: true, }, - props: { - pipeline: { - type: Object, - required: true, - }, - autoDevopsHelpPath: { - type: String, - required: true, - }, + }, + computed: { + user() { + return this.pipeline.user; }, - computed: { - user() { - return this.pipeline.user; - }, - popoverOptions() { - return { - html: true, - trigger: 'focus', - placement: 'top', - title: `<div class="autodevops-title"> + popoverOptions() { + return { + 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 + content: `<a class="autodevops-link" href="${this.autoDevopsHelpPath}" target="_blank" rel="noopener noreferrer nofollow"> Learn more about Auto DevOps </a>`, - }; - }, + }; }, - }; + }, +}; </script> <template> <div class="table-section section-15 d-none d-sm-none d-md-block pipeline-tags"> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index b31b4bad7a0..c9d2dc3a3c5 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -1,283 +1,283 @@ <script> - import _ from 'underscore'; - import { __, sprintf, s__ } from '../../locale'; - import createFlash from '../../flash'; - import PipelinesService from '../services/pipelines_service'; - import pipelinesMixin from '../mixins/pipelines'; - import TablePagination from '../../vue_shared/components/table_pagination.vue'; - import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue'; - import NavigationControls from './nav_controls.vue'; - import { getParameterByName } from '../../lib/utils/common_utils'; - import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; +import _ from 'underscore'; +import { __, sprintf, s__ } from '../../locale'; +import createFlash from '../../flash'; +import PipelinesService from '../services/pipelines_service'; +import pipelinesMixin from '../mixins/pipelines'; +import TablePagination from '../../vue_shared/components/table_pagination.vue'; +import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue'; +import NavigationControls from './nav_controls.vue'; +import { getParameterByName } from '../../lib/utils/common_utils'; +import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; - export default { - components: { - TablePagination, - NavigationTabs, - NavigationControls, +export default { + components: { + TablePagination, + NavigationTabs, + NavigationControls, + }, + mixins: [pipelinesMixin, CIPaginationMixin], + props: { + store: { + type: Object, + required: true, }, - mixins: [pipelinesMixin, CIPaginationMixin], - props: { - store: { - type: Object, - required: true, - }, - // Can be rendered in 3 different places, with some visual differences - // Accepts root | child - // `root` -> main view - // `child` -> rendered inside MR or Commit View - viewType: { - type: String, - required: false, - default: 'root', - }, - endpoint: { - type: String, - required: true, - }, - helpPagePath: { - type: String, - required: true, - }, - emptyStateSvgPath: { - type: String, - required: true, - }, - errorStateSvgPath: { - type: String, - required: true, - }, - noPipelinesSvgPath: { - type: String, - required: true, - }, - autoDevopsPath: { - type: String, - required: true, - }, - hasGitlabCi: { - type: Boolean, - required: true, - }, - canCreatePipeline: { - type: Boolean, - required: true, - }, - ciLintPath: { - type: String, - required: false, - default: null, - }, - resetCachePath: { - type: String, - required: false, - default: null, - }, - newPipelinePath: { - type: String, - required: false, - default: null, - }, + // Can be rendered in 3 different places, with some visual differences + // Accepts root | child + // `root` -> main view + // `child` -> rendered inside MR or Commit View + viewType: { + type: String, + required: false, + default: 'root', }, - data() { - return { - // Start with loading state to avoid a glitch when the empty state will be rendered - isLoading: true, - state: this.store.state, - scope: getParameterByName('scope') || 'all', - page: getParameterByName('page') || '1', - requestData: {}, - isResetCacheButtonLoading: false, - }; + endpoint: { + type: String, + required: true, }, - stateMap: { - // with tabs - loading: 'loading', - tableList: 'tableList', - error: 'error', - emptyTab: 'emptyTab', - - // without tabs - emptyState: 'emptyState', + helpPagePath: { + type: String, + required: true, + }, + emptyStateSvgPath: { + type: String, + required: true, + }, + errorStateSvgPath: { + type: String, + required: true, + }, + noPipelinesSvgPath: { + type: String, + required: true, + }, + autoDevopsPath: { + type: String, + required: true, + }, + hasGitlabCi: { + type: Boolean, + required: true, + }, + canCreatePipeline: { + type: Boolean, + required: true, + }, + ciLintPath: { + type: String, + required: false, + default: null, }, - scopes: { - all: 'all', - pending: 'pending', - running: 'running', - finished: 'finished', - branches: 'branches', - tags: 'tags', + resetCachePath: { + type: String, + required: false, + default: null, }, - computed: { - /** - * `hasGitlabCi` handles both internal and external CI. - * The order on which the checks are made in this method is - * important to guarantee we handle all the corner cases. - */ - stateToRender() { - const { stateMap } = this.$options; + newPipelinePath: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + // Start with loading state to avoid a glitch when the empty state will be rendered + isLoading: true, + state: this.store.state, + scope: getParameterByName('scope') || 'all', + page: getParameterByName('page') || '1', + requestData: {}, + isResetCacheButtonLoading: false, + }; + }, + stateMap: { + // with tabs + loading: 'loading', + tableList: 'tableList', + error: 'error', + emptyTab: 'emptyTab', + + // without tabs + emptyState: 'emptyState', + }, + scopes: { + all: 'all', + pending: 'pending', + running: 'running', + finished: 'finished', + branches: 'branches', + tags: 'tags', + }, + computed: { + /** + * `hasGitlabCi` handles both internal and external CI. + * The order on which the checks are made in this method is + * important to guarantee we handle all the corner cases. + */ + stateToRender() { + const { stateMap } = this.$options; - if (this.isLoading) { - return stateMap.loading; - } + if (this.isLoading) { + return stateMap.loading; + } - if (this.hasError) { - return stateMap.error; - } + if (this.hasError) { + return stateMap.error; + } - if (this.state.pipelines.length) { - return stateMap.tableList; - } + if (this.state.pipelines.length) { + return stateMap.tableList; + } - if ((this.scope !== 'all' && this.scope !== null) || this.hasGitlabCi) { - return stateMap.emptyTab; - } + if ((this.scope !== 'all' && this.scope !== null) || this.hasGitlabCi) { + return stateMap.emptyTab; + } - return stateMap.emptyState; - }, - /** - * Tabs are rendered in all states except empty state. - * They are not rendered before the first request to avoid a flicker on first load. - */ - shouldRenderTabs() { - const { stateMap } = this.$options; - return ( - this.hasMadeRequest && - [stateMap.loading, stateMap.tableList, stateMap.error, stateMap.emptyTab].includes( - this.stateToRender, - ) - ); - }, + return stateMap.emptyState; + }, + /** + * Tabs are rendered in all states except empty state. + * They are not rendered before the first request to avoid a flicker on first load. + */ + shouldRenderTabs() { + const { stateMap } = this.$options; + return ( + this.hasMadeRequest && + [stateMap.loading, stateMap.tableList, stateMap.error, stateMap.emptyTab].includes( + this.stateToRender, + ) + ); + }, - shouldRenderButtons() { - return ( - (this.newPipelinePath || this.resetCachePath || this.ciLintPath) && this.shouldRenderTabs - ); - }, + shouldRenderButtons() { + return ( + (this.newPipelinePath || this.resetCachePath || this.ciLintPath) && this.shouldRenderTabs + ); + }, - shouldRenderPagination() { - return ( - !this.isLoading && - this.state.pipelines.length && - this.state.pageInfo.total > this.state.pageInfo.perPage - ); - }, + shouldRenderPagination() { + return ( + !this.isLoading && + this.state.pipelines.length && + this.state.pageInfo.total > this.state.pageInfo.perPage + ); + }, - emptyTabMessage() { - const { scopes } = this.$options; - const possibleScopes = [scopes.pending, scopes.running, scopes.finished]; + emptyTabMessage() { + const { scopes } = this.$options; + const possibleScopes = [scopes.pending, scopes.running, scopes.finished]; - if (possibleScopes.includes(this.scope)) { - return sprintf(s__('Pipelines|There are currently no %{scope} pipelines.'), { - scope: this.scope, - }); - } + if (possibleScopes.includes(this.scope)) { + return sprintf(s__('Pipelines|There are currently no %{scope} pipelines.'), { + scope: this.scope, + }); + } - return s__('Pipelines|There are currently no pipelines.'); - }, + return s__('Pipelines|There are currently no pipelines.'); + }, - tabs() { - const { count } = this.state; - const { scopes } = this.$options; + tabs() { + const { count } = this.state; + const { scopes } = this.$options; - return [ - { - name: __('All'), - scope: scopes.all, - count: count.all, - isActive: this.scope === 'all', - }, - { - name: __('Pending'), - scope: scopes.pending, - count: count.pending, - isActive: this.scope === 'pending', - }, - { - name: __('Running'), - scope: scopes.running, - count: count.running, - isActive: this.scope === 'running', - }, - { - name: __('Finished'), - scope: scopes.finished, - count: count.finished, - isActive: this.scope === 'finished', - }, - { - name: __('Branches'), - scope: scopes.branches, - isActive: this.scope === 'branches', - }, - { - name: __('Tags'), - scope: scopes.tags, - isActive: this.scope === 'tags', - }, - ]; - }, + return [ + { + name: __('All'), + scope: scopes.all, + count: count.all, + isActive: this.scope === 'all', + }, + { + name: __('Pending'), + scope: scopes.pending, + count: count.pending, + isActive: this.scope === 'pending', + }, + { + name: __('Running'), + scope: scopes.running, + count: count.running, + isActive: this.scope === 'running', + }, + { + name: __('Finished'), + scope: scopes.finished, + count: count.finished, + isActive: this.scope === 'finished', + }, + { + name: __('Branches'), + scope: scopes.branches, + isActive: this.scope === 'branches', + }, + { + name: __('Tags'), + scope: scopes.tags, + isActive: this.scope === 'tags', + }, + ]; }, - created() { - this.service = new PipelinesService(this.endpoint); - this.requestData = { page: this.page, scope: this.scope }; + }, + created() { + this.service = new PipelinesService(this.endpoint); + this.requestData = { page: this.page, scope: this.scope }; + }, + methods: { + successCallback(resp) { + // Because we are polling & the user is interacting verify if the response received + // matches the last request made + if (_.isEqual(resp.config.params, this.requestData)) { + this.store.storeCount(resp.data.count); + this.store.storePagination(resp.headers); + this.setCommonData(resp.data.pipelines); + } }, - methods: { - successCallback(resp) { - // Because we are polling & the user is interacting verify if the response received - // matches the last request made - if (_.isEqual(resp.config.params, this.requestData)) { - this.store.storeCount(resp.data.count); - this.store.storePagination(resp.headers); - this.setCommonData(resp.data.pipelines); - } - }, - /** - * Handles URL and query parameter changes. - * When the user uses the pagination or the tabs, - * - update URL - * - Make API request to the server with new parameters - * - Update the polling function - * - Update the internal state - */ - updateContent(parameters) { - this.updateInternalState(parameters); + /** + * Handles URL and query parameter changes. + * When the user uses the pagination or the tabs, + * - update URL + * - Make API request to the server with new parameters + * - Update the polling function + * - Update the internal state + */ + updateContent(parameters) { + this.updateInternalState(parameters); - // fetch new data - return this.service - .getPipelines(this.requestData) - .then(response => { - this.isLoading = false; - this.successCallback(response); + // fetch new data + return this.service + .getPipelines(this.requestData) + .then(response => { + this.isLoading = false; + this.successCallback(response); - // restart polling - this.poll.restart({ data: this.requestData }); - }) - .catch(() => { - this.isLoading = false; - this.errorCallback(); + // restart polling + this.poll.restart({ data: this.requestData }); + }) + .catch(() => { + this.isLoading = false; + this.errorCallback(); - // restart polling - this.poll.restart({ data: this.requestData }); - }); - }, + // restart polling + this.poll.restart({ data: this.requestData }); + }); + }, - handleResetRunnersCache(endpoint) { - this.isResetCacheButtonLoading = true; + handleResetRunnersCache(endpoint) { + this.isResetCacheButtonLoading = true; - this.service - .postAction(endpoint) - .then(() => { - this.isResetCacheButtonLoading = false; - createFlash(s__('Pipelines|Project cache successfully reset.'), 'notice'); - }) - .catch(() => { - this.isResetCacheButtonLoading = false; - createFlash(s__('Pipelines|Something went wrong while cleaning runners cache.')); - }); - }, + this.service + .postAction(endpoint) + .then(() => { + this.isResetCacheButtonLoading = false; + createFlash(s__('Pipelines|Project cache successfully reset.'), 'notice'); + }) + .catch(() => { + this.isResetCacheButtonLoading = false; + createFlash(s__('Pipelines|Something went wrong while cleaning runners cache.')); + }); }, - }; + }, +}; </script> <template> <div class="pipelines-container"> diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 5070c253f11..1c8d7303c52 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -1,44 +1,44 @@ <script> - 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'; +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 { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + components: { + loadingIcon, + icon, + }, + props: { + actions: { + type: Array, + required: true, }, - components: { - loadingIcon, - icon, - }, - props: { - actions: { - type: Array, - required: true, - }, - }, - data() { - return { - isLoading: false, - }; - }, - methods: { - onClickAction(endpoint) { - this.isLoading = true; + }, + data() { + return { + isLoading: false, + }; + }, + 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 class="btn-group"> diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue index 490df47e154..d40de95e051 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -1,21 +1,21 @@ <script> - import tooltip from '../../vue_shared/directives/tooltip'; - import icon from '../../vue_shared/components/icon.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; +import icon from '../../vue_shared/components/icon.vue'; - export default { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + components: { + icon, + }, + props: { + artifacts: { + type: Array, + required: true, }, - components: { - icon, - }, - props: { - artifacts: { - type: Array, - required: true, - }, - }, - }; + }, +}; </script> <template> <div diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 2e777783636..0d7324f3fb5 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -1,74 +1,82 @@ <script> - import Modal from '~/vue_shared/components/gl_modal.vue'; - import { s__, sprintf } from '~/locale'; - import PipelinesTableRowComponent from './pipelines_table_row.vue'; - import eventHub from '../event_hub'; +import Modal from '~/vue_shared/components/gl_modal.vue'; +import { s__, sprintf } from '~/locale'; +import PipelinesTableRowComponent from './pipelines_table_row.vue'; +import eventHub from '../event_hub'; - /** - * Pipelines Table Component. - * - * Given an array of objects, renders a table. - */ - export default { - components: { - PipelinesTableRowComponent, - Modal, +/** + * Pipelines Table Component. + * + * Given an array of objects, renders a table. + */ +export default { + components: { + PipelinesTableRowComponent, + Modal, + }, + props: { + pipelines: { + type: Array, + required: true, }, - props: { - pipelines: { - type: Array, - required: true, - }, - updateGraphDropdown: { - type: Boolean, - required: false, - default: false, - }, - autoDevopsHelpPath: { - type: String, - required: true, - }, - viewType: { - type: String, - required: true, - }, + updateGraphDropdown: { + type: Boolean, + required: false, + default: false, }, - data() { - return { - pipelineId: '', - endpoint: '', - cancelingPipeline: null, - }; + autoDevopsHelpPath: { + type: String, + required: true, }, - computed: { - modalTitle() { - return sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), { + viewType: { + type: String, + required: true, + }, + }, + data() { + return { + pipelineId: '', + endpoint: '', + cancelingPipeline: null, + }; + }, + computed: { + modalTitle() { + return sprintf( + s__('Pipeline|Stop pipeline #%{pipelineId}?'), + { pipelineId: `${this.pipelineId}`, - }, false); - }, - modalText() { - return sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), { - pipelineId: `<strong>#${this.pipelineId}</strong>`, - }, false); - }, + }, + false, + ); }, - created() { - eventHub.$on('openConfirmationModal', this.setModalData); + modalText() { + return sprintf( + s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), + { + pipelineId: `<strong>#${this.pipelineId}</strong>`, + }, + false, + ); }, - beforeDestroy() { - eventHub.$off('openConfirmationModal', this.setModalData); + }, + created() { + eventHub.$on('openConfirmationModal', this.setModalData); + }, + beforeDestroy() { + eventHub.$off('openConfirmationModal', this.setModalData); + }, + methods: { + setModalData(data) { + this.pipelineId = data.pipelineId; + this.endpoint = data.endpoint; }, - methods: { - setModalData(data) { - this.pipelineId = data.pipelineId; - this.endpoint = data.endpoint; - }, - onSubmit() { - eventHub.$emit('postAction', this.endpoint); - this.cancelingPipeline = this.pipelineId; - }, + onSubmit() { + eventHub.$emit('postAction', this.endpoint); + this.cancelingPipeline = this.pipelineId; }, - }; + }, +}; </script> <template> <div class="ci-table"> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index b2744a30c2a..804822a3ea8 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -1,255 +1,253 @@ <script> - import eventHub from '../event_hub'; - 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'; - import LoadingButton from '../../vue_shared/components/loading_button.vue'; - import Icon from '../../vue_shared/components/icon.vue'; - import { PIPELINES_TABLE } from '../constants'; +import eventHub from '../event_hub'; +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'; +import LoadingButton from '../../vue_shared/components/loading_button.vue'; +import Icon from '../../vue_shared/components/icon.vue'; +import { PIPELINES_TABLE } from '../constants'; - /** - * Pipeline table row. - * - * Given the received object renders a table row in the pipelines' table. - */ - export default { - components: { - PipelinesActionsComponent, - PipelinesArtifactsComponent, - CommitComponent, - PipelineStage, - PipelineUrl, - CiBadge, - PipelinesTimeago, - LoadingButton, - Icon, +/** + * Pipeline table row. + * + * Given the received object renders a table row in the pipelines' table. + */ +export default { + components: { + PipelinesActionsComponent, + PipelinesArtifactsComponent, + CommitComponent, + PipelineStage, + PipelineUrl, + CiBadge, + PipelinesTimeago, + LoadingButton, + Icon, + }, + props: { + pipeline: { + type: Object, + required: true, }, - props: { - pipeline: { - type: Object, - required: true, - }, - updateGraphDropdown: { - type: Boolean, - required: false, - default: false, - }, - autoDevopsHelpPath: { - type: String, - required: true, - }, - viewType: { - type: String, - required: true, - }, - cancelingPipeline: { - type: String, - required: false, - default: null, - }, + updateGraphDropdown: { + type: Boolean, + required: false, + default: false, }, - pipelinesTable: PIPELINES_TABLE, - data() { - return { - isRetrying: false, - }; + autoDevopsHelpPath: { + type: String, + required: true, }, - 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; + viewType: { + type: String, + required: true, + }, + cancelingPipeline: { + type: String, + required: false, + default: null, + }, + }, + pipelinesTable: PIPELINES_TABLE, + data() { + return { + isRetrying: false, + }; + }, + 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, { - avatar_url: this.pipeline.commit.author_gravatar_url, - }); - } - // 4. If committer is not a GitLab User he/she can have a Gravatar - } else { - commitAuthorInformation = { + // 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, - 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') { - // eslint-disable-next-line no-param-reassign - accumulator.ref_url = this.pipeline.ref[prop]; - } else { - // eslint-disable-next-line no-param-reassign - 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') { + // eslint-disable-next-line no-param-reassign + accumulator.ref_url = this.pipeline.ref[prop]; + } else { + // eslint-disable-next-line no-param-reassign + 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'; + }, - isCancelling() { - return this.cancelingPipeline === this.pipeline.id; - }, + isCancelling() { + return this.cancelingPipeline === this.pipeline.id; }, + }, - methods: { - handleCancelClick() { - eventHub.$emit('openConfirmationModal', { - pipelineId: this.pipeline.id, - endpoint: this.pipeline.cancel_path, - }); - }, - handleRetryClick() { - this.isRetrying = true; - eventHub.$emit('retryPipeline', this.pipeline.retry_path); - }, + methods: { + handleCancelClick() { + eventHub.$emit('openConfirmationModal', { + pipelineId: this.pipeline.id, + endpoint: this.pipeline.cancel_path, + }); + }, + handleRetryClick() { + this.isRetrying = true; + eventHub.$emit('retryPipeline', this.pipeline.retry_path); }, - }; + }, +}; </script> <template> <div class="commit gl-responsive-table-row"> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index b9231c002fd..56fdb858088 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -186,32 +186,27 @@ export default { </i> </button> - <ul + <div class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container" aria-labelledby="stageDropdown" > - - <li + <loading-icon v-if="isLoading"/> + <ul + v-else class="js-builds-dropdown-list scrollable-menu" > - - <loading-icon v-if="isLoading"/> - - <ul - v-else + <li + v-for="job in dropdownContent" + :key="job.id" > - <li - v-for="job in dropdownContent" - :key="job.id" - > - <job-component - :job="job" - css-class-job-name="mini-pipeline-graph-dropdown-item" - @pipelineActionRequestComplete="pipelineActionRequestComplete" - /> - </li> - </ul> - </li> - </ul> + <job-component + :dropdown-length="dropdownContent.length" + :job="job" + css-class-job-name="mini-pipeline-graph-dropdown-item" + @pipelineActionRequestComplete="pipelineActionRequestComplete" + /> + </li> + </ul> + </div> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue index 0a97df2dc18..cd43d78de40 100644 --- a/app/assets/javascripts/pipelines/components/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/time_ago.vue @@ -1,60 +1,58 @@ <script> - import iconTimerSvg from 'icons/_icon_timer.svg'; - import '../../lib/utils/datetime_utility'; - import tooltip from '../../vue_shared/directives/tooltip'; - import timeagoMixin from '../../vue_shared/mixins/timeago'; +import iconTimerSvg from 'icons/_icon_timer.svg'; +import '../../lib/utils/datetime_utility'; +import tooltip from '../../vue_shared/directives/tooltip'; +import timeagoMixin from '../../vue_shared/mixins/timeago'; - export default { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + mixins: [timeagoMixin], + props: { + finishedTime: { + type: String, + required: true, }, - mixins: [ - timeagoMixin, - ], - props: { - finishedTime: { - type: String, - required: true, - }, - duration: { - type: Number, - required: true, - }, + duration: { + type: Number, + required: true, }, - data() { - return { - iconTimerSvg, - }; + }, + data() { + return { + iconTimerSvg, + }; + }, + computed: { + hasDuration() { + return this.duration > 0; }, - computed: { - hasDuration() { - return this.duration > 0; - }, - hasFinishedTime() { - return this.finishedTime !== ''; - }, - durationFormated() { - const date = new Date(this.duration * 1000); + hasFinishedTime() { + return this.finishedTime !== ''; + }, + durationFormated() { + const date = new Date(this.duration * 1000); - let hh = date.getUTCHours(); - let mm = date.getUTCMinutes(); - let ss = date.getSeconds(); + let hh = date.getUTCHours(); + let mm = date.getUTCMinutes(); + let ss = date.getSeconds(); - // left pad - if (hh < 10) { - hh = `0${hh}`; - } - if (mm < 10) { - mm = `0${mm}`; - } - if (ss < 10) { - ss = `0${ss}`; - } + // left pad + if (hh < 10) { + hh = `0${hh}`; + } + if (mm < 10) { + mm = `0${mm}`; + } + if (ss < 10) { + ss = `0${ss}`; + } - return `${hh}:${mm}:${ss}`; - }, + return `${hh}:${mm}:${ss}`; }, - }; + }, +}; </script> <template> <div class="table-section section-15 pipelines-time-ago"> diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index 30b1eee186d..2cb558b0dec 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -75,8 +75,7 @@ export default { // Stop polling this.poll.stop(); // Update the table - return this.getPipelines() - .then(() => this.poll.restart()); + return this.getPipelines().then(() => this.poll.restart()); }, fetchPipelines() { if (!this.isMakingRequest) { @@ -86,9 +85,10 @@ export default { } }, getPipelines() { - return this.service.getPipelines(this.requestData) + return this.service + .getPipelines(this.requestData) .then(response => this.successCallback(response)) - .catch((error) => this.errorCallback(error)); + .catch(error => this.errorCallback(error)); }, setCommonData(pipelines) { this.store.storePipelines(pipelines); @@ -118,7 +118,8 @@ export default { } }, postAction(endpoint) { - this.service.postAction(endpoint) + this.service + .postAction(endpoint) .then(() => this.fetchPipelines()) .catch(() => Flash(__('An error occurred while making the request.'))); }, diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index cf3ff48e608..dc9befe6349 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -31,7 +31,8 @@ export default () => { requestRefreshPipelineGraph() { // When an action is clicked // (wether in the dropdown or in the main nodes, we refresh the big graph) - this.mediator.refreshPipeline() + this.mediator + .refreshPipeline() .catch(() => Flash(__('An error occurred while making the request.'))); }, }, diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js index 5633e54b28a..bd1e1895660 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js +++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js @@ -52,7 +52,8 @@ export default class pipelinesMediator { refreshPipeline() { this.poll.stop(); - return this.service.getPipeline() + return this.service + .getPipeline() .then(response => this.successCallback(response)) .catch(() => this.errorCallback()) .finally(() => this.poll.restart()); diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 240dde56325..bce7556bd40 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -47,7 +47,10 @@ export default function projectSelect() { projectsCallback = finalCallback; } if (_this.groupId) { - return Api.groupProjects(_this.groupId, query.term, projectsCallback); + return Api.groupProjects(_this.groupId, query.term, { + with_issues_enabled: _this.withIssuesEnabled, + with_merge_requests_enabled: _this.withMergeRequestsEnabled, + }, projectsCallback); } else { return Api.projects(query.term, { order_by: _this.orderBy, diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js index 060f374310c..8681a1776c6 100644 --- a/app/assets/javascripts/shared/milestones/form.js +++ b/app/assets/javascripts/shared/milestones/form.js @@ -8,10 +8,11 @@ export default (initGFM = true) => { new DueDateSelectors(); // eslint-disable-line no-new // eslint-disable-next-line no-new new GLForm($('.milestone-form'), { - emojis: initGFM, + emojis: true, members: initGFM, issues: initGFM, mergeRequests: initGFM, + epics: initGFM, milestones: initGFM, labels: initGFM, }); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index c44419d24e6..5e464f8a0e2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -1,4 +1,5 @@ <script> +import Icon from '~/vue_shared/components/icon.vue'; import timeagoMixin from '../../vue_shared/mixins/timeago'; import tooltip from '../../vue_shared/directives/tooltip'; import LoadingButton from '../../vue_shared/components/loading_button.vue'; @@ -14,6 +15,7 @@ export default { LoadingButton, MemoryUsage, StatusIcon, + Icon, }, directives: { tooltip, @@ -110,11 +112,10 @@ export default { class="deploy-link js-deploy-url" > {{ deployment.external_url_formatted }} - <i - class="fa fa-external-link" - aria-hidden="true" - > - </i> + <icon + :size="16" + name="external-link" + /> </a> </template> <span 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 index 1fdc3218671..53c4dc8c8f4 100644 --- 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 @@ -32,7 +32,7 @@ }; </script> <template> - <div class="space-children flex-container-block append-right-10"> + <div class="space-children d-flex append-right-10"> <div v-if="isLoading" class="mr-widget-icon" 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 index 0d9a560c88e..97f4196b94d 100644 --- 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 @@ -82,7 +82,7 @@ <div class="mr-widget-body media"> <status-icon status="success" /> <div class="media-body"> - <h4 class="flex-container-block"> + <h4 class="d-flex align-items-start"> <span class="append-right-10"> {{ s__("mrWidget|Set by") }} <mr-widget-author :author="mr.setToMWPSBy" /> @@ -119,7 +119,7 @@ </p> <p v-else - class="flex-container-block" + class="d-flex align-items-start" > <span class="append-right-10"> {{ s__("mrWidget|The source branch will not be removed") }} diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index fba67681777..298971a36b2 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -67,6 +67,7 @@ members: this.enableAutocomplete, issues: this.enableAutocomplete, mergeRequests: this.enableAutocomplete, + epics: this.enableAutocomplete, milestones: this.enableAutocomplete, labels: this.enableAutocomplete, }); diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index f610a1aea08..ded33e8b151 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -128,6 +128,11 @@ table { border-spacing: 0; } +.tooltip { + // Fix bootstrap4 bug whereby tooltips flicker when they are hovered over their borders + pointer-events: none; +} + .popover { font-size: 14px; } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 1d4828be223..340fddd398b 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -350,11 +350,6 @@ } } -.flex-container-block { - display: -webkit-flex; - display: flex; -} - .flex-right { margin-left: auto; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 326499125fc..218e37602dd 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -262,12 +262,7 @@ li.note { } .milestone { - &.milestone-closed { - background: $gray-light; - } - .progress { - margin-bottom: 0; margin-top: 4px; box-shadow: none; background-color: $border-gray-light; diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index 9cbaaa5dc8d..ea4cb9a0b75 100644 --- a/app/assets/stylesheets/framework/contextual_sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss @@ -68,8 +68,7 @@ } .nav-sidebar { - transition: width $sidebar-transition-duration, - left $sidebar-transition-duration; + transition: width $sidebar-transition-duration, left $sidebar-transition-duration; position: fixed; z-index: 400; width: $contextual-sidebar-width; @@ -77,12 +76,12 @@ bottom: 0; left: 0; background-color: $gray-light; - box-shadow: inset -2px 0 0 $border-color; + box-shadow: inset -1px 0 0 $border-color; transform: translate3d(0, 0, 0); &:not(.sidebar-collapsed-desktop) { @media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) { - box-shadow: inset -2px 0 0 $border-color, + box-shadow: inset -1px 0 0 $border-color, 2px 1px 3px $dropdown-shadow-color; } } @@ -214,7 +213,7 @@ > li { > a { @include media-breakpoint-up(sm) { - margin-right: 2px; + margin-right: 1px; } &:hover { @@ -224,7 +223,7 @@ &.is-showing-fly-out { > a { - margin-right: 2px; + margin-right: 1px; } .sidebar-sub-level-items { @@ -317,14 +316,14 @@ .toggle-sidebar-button, .close-nav-button { - width: $contextual-sidebar-width - 2px; + width: $contextual-sidebar-width - 1px; transition: width $sidebar-transition-duration; position: fixed; bottom: 0; padding: $gl-padding; background-color: $gray-light; border: 0; - border-top: 2px solid $border-color; + border-top: 1px solid $border-color; color: $gl-text-color-secondary; display: flex; align-items: center; @@ -379,7 +378,7 @@ .toggle-sidebar-button { padding: 16px; - width: $contextual-sidebar-collapsed-width - 2px; + width: $contextual-sidebar-collapsed-width - 1px; .collapse-text, .icon-angle-double-left { diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index f060254777c..00eac1688f2 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -322,14 +322,17 @@ span.idiff { } .file-title-flex-parent { - display: flex; - align-items: center; - justify-content: space-between; - background-color: $gray-light; - border-bottom: 1px solid $border-color; - padding: 5px $gl-padding; - margin: 0; - border-radius: $border-radius-default $border-radius-default 0 0; + &, + .file-holder & { + display: flex; + align-items: center; + justify-content: space-between; + background-color: $gray-light; + border-bottom: 1px solid $border-color; + padding: 5px $gl-padding; + margin: 0; + border-radius: $border-radius-default $border-radius-default 0 0; + } .file-header-content { white-space: nowrap; @@ -337,6 +340,17 @@ span.idiff { text-overflow: ellipsis; padding-right: 30px; position: relative; + width: auto; + + @media (max-width: map-get($grid-breakpoints, sm)-1) { + width: 100%; + } + } + + .file-holder & { + .file-actions { + position: static; + } } .btn-clipboard { diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 2b2e6d69e33..282e424fc38 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -243,3 +243,15 @@ label { } } } + +.input-icon-wrapper { + position: relative; + + .input-icon-right { + position: absolute; + right: 0.8em; + top: 50%; + transform: translateY(-50%); + color: $theme-gray-600; + } +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 5789c3fa1b1..8bcaf5eb6ac 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -268,8 +268,6 @@ .navbar-sub-nav, .navbar-nav { - align-items: center; - > li { > a:hover, > a:focus { diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 1d247671761..86de88729ee 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -45,4 +45,9 @@ &.status-box-upcoming { background: $gl-text-color-secondary; } + + &.status-box-milestone { + color: $gl-text-color; + background: $gray-darker; + } } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 49226ae8eac..f75be4e01cd 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -261,12 +261,16 @@ vertical-align: baseline; } - a.autodevops-badge { - color: $white-light; - } + a { + color: $gl-text-color; - a.autodevops-link { - color: $gl-link-color; + &.autodevops-badge { + color: $white-light; + } + + &.autodevops-link { + color: $gl-link-color; + } } .commit-row-description { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index d652982410a..a90a9c6e486 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -14,8 +14,8 @@ background-color: $gray-normal; } - .diff-toggle-caret { - padding-right: 6px; + svg { + vertical-align: text-bottom; } } @@ -737,6 +737,10 @@ max-width: 560px; width: 100%; z-index: 150; + min-height: $dropdown-min-height; + max-height: $dropdown-max-height; + overflow-y: auto; + margin-bottom: 0; @include media-breakpoint-up(sm) { left: $gl-padding; diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 79cac7f4ff0..391dfea0703 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -79,6 +79,7 @@ justify-content: space-between; padding: $gl-padding; border-radius: $border-radius-default; + border: 1px solid $theme-gray-100; &.sortable-ghost { opacity: 0.3; @@ -89,6 +90,7 @@ cursor: move; cursor: -webkit-grab; cursor: -moz-grab; + border: 0; &:active { cursor: -webkit-grabbing; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index ccf5d632614..efd730af558 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -737,6 +737,10 @@ > *:not(:last-child) { margin-right: .3em; } + + svg { + vertical-align: text-top; + } } .deploy-link { diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index dba83e56d72..46437ce5841 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -3,8 +3,20 @@ } .milestones { + padding: $gl-padding-8; + margin-top: $gl-padding-8; + border-radius: $border-radius-default; + background-color: $theme-gray-100; + .milestone { - padding: 10px 16px; + border: 0; + padding: $gl-padding-top $gl-padding; + border-radius: $border-radius-default; + background-color: $white-light; + + &:not(:last-child) { + margin-bottom: $gl-padding-4; + } h4 { font-weight: $gl-font-weight-bold; @@ -13,6 +25,24 @@ .progress { width: 100%; height: 6px; + margin-bottom: $gl-padding-4; + } + + .milestone-progress { + a { + color: $gl-link-color; + } + } + + .status-box { + font-size: $tooltip-font-size; + margin-top: 0; + margin-right: $gl-padding-4; + + @include media-breakpoint-down(xs) { + line-height: unset; + padding: $gl-padding-4 $gl-input-padding; + } } } } @@ -229,6 +259,10 @@ } } +.milestone-range { + color: $gl-text-color-tertiary; +} + @include media-breakpoint-down(xs) { .milestone-banner-text, .milestone-banner-link { diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 2f28031b9c8..e264b06c4b2 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -255,25 +255,12 @@ } } -.modal-doorkeepr-auth, -.doorkeeper-app-form { - .scope-description { - color: $theme-gray-700; - } -} - .modal-doorkeepr-auth { .modal-body { padding: $gl-padding; } } -.doorkeeper-app-form { - .scope-description { - margin: 0 0 5px 17px; - } -} - .deprecated-service { cursor: default; } diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index fb788c47ef1..6944857bd33 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -52,8 +52,7 @@ class Admin::HooksController < Admin::ApplicationController end def hook_logs - @hook_logs ||= - Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page]) + @hook_logs ||= hook.web_hook_logs.recent.page(params[:page]) end def hook_params diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index dd7aa1a67b9..6800d742b0a 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -58,8 +58,7 @@ class Projects::HooksController < Projects::ApplicationController end def hook_logs - @hook_logs ||= - Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page]) + @hook_logs ||= hook.web_hook_logs.recent.page(params[:page]) end def hook_params diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb index ee4ed674110..3f4962b543d 100644 --- a/app/controllers/projects/lfs_api_controller.rb +++ b/app/controllers/projects/lfs_api_controller.rb @@ -93,7 +93,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController end def lfs_check_batch_operation! - if upload_request? && Gitlab::Database.read_only? + if batch_operation_disallowed? render( json: { message: lfs_read_only_message @@ -105,6 +105,11 @@ class Projects::LfsApiController < Projects::GitHttpClientController end # Overridden in EE + def batch_operation_disallowed? + upload_request? && Gitlab::Database.read_only? + end + + # Overridden in EE def lfs_read_only_message _('You cannot write to this read-only GitLab instance.') end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index f85dcfe6bfc..594563d1f6f 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -77,7 +77,7 @@ class Projects::MilestonesController < Projects::ApplicationController def promote promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone) - flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\">group milestone</a>.".html_safe + flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\"><u>group milestone</u></a>.".html_safe respond_to do |format| format.html do redirect_to project_milestones_path(project) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 5d5f72c4d86..6fdfd964fca 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -7,7 +7,7 @@ # current_user - which user use # params: # scope: 'created_by_me' or 'assigned_to_me' or 'all' -# state: 'opened' or 'closed' or 'all' +# state: 'opened' or 'closed' or 'locked' or 'all' # group_id: integer # project_id: integer # milestone_title: string @@ -311,6 +311,8 @@ class IssuableFinder items.respond_to?(:merged) ? items.merged : items.closed when 'opened' items.opened + when 'locked' + items.where(state: 'locked') else items end diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 8d84ed4bdfb..40089c082c1 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -6,7 +6,7 @@ # current_user - which user use # params: # scope: 'created_by_me' or 'assigned_to_me' or 'all' -# state: 'open', 'closed', 'merged', or 'all' +# state: 'open', 'closed', 'merged', 'locked', or 'all' # group_id: integer # project_id: integer # milestone_title: string diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 82a7931c557..097be8a0643 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -108,7 +108,7 @@ module MergeRequestsHelper data_attrs = { action: tab.to_s, target: "##{tab}", - toggle: options.fetch(:force_link, false) ? '' : 'tab' + toggle: options.fetch(:force_link, false) ? '' : 'tabvue' } url = case tab diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index e1a0cf1604c..3fa2e5452c8 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -148,6 +148,7 @@ module NotesHelper members: autocomplete, issues: autocomplete, mergeRequests: autocomplete, + epics: autocomplete, milestones: autocomplete, labels: autocomplete } diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb index e72c125fb69..59a1f2aed69 100644 --- a/app/models/hooks/web_hook_log.rb +++ b/app/models/hooks/web_hook_log.rb @@ -7,6 +7,11 @@ class WebHookLog < ActiveRecord::Base validates :web_hook, presence: true + def self.recent + where('created_at >= ?', 2.days.ago.beginning_of_day) + .order(created_at: :desc) + end + def success? response_status =~ /^2/ end diff --git a/app/models/issue.rb b/app/models/issue.rb index d3df2da14e2..4715d942c8d 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -48,7 +48,7 @@ class Issue < ActiveRecord::Base scope :unassigned, -> { where('NOT EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') } scope :assigned_to, ->(u) { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = ? AND issue_id = issues.id)', u.id)} - scope :with_due_date, -> { where('due_date IS NOT NULL') } + scope :with_due_date, -> { where.not(due_date: nil) } scope :without_due_date, -> { where(due_date: nil) } scope :due_before, ->(date) { where('issues.due_date < ?', date) } scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) } @@ -56,7 +56,7 @@ class Issue < ActiveRecord::Base scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } - scope :order_closest_future_date, -> { reorder('CASE WHEN due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - due_date) ASC') } + scope :order_closest_future_date, -> { reorder('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC') } scope :preload_associations, -> { preload(:labels, project: :namespace) } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6c96c8ca391..b4090fd8baf 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -128,14 +128,9 @@ class MergeRequest < ActiveRecord::Base end after_transition unchecked: :cannot_be_merged do |merge_request, transition| - begin - if merge_request.notify_conflict? - NotificationService.new.merge_request_unmergeable(merge_request) - TodoService.new.merge_request_became_unmergeable(merge_request) - end - rescue Gitlab::Git::CommandError - # Checking mergeability can trigger exception, e.g. non-utf8 - # We ignore this type of errors. + if merge_request.notify_conflict? + NotificationService.new.merge_request_unmergeable(merge_request) + TodoService.new.merge_request_became_unmergeable(merge_request) end end @@ -707,7 +702,14 @@ class MergeRequest < ActiveRecord::Base end def notify_conflict? - (opened? || locked?) && !project.repository.can_be_merged?(diff_head_sha, target_branch) + (opened? || locked?) && + has_commits? && + !branch_missing? && + !project.repository.can_be_merged?(diff_head_sha, target_branch) + rescue Gitlab::Git::CommandError + # Checking mergeability can trigger exception, e.g. non-utf8 + # We ignore this type of errors. + false end def related_notes diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 7f4c47a6d14..edc5c00d9c4 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -67,11 +67,11 @@ class BambooService < CiService def execute(data) return unless supported_events.include?(data[:object_kind]) - get_path("updateAndBuild.action?buildKey=#{build_key}") + get_path("updateAndBuild.action", { buildKey: build_key }) end def calculate_reactive_cache(sha, ref) - response = get_path("rest/api/latest/result?label=#{sha}") + response = get_path("rest/api/latest/result/byChangeset/#{sha}") { build_page: read_build_page(response), commit_status: read_commit_status(response) } end @@ -113,18 +113,20 @@ class BambooService < CiService URI.join("#{bamboo_url}/", path).to_s end - def get_path(path) + def get_path(path, query_params = {}) url = build_url(path) if username.blank? && password.blank? - Gitlab::HTTP.get(url, verify: false) + Gitlab::HTTP.get(url, verify: false, query: query_params) else - url << '&os_authType=basic' - Gitlab::HTTP.get(url, verify: false, - basic_auth: { - username: username, - password: password - }) + query_params[:os_authType] = 'basic' + Gitlab::HTTP.get(url, + verify: false, + query: query_params, + basic_auth: { + username: username, + password: password + }) end end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 3056c20516a..5f9894f1168 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -99,11 +99,11 @@ class Repository "#<#{self.class.name}:#{@disk_path}>" end - def commit(ref = 'HEAD') + def commit(ref = nil) return nil unless exists? return ref if ref.is_a?(::Commit) - find_commit(ref) + find_commit(ref || root_ref) end # Finding a commit by the passed SHA @@ -283,6 +283,10 @@ class Repository ) end + def cached_methods + CACHED_METHODS + end + def expire_tags_cache expire_method_caches(%i(tag_names tag_count)) @tags = nil @@ -423,7 +427,7 @@ class Repository # Runs code after the HEAD of a repository is changed. def after_change_head - expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys) + expire_all_method_caches end # Runs code after a repository has been forked/imported. diff --git a/app/models/user.rb b/app/models/user.rb index 8e0dc91b2a7..48629c58490 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -244,7 +244,7 @@ class User < ActiveRecord::Base scope :blocked, -> { with_states(:blocked, :ldap_blocked) } scope :external, -> { where(external: true) } scope :active, -> { with_state(:active).non_internal } - scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') } + scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) } scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) } scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) } scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) } diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index ad655a7b3f4..d4d622d84ab 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -27,6 +27,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated def statistics_buttons(show_auto_devops_callout:) [ + readme_anchor_data, changelog_anchor_data, license_anchor_data, contribution_guide_anchor_data, @@ -212,11 +213,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def readme_anchor_data - if current_user && can_current_user_push_to_default_branch? && repository.readme.blank? + if current_user && can_current_user_push_to_default_branch? && repository.readme.nil? OpenStruct.new(enabled: false, label: _('Add Readme'), link: add_readme_path) - elsif repository.readme.present? + elsif repository.readme OpenStruct.new(enabled: true, label: _('Readme'), link: default_view != 'readme' ? readme_path : '#readme') diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index ca4480fe2b1..2de9624aed4 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -35,7 +35,7 @@ class BuildDetailsEntity < JobEntity def build_failed_issue_options { title: "Job Failed ##{build.id}", - description: "Job [##{build.id}](#{project_job_path(project, build)}) failed for #{build.sha}:\n" } + description: "Job [##{build.id}](#{project_job_url(project, build)}) failed for #{build.sha}:\n" } end def current_user diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 78e79344c99..6e5c29a5c40 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -58,7 +58,8 @@ module Issues def cloneable_label_ids params = { project_id: @new_project.id, - title: @old_issue.labels.pluck(:title) + title: @old_issue.labels.pluck(:title), + include_ancestor_groups: true } LabelsFinder.new(current_user, params).execute.pluck(:id) diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 3cdeb103bb8..18f2c1a509f 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -2,7 +2,7 @@ - breadcrumb_title "Dashboard" %div{ class: container_class } - = render_if_exists "admin/licenses/breakdown", license: @license + = render_if_exists 'admin/licenses/breakdown', license: @license .admin-dashboard.prepend-top-default .row @@ -22,7 +22,7 @@ %h3.text-center Users: = approximate_count_with_delimiters(@counts, User) - = render_if_exists 'users_statistics' + = render_if_exists 'admin/dashboard/users_statistics' %hr = link_to 'New user', new_admin_user_path, class: "btn btn-new" .col-sm-4 @@ -101,7 +101,7 @@ %span.light.float-right = boolean_to_icon Gitlab::IncomingEmail.enabled? - = render_if_exists 'elastic_and_geo' + = render_if_exists 'admin/dashboard/elastic_and_geo' - container_reg = "Container Registry" %p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") } @@ -151,7 +151,7 @@ %span.float-right = Gitlab::Pages::VERSION - = render_if_exists 'geo' + = render_if_exists 'admin/dashboard/geo' %p Ruby diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml index 231c0f70882..946d868da01 100644 --- a/app/views/admin/identities/_form.html.haml +++ b/app/views/admin/identities/_form.html.haml @@ -7,10 +7,10 @@ - values = Gitlab::Auth::OAuth::Provider.providers.map { |name| ["#{Gitlab::Auth::OAuth::Provider.label_for(name)} (#{name})", name] } = f.select :provider, values, { allow_blank: false }, class: 'form-control' .form-group.row - = f.label :extern_uid, "Identifier", class: 'col-form-label col-sm-2' + = f.label :extern_uid, _("Identifier"), class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :extern_uid, class: 'form-control', required: true .form-actions - = f.submit 'Save changes', class: "btn btn-save" + = f.submit _('Save changes'), class: "btn btn-save" diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml index 50fe9478a78..5ed59809db5 100644 --- a/app/views/admin/identities/_identity.html.haml +++ b/app/views/admin/identities/_identity.html.haml @@ -5,8 +5,8 @@ = identity.extern_uid %td = link_to edit_admin_user_identity_path(@user, identity), class: 'btn btn-sm btn-grouped' do - Edit + = _("Edit") = link_to [:admin, @user, identity], method: :delete, class: 'btn btn-sm btn-danger', - data: { confirm: "Are you sure you want to remove this identity?" } do - Delete + data: { confirm: _("Are you sure you want to remove this identity?") } do + = _('Delete') diff --git a/app/views/admin/identities/edit.html.haml b/app/views/admin/identities/edit.html.haml index 515d46b0f29..1ad6ce969cb 100644 --- a/app/views/admin/identities/edit.html.haml +++ b/app/views/admin/identities/edit.html.haml @@ -1,6 +1,6 @@ -- page_title "Edit", @identity.provider, "Identities", @user.name, "Users" +- page_title _("Edit"), @identity.provider, _("Identities"), @user.name, _("Users") %h3.page-title - Edit identity for #{@user.name} + = _('Edit identity for %{user_name}') % { user_name: @user.name } %hr = render 'form' diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml index ee51fb3fda1..59373ee6752 100644 --- a/app/views/admin/identities/index.html.haml +++ b/app/views/admin/identities/index.html.haml @@ -1,15 +1,15 @@ -- page_title "Identities", @user.name, "Users" +- page_title _("Identities"), @user.name, _("Users") = render 'admin/users/head' -= link_to 'New identity', new_admin_user_identity_path, class: 'float-right btn btn-new' += link_to _('New identity'), new_admin_user_identity_path, class: 'float-right btn btn-new' - if @identities.present? .table-holder %table.table %thead %tr - %th Provider - %th Identifier + %th= _('Provider') + %th= _('Identifier') %th = render @identities - else - %h4 This user has no identities + %h4= _('This user has no identities') diff --git a/app/views/admin/identities/new.html.haml b/app/views/admin/identities/new.html.haml index e30bf0ef0ee..ee743b0fd3c 100644 --- a/app/views/admin/identities/new.html.haml +++ b/app/views/admin/identities/new.html.haml @@ -1,4 +1,4 @@ -- page_title "New Identity" -%h3.page-title New identity +- page_title _("New Identity") +%h3.page-title= _('New identity') %hr = render 'form' diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index 6d9c6b5572a..28cdc7607e0 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -35,7 +35,7 @@ - @pre_auth.scopes.each do |scope| %li %strong= t scope, scope: [:doorkeeper, :scopes] - .scope-description= t scope, scope: [:doorkeeper, :scope_desc] + .text-secondary= t scope, scope: [:doorkeeper, :scope_desc] .form-actions.text-right = form_tag oauth_authorization_path, method: :delete, class: 'inline' do = hidden_field_tag :client_id, @pre_auth.client.uid diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 8037cf4b69d..5e1ae1dbe38 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -9,7 +9,7 @@ = render 'shared/issuable/nav', type: :issues .nav-controls = render 'shared/issuable/feed_buttons' - = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues + = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues, with_feature_enabled: 'issues' = render 'shared/issuable/search_bar', type: :issues diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index 4ccd16f3e11..e2a317dbf67 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -7,7 +7,7 @@ = render 'shared/issuable/nav', type: :merge_requests - if current_user .nav-controls - = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests, with_feature_enabled: 'merge_requests' = render 'shared/issuable/search_bar', type: :merge_requests diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 5cec443e969..d8e32651b36 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -21,7 +21,7 @@ - if current_user = render 'layouts/header/new_dropdown' - if header_link?(:search) - %li.nav-item.d-none.d-sm-none.d-md-block + %li.nav-item.d-none.d-sm-none.d-md-block.m-auto = render 'layouts/search' unless current_controller?(:search) %li.nav-item.d-inline-block.d-sm-none.d-md-none = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml index db57da99ec7..d45ae6ec91f 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/projects/clusters/user/_form.html.haml @@ -3,9 +3,10 @@ .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light' = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') - .form-group - = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-light' - = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') + - if has_multiple_clusters?(@project) + .form-group + = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-light' + = field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope') = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| .form-group diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml index b2eacabc21a..f7a5d85500f 100644 --- a/app/views/projects/merge_requests/creations/_new_submit.html.haml +++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml @@ -24,23 +24,28 @@ There are no commits yet. = custom_icon ('illustration_no_commits') - else - %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom - %li.commits-tab - = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do - Commits - %span.badge.badge-pill= @commits.size - - if @pipelines.any? - %li.builds-tab - = link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do - Pipelines - %span.badge.badge-pill= @pipelines.size - %li.diffs-tab - = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do - Changes - %span.badge.badge-pill= @merge_request.diff_size + .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } + .merge-request-tabs-container + .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom.js-tabs-affix + %li.commits-tab.new-tab + = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tabvue'} do + Commits + %span.badge.badge-pill= @commits.size + - if @pipelines.any? + %li.builds-tab + = link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tabvue'} do + Pipelines + %span.badge.badge-pill= @pipelines.size + %li.diffs-tab + = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tabvue'} do + Changes + %span.badge.badge-pill= @merge_request.diff_size - .tab-content - #commits.commits.tab-pane.active + #diff-notes-app.tab-content + #new.commits.tab-pane.active = render "projects/merge_requests/commits" #diffs.diffs.tab-pane -# This tab is always loaded via AJAX diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index 4359362bb05..31c2616d283 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -63,4 +63,4 @@ .form-text.text-muted = s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted } - = f.submit 'Save changes', class: "btn btn-success prepend-top-15" + = f.submit _('Save changes'), class: "btn btn-success prepend-top-15" diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index 7142a9d635e..5025460a2d0 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -4,20 +4,20 @@ = form_errors(@project) %fieldset.builds-feature .form-group.append-bottom-default.js-secret-runner-token - = f.label :runners_token, "Runner token", class: 'label-light' + = f.label :runners_token, _("Runner token"), class: 'label-light' .form-control.js-secret-value-placeholder = '*' * 20 = f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89' - %p.form-text.text-muted The secure token used by the Runner to checkout the project + %p.form-text.text-muted= _("The secure token used by the Runner to checkout the project") %button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } } = _('Reveal value') %hr .form-group %h5.prepend-top-0 - Git strategy for pipelines + = _("Git strategy for pipelines") %p - Choose between <code>clone</code> or <code>fetch</code> to get the recent application code + = _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code") = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank' .form-check = f.radio_button :build_allow_git_fetch, 'false', { class: 'form-check-input' } @@ -25,29 +25,29 @@ %strong git clone %br %span.descr - Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job + = _("Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job") .form-check = f.radio_button :build_allow_git_fetch, 'true', { class: 'form-check-input' } = f.label :build_allow_git_fetch_true, class: 'form-check-label' do %strong git fetch %br %span.descr - Faster as it re-uses the project workspace (falling back to clone if it doesn't exist) + = _("Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)") %hr .form-group - = f.label :build_timeout_human_readable, 'Timeout', class: 'label-light' + = f.label :build_timeout_human_readable, _('Timeout'), class: 'label-light' = f.text_field :build_timeout_human_readable, class: 'form-control' %p.form-text.text-muted - Per job. If a job passes this threshold, it will be marked as failed + = _("Per job. If a job passes this threshold, it will be marked as failed") = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank' %hr .form-group - = f.label :ci_config_path, 'Custom CI config path', class: 'label-light' + = f.label :ci_config_path, _('Custom CI config path'), class: 'label-light' = f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml' %p.form-text.text-muted - The path to CI config file. Defaults to <code>.gitlab-ci.yml</code> + = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>") = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank' %hr @@ -55,36 +55,35 @@ .form-check = f.check_box :public_builds, { class: 'form-check-input' } = f.label :public_builds, class: 'form-check-label' do - %strong Public pipelines + %strong= _("Public pipelines") .form-text.text-muted - Allow public access to pipelines and job details, including output logs and artifacts + = _("Allow public access to pipelines and job details, including output logs and artifacts") = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank' .bs-callout.bs-callout-info - %p If enabled: + %p #{_("If enabled")}: %ul %li - For public projects, anyone can view pipelines and access job details (output logs and artifacts) + = _("For public projects, anyone can view pipelines and access job details (output logs and artifacts)") %li - For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts) + = _("For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)") %li - For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts) + = _("For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)") %p - If disabled, the access level will depend on the user's - permissions in the project. + = _("If disabled, the access level will depend on the user's permissions in the project.") %hr .form-group .form-check = f.check_box :auto_cancel_pending_pipelines, { class: 'form-check-input' }, 'enabled', 'disabled' = f.label :auto_cancel_pending_pipelines, class: 'form-check-label' do - %strong Auto-cancel redundant, pending pipelines + %strong= _("Auto-cancel redundant, pending pipelines") .form-text.text-muted - New pipelines will cancel older, pending pipelines on the same branch + = _("New pipelines will cancel older, pending pipelines on the same branch") = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'auto-cancel-pending-pipelines'), target: '_blank' %hr .form-group - = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light' + = f.label :build_coverage_regex, _("Test coverage parsing"), class: 'label-light' .input-group %span.input-group-prepend .input-group-text / @@ -92,11 +91,10 @@ %span.input-group-append .input-group-text / %p.form-text.text-muted - A regular expression that will be used to find the test coverage - output in the job trace. Leave blank to disable + = _("A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable") = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing'), target: '_blank' .bs-callout.bs-callout-info - %p Below are examples of regex for existing tools: + %p= _("Below are examples of regex for existing tools:") %ul %li Simplecov (Ruby) - @@ -120,7 +118,7 @@ JaCoCo (Java/Kotlin) %code Total.*?([0-9]{1,3})% - = f.submit 'Save changes', class: "btn btn-save" + = f.submit _('Save changes'), class: "btn btn-save" %hr diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 4d096699a70..be22bbd7a9b 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -1,6 +1,6 @@ - @content_class = "limit-container-width" unless fluid_layout -- page_title "CI / CD Settings" -- page_title "CI / CD" +- page_title _("CI / CD Settings") +- page_title _("CI / CD") - expanded = Rails.env.test? - general_expanded = @project.errors.empty? ? expanded : true @@ -8,11 +8,11 @@ %section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) } .settings-header %h4 - General pipelines + = _("General pipelines") %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + = expanded ? _('Collapse') : _('Expand') %p - Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report. + = _("Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.") .settings-content = render 'form' @@ -31,11 +31,11 @@ %section.qa-runners-settings.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 - Runners + = _("Runners") %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + = expanded ? _('Collapse') : _('Expand') %p - Register and see your runners for this project. + = _("Register and see your runners for this project.") .settings-content = render 'projects/runners/index' @@ -45,7 +45,7 @@ = _('Variables') = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer' %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + = expanded ? _('Collapse') : _('Expand') %p.append-bottom-0 = render "ci/variables/content" .settings-content @@ -54,12 +54,10 @@ %section.settings.no-animate#js-pipeline-triggers{ class: ('expanded' if expanded) } .settings-header %h4 - Pipeline triggers + = _("Pipeline triggers") %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + = expanded ? _('Collapse') : _('Expand') %p - Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will - impersonate their associated user including their access to projects and their project - permissions. + = _("Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will impersonate their associated user including their access to projects and their project permissions.") .settings-content = render 'projects/triggers/index' diff --git a/app/views/projects/settings/integrations/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml index 77d88aed883..ef445f2e139 100644 --- a/app/views/projects/settings/integrations/_project_hook.html.haml +++ b/app/views/projects/settings/integrations/_project_hook.html.haml @@ -8,9 +8,9 @@ %span.badge.badge-gray.deploy-project-label= event.to_s.titleize .col-md-4.col-lg-5.text-right-lg.prepend-top-5 %span.append-right-10.inline - SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'} - = link_to 'Edit', edit_project_hook_path(@project, hook), class: 'btn btn-sm' + #{_("SSL Verification")}: #{hook.enable_ssl_verification ? _('enabled') : _('disabled')} + = link_to _('Edit'), edit_project_hook_path(@project, hook), class: 'btn btn-sm' = render 'shared/web_hooks/test_button', triggers: ProjectHook.triggers, hook: hook, button_class: 'btn-sm' - = link_to project_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-transparent' do - %span.sr-only Remove + = link_to project_hook_path(@project, hook), data: { confirm: _('Are you sure?') }, method: :delete, class: 'btn btn-transparent' do + %span.sr-only= _("Remove") = icon('trash') diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml index 2f1a548e119..76770290f36 100644 --- a/app/views/projects/settings/integrations/show.html.haml +++ b/app/views/projects/settings/integrations/show.html.haml @@ -1,5 +1,5 @@ - @content_class = "limit-container-width" unless fluid_layout -- breadcrumb_title "Integrations Settings" -- page_title 'Integrations' +- breadcrumb_title _("Integrations Settings") +- page_title _('Integrations') = render 'projects/hooks/index' = render 'projects/services/index' diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml index ea2cd36b212..5fca734222b 100644 --- a/app/views/projects/settings/members/show.html.haml +++ b/app/views/projects/settings/members/show.html.haml @@ -1,5 +1,5 @@ - @content_class = "limit-container-width" unless fluid_layout -- page_title "Members" +- page_title _("Members") = render "projects/project_members/index" diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml index 5dda2ec28b4..98c609d7bd4 100644 --- a/app/views/projects/settings/repository/show.html.haml +++ b/app/views/projects/settings/repository/show.html.haml @@ -1,5 +1,5 @@ -- breadcrumb_title "Repository Settings" -- page_title "Repository" +- breadcrumb_title _("Repository Settings") +- page_title _("Repository") - @content_class = "limit-container-width" unless fluid_layout = render "projects/mirrors/show" diff --git a/app/views/shared/_milestone_expired.html.haml b/app/views/shared/_milestone_expired.html.haml index 5e9007aaaac..099e3ac8462 100644 --- a/app/views/shared/_milestone_expired.html.haml +++ b/app/views/shared/_milestone_expired.html.haml @@ -1,7 +1,6 @@ - if milestone.expired? and not milestone.closed? - %span.cred (Expired) + .status-box.status-box-expired.append-bottom-5 Expired - if milestone.upcoming? - %span.clgray (Upcoming) -- if milestone.due_date || milestone.start_date - %span - = milestone_date_range(milestone) + .status-box.status-box-mr-merged.append-bottom-5 Upcoming +- if milestone.closed? + .status-box.status-box-closed.append-bottom-5 Closed diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml index b8b1f4ca42f..28407b543b9 100644 --- a/app/views/shared/_personal_access_tokens_form.html.haml +++ b/app/views/shared/_personal_access_tokens_form.html.haml @@ -9,13 +9,17 @@ = form_errors(token) - .form-group - = f.label :name, class: 'label-light' - = f.text_field :name, class: "form-control", required: true + .row + .form-group.col-md-6 + = f.label :name, class: 'label-light' + = f.text_field :name, class: "form-control", required: true - .form-group - = f.label :expires_at, class: 'label-light' - = f.text_field :expires_at, class: "datepicker form-control" + .row + .form-group.col-md-6 + = f.label :expires_at, class: 'label-light' + .input-icon-wrapper + = f.text_field :expires_at, class: "datepicker form-control", placeholder: 'YYYY-MM-DD' + = icon('calendar', { class: 'input-icon-right' }) .form-group = f.label :scopes, class: 'label-light' diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml index 65de6172d89..03e008f5fa0 100644 --- a/app/views/shared/boards/components/_board.html.haml +++ b/app/views/shared/boards/components/_board.html.haml @@ -32,7 +32,7 @@ "v-if" => "!list.preset && list.id" } %button.board-delete.has-tooltip.float-right{ type: "button", title: _("Delete list"), "aria-label" => _("Delete list"), data: { placement: "bottom" }, "@click.stop" => "deleteBoard" } = icon("trash") - .issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank"' } + .issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"' } %span.issue-count-badge-count.float-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' } {{ list.issuesSize }} - if can?(current_user, :admin_list, current_board_parent) @@ -43,8 +43,7 @@ "title" => _("New issue"), data: { placement: "top", container: "body" } } = icon("plus", class: "js-no-trigger-collapse") - - %board-list{ "v-if" => 'list.type !== "blank"', + %board-list{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"', ":list" => "list", ":issues" => "list.issues", ":loading" => "list.loading", @@ -55,3 +54,4 @@ "ref" => "board-list" } - if can?(current_user, :admin_list, current_board_parent) %board-blank-state{ "v-if" => 'list.id == "blank"' } + = render_if_exists 'shared/boards/board_promotion_state' diff --git a/app/views/shared/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml index 774dafe5f2c..1ff956649ed 100644 --- a/app/views/shared/boards/components/_sidebar.html.haml +++ b/app/views/shared/boards/components/_sidebar.html.haml @@ -8,6 +8,7 @@ {{ issue.title }} %br/ %span + = render_if_exists "shared/boards/components/sidebar/issue_project_path" = precede "#" do {{ issue.iid }} %a.gutter-toggle.float-right{ role: "button", @@ -17,9 +18,11 @@ = custom_icon("icon_close", size: 15) .js-issuable-update = render "shared/boards/components/sidebar/assignee" + = render_if_exists "shared/boards/components/sidebar/epic" = render "shared/boards/components/sidebar/milestone" = render "shared/boards/components/sidebar/due_date" = render "shared/boards/components/sidebar/labels" + = render_if_exists "shared/boards/components/sidebar/weight" = render "shared/boards/components/sidebar/notifications" %remove-btn{ ":issue" => "issue", ":issue-update" => "issue.sidebarInfoEndpoint", diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index c7c33288e9d..2e26fe63d3e 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -16,7 +16,7 @@ - if has_button .text-center - if project_select_button - = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues + = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues, with_feature_enabled: 'issues' - else = link_to 'New issue', button_path, class: 'btn btn-success', title: 'New issue', id: 'new_issue_link' - else diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml index 014220761a9..186139f3526 100644 --- a/app/views/shared/empty_states/_merge_requests.html.haml +++ b/app/views/shared/empty_states/_merge_requests.html.haml @@ -15,7 +15,7 @@ = _("Interested parties can even contribute by pushing commits if they want to.") .text-center - if project_select_button - = render 'shared/new_project_item_select', path: 'merge_requests/new', label: _('New merge request'), type: :merge_requests + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: _('New merge request'), type: :merge_requests, with_feature_enabled: 'merge_requests' - else = link_to _('New merge request'), button_path, class: 'btn btn-new', title: _('New merge request'), id: 'new_merge_request_link' - else diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml index c35d0b3751f..e49bdec386a 100644 --- a/app/views/shared/issuable/form/_title.html.haml +++ b/app/views/shared/issuable/form/_title.html.haml @@ -6,7 +6,7 @@ %div{ class: div_class } = form.text_field :title, required: true, maxlength: 255, autofocus: true, - autocomplete: 'off', class: 'form-control pad qa-issuable-form-title' + autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', placeholder: _('Title') - if issuable.respond_to?(:work_in_progress?) %p.form-text.text-muted diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 09bbd04c2bf..c559945a9c9 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -1,76 +1,59 @@ - dashboard = local_assigns[:dashboard] - custom_dom_id = dom_id(milestone.try(:milestones) ? milestone.milestones.first : milestone) +- milestone_type = milestone.group_milestone? ? 'Group Milestone' : 'Project Milestone' %li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id } .row .col-sm-6 - %strong= link_to truncate(milestone.title, length: 100), milestone_path - - if milestone.group_milestone? - %span - Group Milestone - - else - %span - Project Milestone + .append-bottom-5 + %strong= link_to truncate(milestone.title, length: 100), milestone_path + - if @group + = " - #{milestone_type}" - .col-sm-6 - .float-right.light #{milestone.percent_complete(current_user)}% complete - .row - .col-sm-6 + - if @project || milestone.is_a?(GlobalMilestone) || milestone.group_milestone? + - if milestone.due_date || milestone.start_date + .milestone-range.append-bottom-5 + = milestone_date_range(milestone) + %div + = render('shared/milestone_expired', milestone: milestone) + - if milestone.legacy_group_milestone? + .projects + - milestone.milestones.each do |milestone| + = link_to milestone_path(milestone) do + %span.label-badge.label-badge-blue.d-inline-block.append-bottom-5 + = dashboard ? milestone.project.full_name : milestone.project.name + + .col-sm-4.milestone-progress + = milestone_progress_bar(milestone) = link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path · = link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path - .col-sm-6= milestone_progress_bar(milestone) - - if milestone.is_a?(GlobalMilestone) || milestone.group_milestone? - .row - .col-sm-6 - - if milestone.legacy_group_milestone? - .expiration= render('shared/milestone_expired', milestone: milestone) - .projects - - milestone.milestones.each do |milestone| - = link_to milestone_path(milestone) do - %span.badge.badge-gray - = dashboard ? milestone.project.full_name : milestone.project.name - - if @group - .col-sm-6.milestone-actions + .float-lg-right.light #{milestone.percent_complete(current_user)}% complete + .col-sm-2 + .milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end + - if @project + - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? + - if @project.group + %button.js-promote-project-milestone-button.btn.btn-blank.btn-sm.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'), + disabled: true, + type: 'button', + data: { url: promote_project_milestone_path(milestone.project, milestone), + milestone_title: milestone.title, + group_name: @project.group.name, + target: '#promote-milestone-modal', + container: 'body', + toggle: 'modal' } } + = sprite_icon('level-up', size: 14) + + = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close btn-grouped" + - unless milestone.active? + = link_to 'Reopen Milestone', project_milestone_path(@project, milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" + - if @group - if can?(current_user, :admin_milestones, @group) - - if milestone.group_milestone? - = link_to edit_group_milestone_path(@group, milestone), class: "btn btn-sm btn-grouped" do - Edit - \ - if milestone.closed? = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" - else = link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-sm btn-grouped btn-close" - - - if @project - .row - .col-sm-6 - = render('shared/milestone_expired', milestone: milestone) - .col-sm-6.milestone-actions - - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? - = link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-sm btn-grouped" do - Edit - \ - - - if @project.group - %button.js-promote-project-milestone-button.btn.btn-sm.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'), - disabled: true, - type: 'button', - data: { url: promote_project_milestone_path(milestone.project, milestone), - milestone_title: milestone.title, - group_name: @project.group.name, - target: '#promote-milestone-modal', - container: 'body', - toggle: 'modal' } } - = _('Promote') - - = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close btn-grouped" - - %button.js-delete-milestone-button.btn.btn-sm.btn-grouped.btn-danger{ data: { toggle: 'modal', - target: '#delete-milestone-modal', - milestone_id: milestone.id, - milestone_title: markdown_field(milestone, :title), - milestone_url: project_milestone_path(milestone.project, milestone), - milestone_issue_count: milestone.issues.count, - milestone_merge_request_count: milestone.merge_requests.count }, - disabled: true } - = _('Delete') - = icon('spin spinner', class: 'js-loading-icon hidden' ) + - if dashboard + .status-box.status-box-milestone + = milestone_type diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml index e5c82962f82..dcb3fca23f2 100644 --- a/app/views/shared/tokens/_scopes_form.html.haml +++ b/app/views/shared/tokens/_scopes_form.html.haml @@ -3,7 +3,7 @@ - token = local_assigns.fetch(:token) - scopes.each do |scope| - %fieldset - = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}" - = label_tag ("#{prefix}_scopes_#{scope}"), scope, class: "label-light" - .scope-description= t scope, scope: [:doorkeeper, :scope_desc] + %fieldset.form-group.form-check + = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: 'form-check-input' + = label_tag ("#{prefix}_scopes_#{scope}"), scope, class: 'label-light form-check-label' + .text-secondary= t scope, scope: [:doorkeeper, :scope_desc] diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index d06f51b1828..b8b854853b7 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -20,6 +20,7 @@ - cronjob:ci_archive_traces_cron - cronjob:trending_projects - cronjob:issue_due_scheduler +- cronjob:prune_web_hook_logs - gcp_cluster:cluster_install_app - gcp_cluster:cluster_provision diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index b0e1d8837d9..abe86066fb4 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -3,6 +3,7 @@ # Worker for updating any project specific caches. class ProjectCacheWorker include ApplicationWorker + include ExclusiveLeaseGuard LEASE_TIMEOUT = 15.minutes.to_i @@ -13,30 +14,30 @@ class ProjectCacheWorker # statistics - An Array containing columns from ProjectStatistics to # refresh, if empty all columns will be refreshed def perform(project_id, files = [], statistics = []) - project = Project.find_by(id: project_id) + @project = Project.find_by(id: project_id) + return unless @project&.repository&.exists? - return unless project && project.repository.exists? + update_statistics(statistics) - update_statistics(project, statistics.map(&:to_sym)) + @project.repository.refresh_method_caches(files.map(&:to_sym)) - project.repository.refresh_method_caches(files.map(&:to_sym)) - - project.cleanup + @project.cleanup end - def update_statistics(project, statistics = []) - return unless try_obtain_lease_for(project.id, :update_statistics) - - Rails.logger.info("Updating statistics for project #{project.id}") + private - project.statistics.refresh!(only: statistics) + def update_statistics(statistics = []) + try_obtain_lease do + Rails.logger.info("Updating statistics for project #{@project.id}") + @project.statistics.refresh!(only: statistics.to_a.map(&:to_sym)) + end end - private + def lease_timeout + LEASE_TIMEOUT + end - def try_obtain_lease_for(project_id, section) - Gitlab::ExclusiveLease - .new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT) - .try_obtain + def lease_key + "project_cache_worker:#{@project.id}:update_statistics" end end diff --git a/app/workers/prune_web_hook_logs_worker.rb b/app/workers/prune_web_hook_logs_worker.rb new file mode 100644 index 00000000000..45c7d32f7eb --- /dev/null +++ b/app/workers/prune_web_hook_logs_worker.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# Worker that deletes a fixed number of outdated rows from the "web_hook_logs" +# table. +class PruneWebHookLogsWorker + include ApplicationWorker + include CronjobQueue + + # The maximum number of rows to remove in a single job. + DELETE_LIMIT = 50_000 + + def perform + # MySQL doesn't allow "DELETE FROM ... WHERE id IN ( ... )" if the inner + # query refers to the same table. To work around this we wrap the IN body in + # another sub query. + WebHookLog + .where( + 'id IN (SELECT id FROM (?) ids_to_remove)', + WebHookLog + .select(:id) + .where('created_at < ?', 90.days.ago.beginning_of_day) + .limit(DELETE_LIMIT) + ) + .delete_all + end +end |