Commit 5f362686 authored by Stan Hu's avatar Stan Hu

Merge branch 'master' into sh-support-bitbucket-server-import

parents f4f4e025 cd578941
...@@ -285,7 +285,8 @@ review-docs-deploy-manual: ...@@ -285,7 +285,8 @@ review-docs-deploy-manual:
- ./$SCRIPT_NAME deploy - ./$SCRIPT_NAME deploy
when: manual when: manual
only: only:
- branches - branches@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ee
<<: *except-docs-and-qa <<: *except-docs-and-qa
# Always trigger a docs build in gitlab-docs only on docs-only branches. # Always trigger a docs build in gitlab-docs only on docs-only branches.
...@@ -298,6 +299,8 @@ review-docs-deploy: ...@@ -298,6 +299,8 @@ review-docs-deploy:
- ./$SCRIPT_NAME deploy - ./$SCRIPT_NAME deploy
only: only:
- /(^docs[\/-].*|.*-docs$)/ - /(^docs[\/-].*|.*-docs$)/
- branches@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ee
<<: *except-qa <<: *except-qa
# Cleanup remote environment of gitlab-docs # Cleanup remote environment of gitlab-docs
......
...@@ -39,6 +39,7 @@ Set the title to: `[Security] Description of the original issue` ...@@ -39,6 +39,7 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details) - [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
### Summary ### Summary
#### Links #### Links
| Description | Link | | Description | Link |
......
Add a description of your merge request here. Merge requests without an adequate Add a description of your merge request here. Merge requests without an adequate
description will not be reviewed until one is added. description will not be reviewed until one is added.
## Database Checklist ## Database checklist
When adding migrations: When adding migrations:
...@@ -31,7 +31,7 @@ When removing columns, tables, indexes or other structures: ...@@ -31,7 +31,7 @@ When removing columns, tables, indexes or other structures:
- [ ] Removed these in a post-deployment migration - [ ] Removed these in a post-deployment migration
- [ ] Made sure the application no longer uses (or ignores) these structures - [ ] Made sure the application no longer uses (or ignores) these structures
## General Checklist ## General checklist
- [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary - [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary
- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/doc_styleguide.html) - [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/doc_styleguide.html)
......
...@@ -76,7 +76,7 @@ GEM ...@@ -76,7 +76,7 @@ GEM
babosa (1.0.2) babosa (1.0.2)
base32 (0.3.2) base32 (0.3.2)
batch-loader (1.2.1) batch-loader (1.2.1)
bcrypt (3.1.11) bcrypt (3.1.12)
bcrypt_pbkdf (1.0.0) bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0) benchmark-ips (2.3.0)
better_errors (2.1.1) better_errors (2.1.1)
......
...@@ -35,7 +35,7 @@ We're hiring developers, support people, and production engineers all the time, ...@@ -35,7 +35,7 @@ We're hiring developers, support people, and production engineers all the time,
There are two editions of GitLab: There are two editions of GitLab:
- GitLab Community Edition (CE) is available freely under the MIT Expat license. - GitLab Community Edition (CE) is available freely under the MIT Expat license.
- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/products/#compare-options) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/products/). - GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/pricing/#compare-options) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/).
## Website ## Website
......
...@@ -150,14 +150,15 @@ const Api = { ...@@ -150,14 +150,15 @@ const Api = {
}, },
// Return group projects list. Filtered by query // 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 url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
const defaults = {
search: query,
per_page: 20,
};
return axios return axios
.get(url, { .get(url, {
params: { params: Object.assign({}, defaults, options),
search: query,
per_page: 20,
},
}) })
.then(({ data }) => callback(data)); .then(({ data }) => callback(data));
}, },
......
/* global dateFormat */
import Vue from 'vue'; import Vue from 'vue';
import dateFormat from 'dateformat';
Vue.filter('due-date', (value) => { Vue.filter('due-date', value => {
const date = new Date(value); const date = new Date(value);
return dateFormat(date, 'mmm d, yyyy', true); return dateFormat(date, 'mmm d, yyyy', true);
}); });
...@@ -3,6 +3,7 @@ import { mapState, mapGetters, mapActions } from 'vuex'; ...@@ -3,6 +3,7 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
import eventHub from '../../notes/event_hub';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import CompareVersions from './compare_versions.vue'; import CompareVersions from './compare_versions.vue';
import ChangedFiles from './changed_files.vue'; import ChangedFiles from './changed_files.vue';
...@@ -62,7 +63,7 @@ export default { ...@@ -62,7 +63,7 @@ export default {
plainDiffPath: state => state.diffs.plainDiffPath, plainDiffPath: state => state.diffs.plainDiffPath,
emailPatchPath: state => state.diffs.emailPatchPath, emailPatchPath: state => state.diffs.emailPatchPath,
}), }),
...mapGetters(['isParallelView']), ...mapGetters(['isParallelView', 'isNotesFetched']),
targetBranch() { targetBranch() {
return { return {
branchName: this.targetBranchName, branchName: this.targetBranchName,
...@@ -94,20 +95,36 @@ export default { ...@@ -94,20 +95,36 @@ export default {
this.adjustView(); this.adjustView();
}, },
shouldShow() { 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(); this.adjustView();
}, },
}, },
mounted() { mounted() {
this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath }); 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() { created() {
this.adjustView(); this.adjustView();
}, },
methods: { methods: {
...mapActions(['setBaseConfig', 'fetchDiffFiles']), ...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) { setActive(filePath) {
this.activeFile = filePath; this.activeFile = filePath;
}, },
...@@ -128,7 +145,7 @@ export default { ...@@ -128,7 +145,7 @@ export default {
</script> </script>
<template> <template>
<div v-if="shouldShow"> <div v-show="shouldShow">
<div <div
v-if="isLoading" v-if="isLoading"
class="loading" class="loading"
......
...@@ -66,59 +66,61 @@ export default { ...@@ -66,59 +66,61 @@ export default {
@click="clearSearch" @click="clearSearch"
></i> ></i>
</div> </div>
<ul> <div class="dropdown-content">
<li <ul>
v-for="diffFile in filteredDiffFiles" <li
:key="diffFile.name" v-for="diffFile in filteredDiffFiles"
> :key="diffFile.name"
<a
:href="`#${diffFile.fileHash}`"
:title="diffFile.newPath"
class="diff-changed-file"
> >
<icon <a
:name="fileChangedIcon(diffFile)" :href="`#${diffFile.fileHash}`"
:size="16" :title="diffFile.newPath"
:class="fileChangedClass(diffFile)" class="diff-changed-file"
class="diff-file-changed-icon append-right-8" >
/> <icon
<span class="diff-changed-file-content append-right-8"> :name="fileChangedIcon(diffFile)"
<strong :size="16"
v-if="diffFile.blob && diffFile.blob.name" :class="fileChangedClass(diffFile)"
class="diff-changed-file-name" class="diff-file-changed-icon append-right-8"
> />
{{ diffFile.blob.name }} <span class="diff-changed-file-content append-right-8">
</strong> <strong
<strong v-if="diffFile.blob && diffFile.blob.name"
v-else class="diff-changed-file-name"
class="diff-changed-blank-file-name" >
> {{ diffFile.blob.name }}
{{ s__('Diffs|No file name available') }} </strong>
</strong> <strong
<span class="diff-changed-file-path prepend-top-5"> v-else
{{ truncatedDiffPath(diffFile.blob.path) }} 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> <span class="diff-changed-stats">
<span class="diff-changed-stats"> <span class="cgreen">
<span class="cgreen"> +{{ diffFile.addedLines }}
+{{ diffFile.addedLines }} </span>
<span class="cred">
-{{ diffFile.removedLines }}
</span>
</span> </span>
<span class="cred"> </a>
-{{ diffFile.removedLines }} </li>
</span>
</span>
</a>
</li>
<li <li
v-show="filteredDiffFiles.length === 0" v-show="filteredDiffFiles.length === 0"
class="dropdown-menu-empty-item" class="dropdown-menu-empty-item"
> >
<a> <a>
{{ __('No files found') }} {{ __('No files found') }}
</a> </a>
</li> </li>
</ul> </ul>
</div>
</div> </div>
</span> </span>
</template> </template>
...@@ -112,7 +112,11 @@ export default { ...@@ -112,7 +112,11 @@ export default {
}, },
methods: { methods: {
handleToggle(e, checkTarget) { 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'); this.$emit('toggleFile');
} }
}, },
...@@ -201,7 +205,7 @@ export default { ...@@ -201,7 +205,7 @@ export default {
<div <div
v-if="!diffFile.submodule && addMergeRequestButtons" v-if="!diffFile.submodule && addMergeRequestButtons"
class="file-actions d-none d-md-block" class="file-actions d-none d-sm-block"
> >
<template <template
v-if="diffFile.blob && diffFile.blob.readableText" v-if="diffFile.blob && diffFile.blob.readableText"
......
...@@ -15,10 +15,6 @@ export const setBaseConfig = ({ commit }, options) => { ...@@ -15,10 +15,6 @@ export const setBaseConfig = ({ commit }, options) => {
commit(types.SET_BASE_CONFIG, { endpoint, projectPath }); commit(types.SET_BASE_CONFIG, { endpoint, projectPath });
}; };
export const setLoadingState = ({ commit }, state) => {
commit(types.SET_LOADING, state);
};
export const fetchDiffFiles = ({ state, commit }) => { export const fetchDiffFiles = ({ state, commit }) => {
commit(types.SET_LOADING, true); commit(types.SET_LOADING, true);
...@@ -88,7 +84,6 @@ export const expandAllFiles = ({ commit }) => { ...@@ -88,7 +84,6 @@ export const expandAllFiles = ({ commit }) => {
export default { export default {
setBaseConfig, setBaseConfig,
setLoadingState,
fetchDiffFiles, fetchDiffFiles,
setInlineDiffViewType, setInlineDiffViewType,
setParallelDiffViewType, setParallelDiffViewType,
......
/* eslint-disable consistent-return, no-new */ /* eslint-disable consistent-return, no-new */
import $ from 'jquery'; import $ from 'jquery';
import Flash from './flash';
import GfmAutoComplete from './gfm_auto_complete'; import GfmAutoComplete from './gfm_auto_complete';
import { convertPermissionToBoolean } from './lib/utils/common_utils'; import { convertPermissionToBoolean } from './lib/utils/common_utils';
import GlFieldErrors from './gl_field_errors'; import GlFieldErrors from './gl_field_errors';
import Shortcuts from './shortcuts'; import Shortcuts from './shortcuts';
import SearchAutocomplete from './search_autocomplete'; import SearchAutocomplete from './search_autocomplete';
import performanceBar from './performance_bar';
function initSearch() { function initSearch() {
// Only when search form is present // Only when search form is present
...@@ -72,9 +72,7 @@ function initGFMInput() { ...@@ -72,9 +72,7 @@ function initGFMInput() {
function initPerformanceBar() { function initPerformanceBar() {
if (document.querySelector('#js-peek')) { if (document.querySelector('#js-peek')) {
import('./performance_bar') performanceBar({ container: '#js-peek' });
.then(m => new m.default({ container: '#js-peek' })) // eslint-disable-line new-cap
.catch(() => Flash('Error loading performance bar module'));
} }
} }
......
/* global dateFormat */
import $ from 'jquery'; import $ from 'jquery';
import Pikaday from 'pikaday'; import Pikaday from 'pikaday';
import dateFormat from 'dateformat';
import { __ } from '~/locale'; import { __ } from '~/locale';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility'; import { timeFor } from './lib/utils/datetime_utility';
...@@ -55,7 +54,7 @@ class DueDateSelect { ...@@ -55,7 +54,7 @@ class DueDateSelect {
format: 'yyyy-mm-dd', format: 'yyyy-mm-dd',
parse: dateString => parsePikadayDate(dateString), parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date), toString: date => pikadayToString(date),
onSelect: (dateText) => { onSelect: dateText => {
$dueDateInput.val(calendar.toString(dateText)); $dueDateInput.val(calendar.toString(dateText));
if (this.$dropdown.hasClass('js-issue-boards-due-date')) { if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
...@@ -73,7 +72,7 @@ class DueDateSelect { ...@@ -73,7 +72,7 @@ class DueDateSelect {
} }
initRemoveDueDate() { 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'); const calendar = this.$datePicker.data('pikaday');
e.preventDefault(); e.preventDefault();
...@@ -124,7 +123,8 @@ class DueDateSelect { ...@@ -124,7 +123,8 @@ class DueDateSelect {
this.$loading.fadeOut(); 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) .then(fadeOutLoader)
.catch(fadeOutLoader); .catch(fadeOutLoader);
} }
...@@ -147,17 +147,18 @@ class DueDateSelect { ...@@ -147,17 +147,18 @@ class DueDateSelect {
$('.js-remove-due-date-holder').toggleClass('hidden', selectedDateValue.length); $('.js-remove-due-date-holder').toggleClass('hidden', selectedDateValue.length);
return axios.put(this.issueUpdateURL, this.datePayload) return axios.put(this.issueUpdateURL, this.datePayload).then(() => {
.then(() => { const tooltipText = hasDueDate
const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date'); ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})`
if (isDropdown) { : __('Due date');
this.$dropdown.trigger('loaded.gl.dropdown'); if (isDropdown) {
this.$dropdown.dropdown('toggle'); this.$dropdown.trigger('loaded.gl.dropdown');
} this.$dropdown.dropdown('toggle');
this.$sidebarCollapsedValue.attr('data-original-title', tooltipText); }
this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
return this.$loading.fadeOut(); return this.$loading.fadeOut();
}); });
} }
} }
...@@ -187,15 +188,19 @@ export default class DueDateSelectors { ...@@ -187,15 +188,19 @@ export default class DueDateSelectors {
$datePicker.data('pikaday', calendar); $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(); e.preventDefault();
const calendar = $(e.target).siblings('.datepicker').data('pikaday'); const calendar = $(e.target)
.siblings('.datepicker')
.data('pikaday');
calendar.setDate(null); calendar.setDate(null);
}); });
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
initIssuableSelect() { 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) => { $('.js-due-date-select').each((i, dropdown) => {
const $dropdown = $(dropdown); const $dropdown = $(dropdown);
......
...@@ -12,7 +12,7 @@ export const defaultAutocompleteConfig = { ...@@ -12,7 +12,7 @@ export const defaultAutocompleteConfig = {
members: true, members: true,
issues: true, issues: true,
mergeRequests: true, mergeRequests: true,
epics: false, epics: true,
milestones: true, milestones: true,
labels: true, labels: true,
}; };
...@@ -493,6 +493,7 @@ GfmAutoComplete.atTypeMap = { ...@@ -493,6 +493,7 @@ GfmAutoComplete.atTypeMap = {
'@': 'members', '@': 'members',
'#': 'issues', '#': 'issues',
'!': 'mergeRequests', '!': 'mergeRequests',
'&': 'epics',
'~': 'labels', '~': 'labels',
'%': 'milestones', '%': 'milestones',
'/': 'commands', '/': 'commands',
......
...@@ -9,6 +9,13 @@ export default class GLForm { ...@@ -9,6 +9,13 @@ export default class GLForm {
this.form = form; this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input'); this.textarea = this.form.find('textarea.js-gfm-input');
this.enableGFM = Object.assign({}, GFMConfig.defaultAutocompleteConfig, enableGFM); 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 // Before we start, we should clean up any previous data for this form
this.destroy(); this.destroy();
// Setup the form // Setup the form
......
...@@ -24,8 +24,8 @@ export default { ...@@ -24,8 +24,8 @@ export default {
this.isLoading = true; this.isLoading = true;
this.$store this.message
.dispatch(this.message.action, this.message.actionPayload) .action(this.message.actionPayload)
.then(() => { .then(() => {
this.isLoading = false; this.isLoading = false;
}) })
......
import Vue from 'vue';
import VueResource from 'vue-resource';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import Api from '~/api'; import Api from '~/api';
Vue.use(VueResource);
export default { export default {
getTreeData(endpoint) {
return Vue.http.get(endpoint, { params: { format: 'json' } });
},
getFileData(endpoint) { getFileData(endpoint) {
return Vue.http.get(endpoint, { params: { format: 'json', viewer: 'none' } }); return axios.get(endpoint, {
params: { format: 'json', viewer: 'none' },
});
}, },
getRawFileData(file) { getRawFileData(file) {
if (file.tempFile) { if (file.tempFile) {
...@@ -21,7 +16,11 @@ export default { ...@@ -21,7 +16,11 @@ export default {
return Promise.resolve(file.raw); 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) { getBaseRawFileData(file, sha) {
if (file.tempFile) { if (file.tempFile) {
...@@ -32,11 +31,11 @@ export default { ...@@ -32,11 +31,11 @@ export default {
return Promise.resolve(file.baseRaw); return Promise.resolve(file.baseRaw);
} }
return Vue.http return axios
.get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), { .get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
params: { format: 'json' }, params: { format: 'json' },
}) })
.then(res => res.text()); .then(({ data }) => data);
}, },
getProjectData(namespace, project) { getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`); return Api.project(`${namespace}/${project}`);
...@@ -53,21 +52,9 @@ export default { ...@@ -53,21 +52,9 @@ export default {
getBranchData(projectId, currentBranchId) { getBranchData(projectId, currentBranchId) {
return Api.branchSingle(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) { commit(projectId, payload) {
return Api.commitMultiple(projectId, payload); return Api.commitMultiple(projectId, payload);
}, },
getTreeLastCommit(endpoint) {
return Vue.http.get(endpoint, {
params: {
format: 'json',
},
});
},
getFiles(projectUrl, branchId) { getFiles(projectUrl, branchId) {
const url = `${projectUrl}/files/${branchId}`; const url = `${projectUrl}/files/${branchId}`;
return axios.get(url, { params: { format: 'json' } }); return axios.get(url, { params: { format: 'json' } });
......
import { normalizeHeaders } from '~/lib/utils/common_utils'; import { __ } from '../../../locale';
import flash from '~/flash'; import { normalizeHeaders } from '../../../lib/utils/common_utils';
import eventHub from '../../eventhub'; import eventHub from '../../eventhub';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
...@@ -66,13 +66,10 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive ...@@ -66,13 +66,10 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
.getFileData( .getFileData(
`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`, `${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`,
) )
.then(res => { .then(({ data, headers }) => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); const normalizedHeaders = normalizeHeaders(headers);
setPageTitle(pageTitle); setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE']));
return res.json();
})
.then(data => {
commit(types.SET_FILE_DATA, { data, file }); commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, path); commit(types.TOGGLE_FILE_OPEN, path);
if (makeFileActive) dispatch('setFileActive', path); if (makeFileActive) dispatch('setFileActive', path);
...@@ -80,7 +77,13 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive ...@@ -80,7 +77,13 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
}) })
.catch(() => { .catch(() => {
commit(types.TOGGLE_LOADING, { entry: file }); 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 }) => { ...@@ -88,7 +91,7 @@ export const setFileMrChange = ({ commit }, { file, mrChange }) => {
commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { 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]; const file = state.entries[path];
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
service service
...@@ -113,7 +116,13 @@ export const getRawFileData = ({ state, commit }, { path, baseSha }) => { ...@@ -113,7 +116,13 @@ export const getRawFileData = ({ state, commit }, { path, baseSha }) => {
} }
}) })
.catch(() => { .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(); reject();
}); });
}); });
......
import flash from '~/flash'; import { __ } from '../../../locale';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
export const getMergeRequestData = ( export const getMergeRequestData = (
{ commit, state }, { commit, dispatch, state },
{ projectId, mergeRequestId, force = false } = {}, { projectId, mergeRequestId, force = false } = {},
) => ) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) { if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
service service
.getProjectMergeRequestData(projectId, mergeRequestId) .getProjectMergeRequestData(projectId, mergeRequestId)
.then(res => res.data) .then(({ data }) => {
.then(data => {
commit(types.SET_MERGE_REQUEST, { commit(types.SET_MERGE_REQUEST, {
projectPath: projectId, projectPath: projectId,
mergeRequestId, mergeRequestId,
...@@ -21,7 +20,15 @@ export const getMergeRequestData = ( ...@@ -21,7 +20,15 @@ export const getMergeRequestData = (
resolve(data); resolve(data);
}) })
.catch(() => { .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}`)); reject(new Error(`Merge Request not loaded ${projectId}`));
}); });
} else { } else {
...@@ -30,15 +37,14 @@ export const getMergeRequestData = ( ...@@ -30,15 +37,14 @@ export const getMergeRequestData = (
}); });
export const getMergeRequestChanges = ( export const getMergeRequestChanges = (
{ commit, state }, { commit, dispatch, state },
{ projectId, mergeRequestId, force = false } = {}, { projectId, mergeRequestId, force = false } = {},
) => ) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) { if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) {
service service
.getProjectMergeRequestChanges(projectId, mergeRequestId) .getProjectMergeRequestChanges(projectId, mergeRequestId)
.then(res => res.data) .then(({ data }) => {
.then(data => {
commit(types.SET_MERGE_REQUEST_CHANGES, { commit(types.SET_MERGE_REQUEST_CHANGES, {
projectPath: projectId, projectPath: projectId,
mergeRequestId, mergeRequestId,
...@@ -47,7 +53,15 @@ export const getMergeRequestChanges = ( ...@@ -47,7 +53,15 @@ export const getMergeRequestChanges = (
resolve(data); resolve(data);
}) })
.catch(() => { .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}`)); reject(new Error(`Merge Request Changes not loaded ${projectId}`));
}); });
} else { } else {
...@@ -56,7 +70,7 @@ export const getMergeRequestChanges = ( ...@@ -56,7 +70,7 @@ export const getMergeRequestChanges = (
}); });
export const getMergeRequestVersions = ( export const getMergeRequestVersions = (
{ commit, state }, { commit, dispatch, state },
{ projectId, mergeRequestId, force = false } = {}, { projectId, mergeRequestId, force = false } = {},
) => ) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
...@@ -73,7 +87,15 @@ export const getMergeRequestVersions = ( ...@@ -73,7 +87,15 @@ export const getMergeRequestVersions = (
resolve(data); resolve(data);
}) })
.catch(() => { .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}`)); reject(new Error(`Merge Request Versions not loaded ${projectId}`));
}); });
} else { } else {
......
...@@ -104,7 +104,7 @@ export const createNewBranchFromDefault = ({ state, dispatch, getters }, branch) ...@@ -104,7 +104,7 @@ export const createNewBranchFromDefault = ({ state, dispatch, getters }, branch)
.catch(() => { .catch(() => {
dispatch('setErrorMessage', { dispatch('setErrorMessage', {
text: __('An error occured creating the new branch.'), text: __('An error occured creating the new branch.'),
action: 'createNewBranchFromDefault', action: payload => dispatch('createNewBranchFromDefault', payload),
actionText: __('Please try again'), actionText: __('Please try again'),
actionPayload: branch, actionPayload: branch,
}); });
...@@ -119,7 +119,7 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => { ...@@ -119,7 +119,7 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
}, },
false, false,
), ),
action: 'createNewBranchFromDefault', action: payload => dispatch('createNewBranchFromDefault', payload),
actionText: __('Create branch'), actionText: __('Create branch'),
actionPayload: branchId, actionPayload: branchId,
}); });
......
import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import { findEntry } from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker'; import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit }, path) => { export const toggleTreeOpen = ({ commit }, path) => {
...@@ -37,32 +34,6 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => { ...@@ -37,32 +34,6 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
dispatch('showTreeEntry', row.path); 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 } = {}) => export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
if ( if (
...@@ -106,14 +77,13 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = ...@@ -106,14 +77,13 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } =
if (e.response.status === 404) { if (e.response.status === 404) {
dispatch('showBranchNotFoundError', branchId); dispatch('showBranchNotFoundError', branchId);
} else { } else {
flash( dispatch('setErrorMessage', {
__('Error loading tree data. Please try again.'), text: __('An error occured whilst loading all the files.'),
'alert', action: payload =>
document, dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)),
null, actionText: __('Please try again'),
false, actionPayload: { projectId, branchId },
true, });
);
} }
reject(e); reject(e);
}); });
......
import $ from 'jquery'; import $ from 'jquery';
import timeago from 'timeago.js'; import timeago from 'timeago.js';
import dateFormat from 'vendor/date.format'; import dateFormat from 'dateformat';
import { pluralize } from './text_utility'; import { pluralize } from './text_utility';
import { languageCode, s__ } from '../../locale'; import { languageCode, s__ } from '../../locale';
window.timeago = timeago; window.timeago = timeago;
window.dateFormat = dateFormat;
/** /**
* Returns i18n month names array. * Returns i18n month names array.
...@@ -143,7 +142,8 @@ export const localTimeAgo = ($timeagoEls, setTimeago = true) => { ...@@ -143,7 +142,8 @@ export const localTimeAgo = ($timeagoEls, setTimeago = true) => {
if (setTimeago) { if (setTimeago) {
// Recreate with custom template // Recreate with custom template
$(el).tooltip({ $(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 => { ...@@ -275,10 +275,8 @@ export const totalDaysInMonth = date => {
* *
* @param {Array} quarter * @param {Array} quarter
*/ */
export const totalDaysInQuarter = quarter => quarter.reduce( export const totalDaysInQuarter = quarter =>
(acc, month) => acc + totalDaysInMonth(month), quarter.reduce((acc, month) => acc + totalDaysInMonth(month), 0);
0,
);
/** /**
* Returns list of Dates referring to Sundays of the month * Returns list of Dates referring to Sundays of the month
...@@ -333,14 +331,8 @@ export const getTimeframeWindowFrom = (startDate, length) => { ...@@ -333,14 +331,8 @@ export const getTimeframeWindowFrom = (startDate, length) => {
// Iterate and set date for the size of length // Iterate and set date for the size of length
// and push date reference to timeframe list // and push date reference to timeframe list
const timeframe = new Array(length) const timeframe = new Array(length)
.fill() .fill()
.map( .map((val, i) => new Date(startDate.getFullYear(), startDate.getMonth() + i, 1));
(val, i) => new Date(
startDate.getFullYear(),
startDate.getMonth() + i,
1,
),
);
// Change date of last timeframe item to last date of the month // Change date of last timeframe item to last date of the month
timeframe[length - 1].setDate(totalDaysInMonth(timeframe[length - 1])); timeframe[length - 1].setDate(totalDaysInMonth(timeframe[length - 1]));
...@@ -362,14 +354,15 @@ export const getTimeframeWindowFrom = (startDate, length) => { ...@@ -362,14 +354,15 @@ export const getTimeframeWindowFrom = (startDate, length) => {
* @param {Date} date * @param {Date} date
* @param {Array} quarter * @param {Array} quarter
*/ */
export const dayInQuarter = (date, quarter) => quarter.reduce((acc, month) => { export const dayInQuarter = (date, quarter) =>
if (date.getMonth() > month.getMonth()) { quarter.reduce((acc, month) => {
return acc + totalDaysInMonth(month); if (date.getMonth() > month.getMonth()) {
} else if (date.getMonth() === month.getMonth()) { return acc + totalDaysInMonth(month);
return acc + date.getDate(); } else if (date.getMonth() === month.getMonth()) {
} return acc + date.getDate();
return acc + 0; }
}, 0); return acc + 0;
}, 0);
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.utils = { window.gl.utils = {
......
...@@ -16,6 +16,7 @@ import Diff from './diff'; ...@@ -16,6 +16,7 @@ import Diff from './diff';
import { localTimeAgo } from './lib/utils/datetime_utility'; import { localTimeAgo } from './lib/utils/datetime_utility';
import syntaxHighlight from './syntax_highlight'; import syntaxHighlight from './syntax_highlight';
import Notes from './notes'; import Notes from './notes';
import { polyfillSticky } from './lib/utils/sticky';
/* eslint-disable max-len */ /* eslint-disable max-len */
// MergeRequestTabs // MergeRequestTabs
...@@ -68,12 +69,23 @@ let { location } = window; ...@@ -68,12 +69,23 @@ let { location } = window;
export default class MergeRequestTabs { export default class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) { 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 navbar = document.querySelector('.navbar-gitlab');
const peek = document.getElementById('js-peek'); const peek = document.getElementById('js-peek');
const paddingTop = 16; const paddingTop = 16;
this.commitsTab = document.querySelector('.tab-content .commits.tab-pane'); this.commitsTab = document.querySelector('.tab-content .commits.tab-pane');
this.currentTab = null;
this.diffsLoaded = false; this.diffsLoaded = false;
this.pipelinesLoaded = false; this.pipelinesLoaded = false;
this.commitsLoaded = false; this.commitsLoaded = false;
...@@ -83,15 +95,15 @@ export default class MergeRequestTabs { ...@@ -83,15 +95,15 @@ export default class MergeRequestTabs {
this.setUrl = setUrl !== undefined ? setUrl : true; this.setUrl = setUrl !== undefined ? setUrl : true;
this.setCurrentAction = this.setCurrentAction.bind(this); this.setCurrentAction = this.setCurrentAction.bind(this);
this.tabShown = this.tabShown.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; this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
if (peek) { if (peek) {
this.stickyTop += peek.offsetHeight; this.stickyTop += peek.offsetHeight;
} }
if (mergeRequestTabs) { if (this.mergeRequestTabs) {
this.stickyTop += mergeRequestTabs.offsetHeight; this.stickyTop += this.mergeRequestTabs.offsetHeight;
} }
if (stubLocation) { if (stubLocation) {
...@@ -99,25 +111,22 @@ export default class MergeRequestTabs { ...@@ -99,25 +111,22 @@ export default class MergeRequestTabs {
} }
this.bindEvents(); 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(); this.initAffix();
} }
bindEvents() { bindEvents() {
$(document) $('.merge-request-tabs a[data-toggle="tabvue"]').on('click', this.clickTab);
.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);
} }
// Used in tests // Used in tests
unbindEvents() { unbindEvents() {
$(document) $('.merge-request-tabs a[data-toggle="tabvue"]').off('click', this.clickTab);
.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);
} }
destroyPipelinesView() { destroyPipelinesView() {
...@@ -129,58 +138,87 @@ export default class MergeRequestTabs { ...@@ -129,58 +138,87 @@ export default class MergeRequestTabs {
} }
} }
showTab(e) {
e.preventDefault();
this.activateTab($(e.target).data('action'));
}
clickTab(e) { clickTab(e) {
if (e.currentTarget && isMetaClick(e)) { if (e.currentTarget) {
const targetLink = e.currentTarget.getAttribute('href');
e.stopImmediatePropagation(); e.stopImmediatePropagation();
e.preventDefault(); 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) { tabShown(action, href) {
const $target = $(e.target); if (action !== this.currentTab && this.mergeRequestTabs) {
const action = $target.data('action'); this.currentTab = action;
if (action === 'commits') { if (this.mergeRequestTabPanesAll) {
this.loadCommits($target.attr('href')); this.mergeRequestTabPanesAll.forEach(el => {
this.expandView(); const tabPane = el;
this.resetViewContainer(); tabPane.style.display = 'none';
this.destroyPipelinesView(); });
} else if (this.isDiffAction(action)) {
if (!isInVueNoteablePage()) {
this.loadDiff($target.attr('href'));
}
if (bp.getBreakpointSize() !== 'lg') {
this.shrinkView();
} }
if (this.diffViewType() === 'parallel') {
this.expandViewContainer(); if (this.mergeRequestTabsAll) {
this.mergeRequestTabsAll.forEach(el => {
el.classList.remove('active');
});
} }
this.destroyPipelinesView();
this.commitsTab.classList.remove('active'); const tabPane = this.mergeRequestTabPanes.querySelector(`#${action}`);
} else if (action === 'pipelines') { if (tabPane) tabPane.style.display = 'block';
this.resetViewContainer(); const tab = this.mergeRequestTabs.querySelector(`.${action}-tab`);
this.mountPipelinesView(); if (tab) tab.classList.add('active');
} else {
if (bp.getBreakpointSize() !== 'xs') { if (action === 'commits') {
this.loadCommits(href);
this.expandView();
this.resetViewContainer();
this.destroyPipelinesView();
} else if (action === 'new') {
this.expandView(); 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(); this.eventHub.$emit('MergeRequestTabChange', this.getCurrentAction());
}
if (this.setUrl) {
this.setCurrentAction(action);
} }
this.eventHub.$emit('MergeRequestTabChange', this.getCurrentAction());
} }
scrollToElement(container) { scrollToElement(container) {
...@@ -193,12 +231,6 @@ export default class MergeRequestTabs { ...@@ -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 // 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 // If the action is "notes", the URL is reset to the standard
...@@ -426,7 +458,6 @@ export default class MergeRequestTabs { ...@@ -426,7 +458,6 @@ export default class MergeRequestTabs {
initAffix() { initAffix() {
const $tabs = $('.js-tabs-affix'); const $tabs = $('.js-tabs-affix');
const $fixedNav = $('.navbar-gitlab');
// Screen space on small screens is usually very sparse // Screen space on small screens is usually very sparse
// So we dont affix the tabs on these // So we dont affix the tabs on these
...@@ -439,21 +470,6 @@ export default class MergeRequestTabs { ...@@ -439,21 +470,6 @@ export default class MergeRequestTabs {
*/ */
if ($tabs.css('position') !== 'static') return; if ($tabs.css('position') !== 'static') return;
const $diffTabs = $('#diff-notes-app'); polyfillSticky($tabs);
$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');
}
} }
} }
...@@ -45,17 +45,17 @@ export default function initMrNotes() { ...@@ -45,17 +45,17 @@ export default function initMrNotes() {
this.updateDiscussionTabCounter(); this.updateDiscussionTabCounter();
}, },
}, },
created() {
this.setActiveTab(window.mrTabs.getCurrentAction());
},
mounted() { mounted() {
this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge'); 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); $(document).on('visibilitychange', this.updateDiscussionTabCounter);
window.mrTabs.eventHub.$on('MergeRequestTabChange', this.setActiveTab);
}, },
beforeDestroy() { beforeDestroy() {
$(document).off('visibilitychange', this.updateDiscussionTabCounter); $(document).off('visibilitychange', this.updateDiscussionTabCounter);
window.mrTabs.eventHub.$off('MergeRequestTabChange', this.setActiveTab);
}, },
methods: { methods: {
...mapActions(['setActiveTab']), ...mapActions(['setActiveTab']),
......
...@@ -3,6 +3,7 @@ import { mapGetters, mapActions } from 'vuex'; ...@@ -3,6 +3,7 @@ import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility'; import { getLocationHash } from '../../lib/utils/url_utility';
import Flash from '../../flash'; import Flash from '../../flash';
import * as constants from '../constants'; import * as constants from '../constants';
import eventHub from '../event_hub';
import noteableNote from './noteable_note.vue'; import noteableNote from './noteable_note.vue';
import noteableDiscussion from './noteable_discussion.vue'; import noteableDiscussion from './noteable_discussion.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue'; import systemNote from '../../vue_shared/components/notes/system_note.vue';
...@@ -49,7 +50,7 @@ export default { ...@@ -49,7 +50,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters(['discussions', 'getNotesDataByProp', 'discussionCount']), ...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount']),
noteableType() { noteableType() {
return this.noteableData.noteableType; return this.noteableData.noteableType;
}, },
...@@ -61,19 +62,30 @@ export default { ...@@ -61,19 +62,30 @@ export default {
isSkeletonNote: true, isSkeletonNote: true,
}); });
} }
return this.discussions; return this.discussions;
}, },
}, },
watch: {
shouldShow() {
if (!this.isNotesFetched) {
this.fetchNotes();
}
},
},
created() { created() {
this.setNotesData(this.notesData); this.setNotesData(this.notesData);
this.setNoteableData(this.noteableData); this.setNoteableData(this.noteableData);
this.setUserData(this.userData); this.setUserData(this.userData);
this.setTargetNoteHash(getLocationHash()); this.setTargetNoteHash(getLocationHash());
eventHub.$once('fetchNotesData', this.fetchNotes);
}, },
mounted() { mounted() {
this.fetchNotes(); if (this.shouldShow) {
const { parentElement } = this.$el; this.fetchNotes();
}
const { parentElement } = this.$el;
if (parentElement && parentElement.classList.contains('js-vue-notes-event')) { if (parentElement && parentElement.classList.contains('js-vue-notes-event')) {
parentElement.addEventListener('toggleAward', event => { parentElement.addEventListener('toggleAward', event => {
const { awardName, noteId } = event.detail; const { awardName, noteId } = event.detail;
...@@ -93,6 +105,7 @@ export default { ...@@ -93,6 +105,7 @@ export default {
setLastFetchedAt: 'setLastFetchedAt', setLastFetchedAt: 'setLastFetchedAt',
setTargetNoteHash: 'setTargetNoteHash', setTargetNoteHash: 'setTargetNoteHash',
toggleDiscussion: 'toggleDiscussion', toggleDiscussion: 'toggleDiscussion',
setNotesFetchedState: 'setNotesFetchedState',
}), }),
getComponentName(discussion) { getComponentName(discussion) {
if (discussion.isSkeletonNote) { if (discussion.isSkeletonNote) {
...@@ -119,11 +132,13 @@ export default { ...@@ -119,11 +132,13 @@ export default {
}) })
.then(() => { .then(() => {
this.isLoading = false; this.isLoading = false;
this.setNotesFetchedState(true);
}) })
.then(() => this.$nextTick()) .then(() => this.$nextTick())
.then(() => this.checkLocationHash()) .then(() => this.checkLocationHash())
.catch(() => { .catch(() => {
this.isLoading = false; this.isLoading = false;
this.setNotesFetchedState(true);
Flash('Something went wrong while fetching comments. Please try again.'); Flash('Something went wrong while fetching comments. Please try again.');
}); });
}, },
...@@ -160,12 +175,13 @@ export default { ...@@ -160,12 +175,13 @@ export default {
<template> <template>
<div <div
v-if="shouldShow" v-show="shouldShow"
id="notes"> id="notes"
>
<ul <ul
id="notes-list" id="notes-list"
class="notes main-notes-list timeline"> class="notes main-notes-list timeline"
>
<component <component
v-for="discussion in allDiscussions" v-for="discussion in allDiscussions"
:is="getComponentName(discussion)" :is="getComponentName(discussion)"
......
...@@ -28,6 +28,9 @@ export const setInitialNotes = ({ commit }, discussions) => ...@@ -28,6 +28,9 @@ export const setInitialNotes = ({ commit }, discussions) =>
export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data); 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 toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
export const fetchDiscussions = ({ commit }, path) => export const fetchDiscussions = ({ commit }, path) =>
......
...@@ -8,6 +8,8 @@ export const targetNoteHash = state => state.targetNoteHash; ...@@ -8,6 +8,8 @@ export const targetNoteHash = state => state.targetNoteHash;
export const getNotesData = state => state.notesData; export const getNotesData = state => state.notesData;
export const isNotesFetched = state => state.isNotesFetched;
export const getNotesDataByProp = state => prop => state.notesData[prop]; export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getNoteableData = state => state.noteableData; export const getNoteableData = state => state.noteableData;
......
...@@ -10,6 +10,7 @@ export default { ...@@ -10,6 +10,7 @@ export default {
// View layer // View layer
isToggleStateButtonLoading: false, isToggleStateButtonLoading: false,
isNotesFetched: false,
// holds endpoints and permissions provided through haml // holds endpoints and permissions provided through haml
notesData: { notesData: {
......
...@@ -15,6 +15,7 @@ export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION'; ...@@ -15,6 +15,7 @@ export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
export const UPDATE_NOTE = 'UPDATE_NOTE'; export const UPDATE_NOTE = 'UPDATE_NOTE';
export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION'; export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES'; export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
// Issue // Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE'; export const CLOSE_ISSUE = 'CLOSE_ISSUE';
......
...@@ -205,6 +205,10 @@ export default { ...@@ -205,6 +205,10 @@ export default {
Object.assign(state, { isToggleStateButtonLoading: value }); 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 }) { [types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId); const discussion = utils.findNoteObjectById(state.discussions, discussionId);
const index = state.discussions.indexOf(discussion); const index = state.discussions.indexOf(discussion);
......
...@@ -105,7 +105,7 @@ export default class Search { ...@@ -105,7 +105,7 @@ export default class Search {
getProjectsData(term) { getProjectsData(term) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (this.groupId) { if (this.groupId) {
Api.groupProjects(this.groupId, term, resolve); Api.groupProjects(this.groupId, term, {}, resolve);
} else { } else {
Api.projects(term, { Api.projects(term, {
order_by: 'id', order_by: 'id',
......
...@@ -8,6 +8,7 @@ export default () => { ...@@ -8,6 +8,7 @@ export default () => {
members: false, members: false,
issues: false, issues: false,
mergeRequests: false, mergeRequests: false,
epics: false,
milestones: false, milestones: false,
labels: false, labels: false,
}); });
......
...@@ -2,6 +2,7 @@ import $ from 'jquery'; ...@@ -2,6 +2,7 @@ import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import { scaleLinear, scaleThreshold } from 'd3-scale'; import { scaleLinear, scaleThreshold } from 'd3-scale';
import { select } from 'd3-selection'; import { select } from 'd3-selection';
import dateFormat from 'dateformat';
import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility'; import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import flash from '~/flash'; import flash from '~/flash';
...@@ -26,7 +27,7 @@ function getSystemDate(systemUtcOffsetSeconds) { ...@@ -26,7 +27,7 @@ function getSystemDate(systemUtcOffsetSeconds) {
function formatTooltipText({ date, count }) { function formatTooltipText({ date, count }) {
const dateObject = new Date(date); const dateObject = new Date(date);
const dateDayName = getDayName(dateObject); const dateDayName = getDayName(dateObject);
const dateText = dateObject.format('mmm d, yyyy'); const dateText = dateFormat(dateObject, 'mmm d, yyyy');
let contribText = 'No contributions'; let contribText = 'No contributions';
if (count > 0) { if (count > 0) {
...@@ -84,7 +85,7 @@ export default class ActivityCalendar { ...@@ -84,7 +85,7 @@ export default class ActivityCalendar {
date.setDate(date.getDate() + i); date.setDate(date.getDate() + i);
const day = date.getDay(); 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 // Create a new group array if this is the first day of the week
// or if is first object // or if is first object
......
<script> <script>
export default { export default {
name: 'PipelinesSvgState', name: 'PipelinesSvgState',
props: { props: {
svgPath: { svgPath: {
type: String, type: String,
required: true, required: true,
}, },
message: { message: {
type: String, type: String,
required: true, required: true,
},
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
export default { export default {
name: 'PipelinesEmptyState', name: 'PipelinesEmptyState',
props: { props: {
helpPagePath: { helpPagePath: {
type: String, type: String,
required: true, required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
canSetCi: {
type: Boolean,
required: true,
},
}, },
}; emptyStateSvgPath: {
type: String,
required: true,
},
canSetCi: {
type: Boolean,
required: true,
},
},
};
</script> </script>
<template> <template>
<div class="row empty-state js-empty-state"> <div class="row empty-state js-empty-state">
......
...@@ -41,7 +41,6 @@ export default { ...@@ -41,7 +41,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
}, },
data() { data() {
return { return {
...@@ -67,7 +66,8 @@ export default { ...@@ -67,7 +66,8 @@ export default {
this.isDisabled = true; this.isDisabled = true;
axios.post(`${this.link}.json`) axios
.post(`${this.link}.json`)
.then(() => { .then(() => {
this.isDisabled = false; this.isDisabled = false;
this.$emit('pipelineActionRequestComplete'); this.$emit('pipelineActionRequestComplete');
......
...@@ -109,6 +109,7 @@ export default { ...@@ -109,6 +109,7 @@ export default {
:key="i" :key="i"
> >
<job-component <job-component
:dropdown-length="job.size"
:job="item" :job="item"
css-class-job-name="mini-pipeline-graph-dropdown-item" css-class-job-name="mini-pipeline-graph-dropdown-item"
@pipelineActionRequestComplete="pipelineActionRequestComplete" @pipelineActionRequestComplete="pipelineActionRequestComplete"
......
...@@ -46,6 +46,11 @@ export default { ...@@ -46,6 +46,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
dropdownLength: {
type: Number,
required: false,
default: Infinity,
},
}, },
computed: { computed: {
status() { status() {
...@@ -70,6 +75,10 @@ export default { ...@@ -70,6 +75,10 @@ export default {
return textBuilder.join(' '); return textBuilder.join(' ');
}, },
tooltipBoundary() {
return this.dropdownLength < 5 ? 'viewport' : null;
},
/** /**
* Verifies if the provided job has an action path * Verifies if the provided job has an action path
* *
...@@ -94,9 +103,9 @@ export default { ...@@ -94,9 +103,9 @@ export default {
:href="status.details_path" :href="status.details_path"
:title="tooltipText" :title="tooltipText"
:class="cssClassJobName" :class="cssClassJobName"
:data-boundary="tooltipBoundary"
data-container="body" data-container="body"
data-html="true" data-html="true"
data-boundary="viewport"
class="js-pipeline-graph-job-link" class="js-pipeline-graph-job-link"
> >
......
<script> <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. * Component that renders both the CI icon status and the job name.
* Used in * Used in
* - Badge component * - Badge component
* - Dropdown badge components * - Dropdown badge components
*/ */
export default { export default {
components: { components: {
ciIcon, ciIcon,
},
props: {
name: {
type: String,
required: true,
}, },
props: {
name: {
type: String,
required: true,
},
status: { status: {
type: Object, type: Object,
required: true, required: true,
},
}, },
}; },
};
</script> </script>
<template> <template>
<span class="ci-job-name-component"> <span class="ci-job-name-component">
......
<script> <script>
import ciHeader from '../../vue_shared/components/header_ci_component.vue'; import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default { export default {
name: 'PipelineHeaderSection', name: 'PipelineHeaderSection',
components: { components: {
ciHeader, ciHeader,
loadingIcon, loadingIcon,
},
props: {
pipeline: {
type: Object,
required: true,
}, },
props: { isLoading: {
pipeline: { type: Boolean,
type: Object, required: true,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
},
data() {
return {
actions: this.getActions(),
};
}, },
},
data() {
return {
actions: this.getActions(),
};
},
computed: { computed: {
status() { status() {
return this.pipeline.details && this.pipeline.details.status; return this.pipeline.details && this.pipeline.details.status;
}, },
shouldRenderContent() { shouldRenderContent() {
return !this.isLoading && Object.keys(this.pipeline).length; return !this.isLoading && Object.keys(this.pipeline).length;
},
}, },
},
watch: { watch: {
pipeline() { pipeline() {
this.actions = this.getActions(); this.actions = this.getActions();
},
}, },
},
methods: { methods: {
postAction(action) { postAction(action) {
const index = this.actions.indexOf(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() { getActions() {
const actions = []; const actions = [];
if (this.pipeline.retry_path) { if (this.pipeline.retry_path) {
actions.push({ actions.push({
label: 'Retry', label: 'Retry',
path: this.pipeline.retry_path, path: this.pipeline.retry_path,
cssClass: 'js-retry-button btn btn-inverted-secondary', cssClass: 'js-retry-button btn btn-inverted-secondary',
type: 'button', type: 'button',
isLoading: false, isLoading: false,
}); });
} }
if (this.pipeline.cancel_path) { if (this.pipeline.cancel_path) {
actions.push({ actions.push({
label: 'Cancel running', label: 'Cancel running',
path: this.pipeline.cancel_path, path: this.pipeline.cancel_path,
cssClass: 'js-btn-cancel-pipeline btn btn-danger', cssClass: 'js-btn-cancel-pipeline btn btn-danger',
type: 'button', type: 'button',
isLoading: false, isLoading: false,
}); });
} }
return actions; return actions;
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="pipeline-header-container"> <div class="pipeline-header-container">
......
<script> <script>
import LoadingButton from '../../vue_shared/components/loading_button.vue'; import LoadingButton from '../../vue_shared/components/loading_button.vue';
export default { export default {
name: 'PipelineNavControls', name: 'PipelineNavControls',
components: { components: {
LoadingButton, LoadingButton,
},
props: {
newPipelinePath: {
type: String,
required: false,
default: null,
}, },
props: {
newPipelinePath: {
type: String,
required: false,
default: null,
},
resetCachePath: { resetCachePath: {
type: String, type: String,
required: false, required: false,
default: null, default: null,
}, },
ciLintPath: { ciLintPath: {
type: String, type: String,
required: false, required: false,
default: null, default: null,
}, },
isResetCacheButtonLoading: { isResetCacheButtonLoading: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
},
}, },
methods: { },
onClickResetCache() { methods: {
this.$emit('resetRunnersCache', this.resetCachePath); onClickResetCache() {
}, this.$emit('resetRunnersCache', this.resetCachePath);
}, },
}; },
};
</script> </script>
<template> <template>
<div class="nav-controls"> <div class="nav-controls">
......
<script> <script>
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import popover from '../../vue_shared/directives/popover'; import popover from '../../vue_shared/directives/popover';
export default { export default {
components: { components: {
userAvatarLink, userAvatarLink,
},
directives: {
tooltip,
popover,
},
props: {
pipeline: {
type: Object,
required: true,
}, },
directives: { autoDevopsHelpPath: {
tooltip, type: String,
popover, required: true,
}, },
props: { },
pipeline: { computed: {
type: Object, user() {
required: true, return this.pipeline.user;
},
autoDevopsHelpPath: {
type: String,
required: true,
},
}, },
computed: { popoverOptions() {
user() { return {
return this.pipeline.user; html: true,
}, trigger: 'focus',
popoverOptions() { placement: 'top',
return { title: `<div class="autodevops-title">
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> This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b>
</div>`, </div>`,
content: `<a content: `<a
class="autodevops-link" class="autodevops-link"
href="${this.autoDevopsHelpPath}" href="${this.autoDevopsHelpPath}"
target="_blank" target="_blank"
rel="noopener noreferrer nofollow"> rel="noopener noreferrer nofollow">
Learn more about Auto DevOps Learn more about Auto DevOps
</a>`, </a>`,
}; };
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="table-section section-15 d-none d-sm-none d-md-block pipeline-tags"> <div class="table-section section-15 d-none d-sm-none d-md-block pipeline-tags">
......
<script> <script>
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import icon from '../../vue_shared/components/icon.vue'; import icon from '../../vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
directives: { directives: {
tooltip, tooltip,
},
components: {
loadingIcon,
icon,
},
props: {
actions: {
type: Array,
required: true,
}, },
components: { },
loadingIcon, data() {
icon, return {
}, isLoading: false,
props: { };
actions: { },
type: Array, methods: {
required: true, 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) { isActionDisabled(action) {
if (action.playable === undefined) { if (action.playable === undefined) {
return false; return false;
} }
return !action.playable; return !action.playable;
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="btn-group"> <div class="btn-group">
......
<script> <script>
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import icon from '../../vue_shared/components/icon.vue'; import icon from '../../vue_shared/components/icon.vue';
export default { export default {
directives: { directives: {
tooltip, tooltip,
},
components: {
icon,
},
props: {
artifacts: {
type: Array,
required: true,
}, },
components: { },
icon, };
},
props: {
artifacts: {
type: Array,
required: true,
},
},
};
</script> </script>
<template> <template>
<div <div
......
<script> <script>
import Modal from '~/vue_shared/components/gl_modal.vue'; import Modal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import PipelinesTableRowComponent from './pipelines_table_row.vue'; import PipelinesTableRowComponent from './pipelines_table_row.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
/** /**
* Pipelines Table Component. * Pipelines Table Component.
* *
* Given an array of objects, renders a table. * Given an array of objects, renders a table.
*/ */
export default { export default {
components: { components: {
PipelinesTableRowComponent, PipelinesTableRowComponent,
Modal, Modal,
},
props: {
pipelines: {
type: Array,
required: true,
}, },
props: { updateGraphDropdown: {
pipelines: { type: Boolean,
type: Array, required: false,
required: true, default: false,
},
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
autoDevopsHelpPath: {
type: String,
required: true,
},
viewType: {
type: String,
required: true,
},
}, },
data() { autoDevopsHelpPath: {
return { type: String,
pipelineId: '', required: true,
endpoint: '',
cancelingPipeline: null,
};
}, },
computed: { viewType: {
modalTitle() { type: String,
return sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), { required: true,
},
},
data() {
return {
pipelineId: '',
endpoint: '',
cancelingPipeline: null,
};
},
computed: {
modalTitle() {
return sprintf(
s__('Pipeline|Stop pipeline #%{pipelineId}?'),
{
pipelineId: `${this.pipelineId}`, pipelineId: `${this.pipelineId}`,
}, false); },
}, false,
modalText() { );
return sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), {
pipelineId: `<strong>#${this.pipelineId}</strong>`,
}, false);
},
}, },
created() { modalText() {
eventHub.$on('openConfirmationModal', this.setModalData); 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: { onSubmit() {
setModalData(data) { eventHub.$emit('postAction', this.endpoint);
this.pipelineId = data.pipelineId; this.cancelingPipeline = this.pipelineId;
this.endpoint = data.endpoint;
},
onSubmit() {
eventHub.$emit('postAction', this.endpoint);
this.cancelingPipeline = this.pipelineId;
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="ci-table"> <div class="ci-table">
......
...@@ -186,32 +186,27 @@ export default { ...@@ -186,32 +186,27 @@ export default {
</i> </i>
</button> </button>
<ul <div
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container" class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
aria-labelledby="stageDropdown" aria-labelledby="stageDropdown"
> >
<loading-icon v-if="isLoading"/>
<li <ul
v-else
class="js-builds-dropdown-list scrollable-menu" class="js-builds-dropdown-list scrollable-menu"
> >
<li
<loading-icon v-if="isLoading"/> v-for="job in dropdownContent"
:key="job.id"
<ul
v-else
> >
<li <job-component
v-for="job in dropdownContent" :dropdown-length="dropdownContent.length"
:key="job.id" :job="job"
> css-class-job-name="mini-pipeline-graph-dropdown-item"
<job-component @pipelineActionRequestComplete="pipelineActionRequestComplete"
:job="job" />
css-class-job-name="mini-pipeline-graph-dropdown-item" </li>
@pipelineActionRequestComplete="pipelineActionRequestComplete" </ul>
/> </div>
</li>
</ul>
</li>
</ul>
</div> </div>
</template> </template>
<script> <script>
import iconTimerSvg from 'icons/_icon_timer.svg'; import iconTimerSvg from 'icons/_icon_timer.svg';
import '../../lib/utils/datetime_utility'; import '../../lib/utils/datetime_utility';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago'; import timeagoMixin from '../../vue_shared/mixins/timeago';
export default { export default {
directives: { directives: {
tooltip, tooltip,
},
mixins: [timeagoMixin],
props: {
finishedTime: {
type: String,
required: true,
}, },
mixins: [ duration: {
timeagoMixin, type: Number,
], required: true,
props: {
finishedTime: {
type: String,
required: true,
},
duration: {
type: Number,
required: true,
},
}, },
data() { },
return { data() {
iconTimerSvg, return {
}; iconTimerSvg,
};
},
computed: {
hasDuration() {
return this.duration > 0;
}, },
computed: { hasFinishedTime() {
hasDuration() { return this.finishedTime !== '';
return this.duration > 0; },
}, durationFormated() {
hasFinishedTime() { const date = new Date(this.duration * 1000);
return this.finishedTime !== '';
},
durationFormated() {
const date = new Date(this.duration * 1000);
let hh = date.getUTCHours(); let hh = date.getUTCHours();
let mm = date.getUTCMinutes(); let mm = date.getUTCMinutes();
let ss = date.getSeconds(); let ss = date.getSeconds();
// left pad // left pad
if (hh < 10) { if (hh < 10) {
hh = `0${hh}`; hh = `0${hh}`;
} }
if (mm < 10) { if (mm < 10) {
mm = `0${mm}`; mm = `0${mm}`;
} }
if (ss < 10) { if (ss < 10) {
ss = `0${ss}`; ss = `0${ss}`;
} }
return `${hh}:${mm}:${ss}`; return `${hh}:${mm}:${ss}`;
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="table-section section-15 pipelines-time-ago"> <div class="table-section section-15 pipelines-time-ago">
......
...@@ -75,8 +75,7 @@ export default { ...@@ -75,8 +75,7 @@ export default {
// Stop polling // Stop polling
this.poll.stop(); this.poll.stop();
// Update the table // Update the table
return this.getPipelines() return this.getPipelines().then(() => this.poll.restart());
.then(() => this.poll.restart());
}, },
fetchPipelines() { fetchPipelines() {
if (!this.isMakingRequest) { if (!this.isMakingRequest) {
...@@ -86,9 +85,10 @@ export default { ...@@ -86,9 +85,10 @@ export default {
} }
}, },
getPipelines() { getPipelines() {
return this.service.getPipelines(this.requestData) return this.service
.getPipelines(this.requestData)
.then(response => this.successCallback(response)) .then(response => this.successCallback(response))
.catch((error) => this.errorCallback(error)); .catch(error => this.errorCallback(error));
}, },
setCommonData(pipelines) { setCommonData(pipelines) {
this.store.storePipelines(pipelines); this.store.storePipelines(pipelines);
...@@ -118,7 +118,8 @@ export default { ...@@ -118,7 +118,8 @@ export default {
} }
}, },
postAction(endpoint) { postAction(endpoint) {
this.service.postAction(endpoint) this.service
.postAction(endpoint)
.then(() => this.fetchPipelines()) .then(() => this.fetchPipelines())
.catch(() => Flash(__('An error occurred while making the request.'))); .catch(() => Flash(__('An error occurred while making the request.')));
}, },
......
...@@ -31,7 +31,8 @@ export default () => { ...@@ -31,7 +31,8 @@ export default () => {
requestRefreshPipelineGraph() { requestRefreshPipelineGraph() {
// When an action is clicked // When an action is clicked
// (wether in the dropdown or in the main nodes, we refresh the big graph) // (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.'))); .catch(() => Flash(__('An error occurred while making the request.')));
}, },
}, },
......
...@@ -52,7 +52,8 @@ export default class pipelinesMediator { ...@@ -52,7 +52,8 @@ export default class pipelinesMediator {
refreshPipeline() { refreshPipeline() {
this.poll.stop(); this.poll.stop();
return this.service.getPipeline() return this.service
.getPipeline()
.then(response => this.successCallback(response)) .then(response => this.successCallback(response))
.catch(() => this.errorCallback()) .catch(() => this.errorCallback())
.finally(() => this.poll.restart()); .finally(() => this.poll.restart());
......
...@@ -47,7 +47,10 @@ export default function projectSelect() { ...@@ -47,7 +47,10 @@ export default function projectSelect() {
projectsCallback = finalCallback; projectsCallback = finalCallback;
} }
if (_this.groupId) { 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 { } else {
return Api.projects(query.term, { return Api.projects(query.term, {
order_by: _this.orderBy, order_by: _this.orderBy,
......
...@@ -8,10 +8,11 @@ export default (initGFM = true) => { ...@@ -8,10 +8,11 @@ export default (initGFM = true) => {
new DueDateSelectors(); // eslint-disable-line no-new new DueDateSelectors(); // eslint-disable-line no-new
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new GLForm($('.milestone-form'), { new GLForm($('.milestone-form'), {
emojis: initGFM, emojis: true,
members: initGFM, members: initGFM,
issues: initGFM, issues: initGFM,
mergeRequests: initGFM, mergeRequests: initGFM,
epics: initGFM,
milestones: initGFM, milestones: initGFM,
labels: initGFM, labels: initGFM,
}); });
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago'; import timeagoMixin from '../../vue_shared/mixins/timeago';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import LoadingButton from '../../vue_shared/components/loading_button.vue'; import LoadingButton from '../../vue_shared/components/loading_button.vue';
...@@ -14,6 +15,7 @@ export default { ...@@ -14,6 +15,7 @@ export default {
LoadingButton, LoadingButton,
MemoryUsage, MemoryUsage,
StatusIcon, StatusIcon,
Icon,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -110,11 +112,10 @@ export default { ...@@ -110,11 +112,10 @@ export default {
class="deploy-link js-deploy-url" class="deploy-link js-deploy-url"
> >
{{ deployment.external_url_formatted }} {{ deployment.external_url_formatted }}
<i <icon
class="fa fa-external-link" :size="16"
aria-hidden="true" name="external-link"
> />
</i>
</a> </a>
</template> </template>
<span <span
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
}; };
</script> </script>
<template> <template>
<div class="space-children flex-container-block append-right-10"> <div class="space-children d-flex append-right-10">
<div <div
v-if="isLoading" v-if="isLoading"
class="mr-widget-icon" class="mr-widget-icon"
......
...@@ -82,7 +82,7 @@ ...@@ -82,7 +82,7 @@
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon status="success" /> <status-icon status="success" />
<div class="media-body"> <div class="media-body">
<h4 class="flex-container-block"> <h4 class="d-flex align-items-start">
<span class="append-right-10"> <span class="append-right-10">
{{ s__("mrWidget|Set by") }} {{ s__("mrWidget|Set by") }}
<mr-widget-author :author="mr.setToMWPSBy" /> <mr-widget-author :author="mr.setToMWPSBy" />
...@@ -119,7 +119,7 @@ ...@@ -119,7 +119,7 @@
</p> </p>
<p <p
v-else v-else
class="flex-container-block" class="d-flex align-items-start"
> >
<span class="append-right-10"> <span class="append-right-10">
{{ s__("mrWidget|The source branch will not be removed") }} {{ s__("mrWidget|The source branch will not be removed") }}
......
...@@ -67,6 +67,7 @@ ...@@ -67,6 +67,7 @@
members: this.enableAutocomplete, members: this.enableAutocomplete,
issues: this.enableAutocomplete, issues: this.enableAutocomplete,
mergeRequests: this.enableAutocomplete, mergeRequests: this.enableAutocomplete,
epics: this.enableAutocomplete,
milestones: this.enableAutocomplete, milestones: this.enableAutocomplete,
labels: this.enableAutocomplete, labels: this.enableAutocomplete,
}); });
......
...@@ -128,6 +128,11 @@ table { ...@@ -128,6 +128,11 @@ table {
border-spacing: 0; border-spacing: 0;
} }
.tooltip {
// Fix bootstrap4 bug whereby tooltips flicker when they are hovered over their borders
pointer-events: none;
}
.popover { .popover {
font-size: 14px; font-size: 14px;
} }
......
...@@ -350,11 +350,6 @@ ...@@ -350,11 +350,6 @@
} }
} }
.flex-container-block {
display: -webkit-flex;
display: flex;
}
.flex-right { .flex-right {
margin-left: auto; margin-left: auto;
} }
...@@ -262,12 +262,7 @@ li.note { ...@@ -262,12 +262,7 @@ li.note {
} }
.milestone { .milestone {
&.milestone-closed {
background: $gray-light;
}
.progress { .progress {
margin-bottom: 0;
margin-top: 4px; margin-top: 4px;
box-shadow: none; box-shadow: none;
background-color: $border-gray-light; background-color: $border-gray-light;
......
...@@ -68,8 +68,7 @@ ...@@ -68,8 +68,7 @@
} }
.nav-sidebar { .nav-sidebar {
transition: width $sidebar-transition-duration, transition: width $sidebar-transition-duration, left $sidebar-transition-duration;
left $sidebar-transition-duration;
position: fixed; position: fixed;
z-index: 400; z-index: 400;
width: $contextual-sidebar-width; width: $contextual-sidebar-width;
...@@ -77,12 +76,12 @@ ...@@ -77,12 +76,12 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
background-color: $gray-light; 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); transform: translate3d(0, 0, 0);
&:not(.sidebar-collapsed-desktop) { &:not(.sidebar-collapsed-desktop) {
@media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) { @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; 2px 1px 3px $dropdown-shadow-color;
} }
} }
...@@ -214,7 +213,7 @@ ...@@ -214,7 +213,7 @@
> li { > li {
> a { > a {
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
margin-right: 2px; margin-right: 1px;
} }
&:hover { &:hover {
...@@ -224,7 +223,7 @@ ...@@ -224,7 +223,7 @@
&.is-showing-fly-out { &.is-showing-fly-out {
> a { > a {
margin-right: 2px; margin-right: 1px;
} }
.sidebar-sub-level-items { .sidebar-sub-level-items {
...@@ -317,14 +316,14 @@ ...@@ -317,14 +316,14 @@
.toggle-sidebar-button, .toggle-sidebar-button,
.close-nav-button { .close-nav-button {
width: $contextual-sidebar-width - 2px; width: $contextual-sidebar-width - 1px;
transition: width $sidebar-transition-duration; transition: width $sidebar-transition-duration;
position: fixed; position: fixed;
bottom: 0; bottom: 0;
padding: $gl-padding; padding: $gl-padding;
background-color: $gray-light; background-color: $gray-light;
border: 0; border: 0;
border-top: 2px solid $border-color; border-top: 1px solid $border-color;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -379,7 +378,7 @@ ...@@ -379,7 +378,7 @@
.toggle-sidebar-button { .toggle-sidebar-button {
padding: 16px; padding: 16px;
width: $contextual-sidebar-collapsed-width - 2px; width: $contextual-sidebar-collapsed-width - 1px;
.collapse-text, .collapse-text,
.icon-angle-double-left { .icon-angle-double-left {
......
...@@ -322,14 +322,17 @@ span.idiff { ...@@ -322,14 +322,17 @@ span.idiff {
} }
.file-title-flex-parent { .file-title-flex-parent {
display: flex; &,
align-items: center; .file-holder & {
justify-content: space-between; display: flex;
background-color: $gray-light; align-items: center;
border-bottom: 1px solid $border-color; justify-content: space-between;
padding: 5px $gl-padding; background-color: $gray-light;
margin: 0; border-bottom: 1px solid $border-color;
border-radius: $border-radius-default $border-radius-default 0 0; padding: 5px $gl-padding;
margin: 0;
border-radius: $border-radius-default $border-radius-default 0 0;
}
.file-header-content { .file-header-content {
white-space: nowrap; white-space: nowrap;
...@@ -337,6 +340,17 @@ span.idiff { ...@@ -337,6 +340,17 @@ span.idiff {
text-overflow: ellipsis; text-overflow: ellipsis;
padding-right: 30px; padding-right: 30px;
position: relative; position: relative;
width: auto;
@media (max-width: map-get($grid-breakpoints, sm)-1) {
width: 100%;
}
}
.file-holder & {
.file-actions {
position: static;
}
} }
.btn-clipboard { .btn-clipboard {
......
...@@ -243,3 +243,15 @@ label { ...@@ -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;
}
}
...@@ -268,8 +268,6 @@ ...@@ -268,8 +268,6 @@
.navbar-sub-nav, .navbar-sub-nav,
.navbar-nav { .navbar-nav {
align-items: center;
> li { > li {
> a:hover, > a:hover,
> a:focus { > a:focus {
......
...@@ -45,4 +45,9 @@ ...@@ -45,4 +45,9 @@
&.status-box-upcoming { &.status-box-upcoming {
background: $gl-text-color-secondary; background: $gl-text-color-secondary;
} }
&.status-box-milestone {
color: $gl-text-color;
background: $gray-darker;
}
} }
...@@ -261,12 +261,16 @@ ...@@ -261,12 +261,16 @@
vertical-align: baseline; vertical-align: baseline;
} }
a.autodevops-badge { a {
color: $white-light; color: $gl-text-color;
}
a.autodevops-link { &.autodevops-badge {
color: $gl-link-color; color: $white-light;
}
&.autodevops-link {
color: $gl-link-color;
}
} }
.commit-row-description { .commit-row-description {
......
...@@ -14,8 +14,8 @@ ...@@ -14,8 +14,8 @@
background-color: $gray-normal; background-color: $gray-normal;
} }
.diff-toggle-caret { svg {
padding-right: 6px; vertical-align: text-bottom;
} }
} }
...@@ -737,6 +737,10 @@ ...@@ -737,6 +737,10 @@
max-width: 560px; max-width: 560px;
width: 100%; width: 100%;
z-index: 150; z-index: 150;
min-height: $dropdown-min-height;
max-height: $dropdown-max-height;
overflow-y: auto;
margin-bottom: 0;
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
left: $gl-padding; left: $gl-padding;
......
...@@ -79,6 +79,7 @@ ...@@ -79,6 +79,7 @@
justify-content: space-between; justify-content: space-between;
padding: $gl-padding; padding: $gl-padding;
border-radius: $border-radius-default; border-radius: $border-radius-default;
border: 1px solid $theme-gray-100;
&.sortable-ghost { &.sortable-ghost {
opacity: 0.3; opacity: 0.3;
...@@ -89,6 +90,7 @@ ...@@ -89,6 +90,7 @@
cursor: move; cursor: move;
cursor: -webkit-grab; cursor: -webkit-grab;
cursor: -moz-grab; cursor: -moz-grab;
border: 0;
&:active { &:active {
cursor: -webkit-grabbing; cursor: -webkit-grabbing;
......
...@@ -737,6 +737,10 @@ ...@@ -737,6 +737,10 @@
> *:not(:last-child) { > *:not(:last-child) {
margin-right: .3em; margin-right: .3em;
} }
svg {
vertical-align: text-top;
}
} }
.deploy-link { .deploy-link {
......
...@@ -3,8 +3,20 @@ ...@@ -3,8 +3,20 @@
} }
.milestones { .milestones {
padding: $gl-padding-8;
margin-top: $gl-padding-8;
border-radius: $border-radius-default;
background-color: $theme-gray-100;
.milestone { .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 { h4 {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
...@@ -13,6 +25,24 @@ ...@@ -13,6 +25,24 @@
.progress { .progress {
width: 100%; width: 100%;
height: 6px; 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 @@ ...@@ -229,6 +259,10 @@
} }
} }
.milestone-range {
color: $gl-text-color-tertiary;
}
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
.milestone-banner-text, .milestone-banner-text,
.milestone-banner-link { .milestone-banner-link {
......
...@@ -255,25 +255,12 @@ ...@@ -255,25 +255,12 @@
} }
} }
.modal-doorkeepr-auth,
.doorkeeper-app-form {
.scope-description {
color: $theme-gray-700;
}
}
.modal-doorkeepr-auth { .modal-doorkeepr-auth {
.modal-body { .modal-body {
padding: $gl-padding; padding: $gl-padding;
} }
} }
.doorkeeper-app-form {
.scope-description {
margin: 0 0 5px 17px;
}
}
.deprecated-service { .deprecated-service {
cursor: default; cursor: default;
} }
......
...@@ -52,8 +52,7 @@ class Admin::HooksController < Admin::ApplicationController ...@@ -52,8 +52,7 @@ class Admin::HooksController < Admin::ApplicationController
end end
def hook_logs def hook_logs
@hook_logs ||= @hook_logs ||= hook.web_hook_logs.recent.page(params[:page])
Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page])
end end
def hook_params def hook_params
......
...@@ -58,8 +58,7 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -58,8 +58,7 @@ class Projects::HooksController < Projects::ApplicationController
end end
def hook_logs def hook_logs
@hook_logs ||= @hook_logs ||= hook.web_hook_logs.recent.page(params[:page])
Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page])
end end
def hook_params def hook_params
......
...@@ -93,7 +93,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController ...@@ -93,7 +93,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
end end
def lfs_check_batch_operation! def lfs_check_batch_operation!
if upload_request? && Gitlab::Database.read_only? if batch_operation_disallowed?
render( render(
json: { json: {
message: lfs_read_only_message message: lfs_read_only_message
...@@ -104,6 +104,11 @@ class Projects::LfsApiController < Projects::GitHttpClientController ...@@ -104,6 +104,11 @@ class Projects::LfsApiController < Projects::GitHttpClientController
end end
end end
# Overridden in EE
def batch_operation_disallowed?
upload_request? && Gitlab::Database.read_only?
end
# Overridden in EE # Overridden in EE
def lfs_read_only_message def lfs_read_only_message
_('You cannot write to this read-only GitLab instance.') _('You cannot write to this read-only GitLab instance.')
......
...@@ -77,7 +77,7 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -77,7 +77,7 @@ class Projects::MilestonesController < Projects::ApplicationController
def promote def promote
promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone) 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| respond_to do |format|
format.html do format.html do
redirect_to project_milestones_path(project) redirect_to project_milestones_path(project)
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
# current_user - which user use # current_user - which user use
# params: # params:
# scope: 'created_by_me' or 'assigned_to_me' or 'all' # 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 # group_id: integer
# project_id: integer # project_id: integer
# milestone_title: string # milestone_title: string
...@@ -311,6 +311,8 @@ class IssuableFinder ...@@ -311,6 +311,8 @@ class IssuableFinder
items.respond_to?(:merged) ? items.merged : items.closed items.respond_to?(:merged) ? items.merged : items.closed
when 'opened' when 'opened'
items.opened items.opened
when 'locked'
items.where(state: 'locked')
else else
items items
end end
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
# current_user - which user use # current_user - which user use
# params: # params:
# scope: 'created_by_me' or 'assigned_to_me' or 'all' # 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 # group_id: integer
# project_id: integer # project_id: integer
# milestone_title: string # milestone_title: string
......
...@@ -108,7 +108,7 @@ module MergeRequestsHelper ...@@ -108,7 +108,7 @@ module MergeRequestsHelper
data_attrs = { data_attrs = {
action: tab.to_s, action: tab.to_s,
target: "##{tab}", target: "##{tab}",
toggle: options.fetch(:force_link, false) ? '' : 'tab' toggle: options.fetch(:force_link, false) ? '' : 'tabvue'
} }
url = case tab url = case tab
......
...@@ -148,6 +148,7 @@ module NotesHelper ...@@ -148,6 +148,7 @@ module NotesHelper
members: autocomplete, members: autocomplete,
issues: autocomplete, issues: autocomplete,
mergeRequests: autocomplete, mergeRequests: autocomplete,
epics: autocomplete,
milestones: autocomplete, milestones: autocomplete,
labels: autocomplete labels: autocomplete
} }
......
...@@ -7,6 +7,11 @@ class WebHookLog < ActiveRecord::Base ...@@ -7,6 +7,11 @@ class WebHookLog < ActiveRecord::Base
validates :web_hook, presence: true validates :web_hook, presence: true
def self.recent
where('created_at >= ?', 2.days.ago.beginning_of_day)
.order(created_at: :desc)
end
def success? def success?
response_status =~ /^2/ response_status =~ /^2/
end end
......
...@@ -48,7 +48,7 @@ class Issue < ActiveRecord::Base ...@@ -48,7 +48,7 @@ class Issue < ActiveRecord::Base
scope :unassigned, -> { where('NOT EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') } 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 :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 :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) } 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) } 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 ...@@ -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_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_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) } scope :preload_associations, -> { preload(:labels, project: :namespace) }
......
...@@ -128,14 +128,9 @@ class MergeRequest < ActiveRecord::Base ...@@ -128,14 +128,9 @@ class MergeRequest < ActiveRecord::Base
end end
after_transition unchecked: :cannot_be_merged do |merge_request, transition| after_transition unchecked: :cannot_be_merged do |merge_request, transition|
begin if merge_request.notify_conflict?
if merge_request.notify_conflict? NotificationService.new.merge_request_unmergeable(merge_request)
NotificationService.new.merge_request_unmergeable(merge_request) TodoService.new.merge_request_became_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.
end end
end end
...@@ -707,7 +702,14 @@ class MergeRequest < ActiveRecord::Base ...@@ -707,7 +702,14 @@ class MergeRequest < ActiveRecord::Base
end end
def notify_conflict? 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 end
def related_notes def related_notes
......
...@@ -67,11 +67,11 @@ class BambooService < CiService ...@@ -67,11 +67,11 @@ class BambooService < CiService
def execute(data) def execute(data)
return unless supported_events.include?(data[:object_kind]) return unless supported_events.include?(data[:object_kind])
get_path("updateAndBuild.action?buildKey=#{build_key}") get_path("updateAndBuild.action", { buildKey: build_key })
end end
def calculate_reactive_cache(sha, ref) 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) } { build_page: read_build_page(response), commit_status: read_commit_status(response) }
end end
...@@ -113,18 +113,20 @@ class BambooService < CiService ...@@ -113,18 +113,20 @@ class BambooService < CiService
URI.join("#{bamboo_url}/", path).to_s URI.join("#{bamboo_url}/", path).to_s
end end
def get_path(path) def get_path(path, query_params = {})
url = build_url(path) url = build_url(path)
if username.blank? && password.blank? if username.blank? && password.blank?
Gitlab::HTTP.get(url, verify: false) Gitlab::HTTP.get(url, verify: false, query: query_params)
else else
url << '&os_authType=basic' query_params[:os_authType] = 'basic'
Gitlab::HTTP.get(url, verify: false, Gitlab::HTTP.get(url,
basic_auth: { verify: false,
username: username, query: query_params,
password: password basic_auth: {
}) username: username,
password: password
})
end end
end end
end end
...@@ -99,11 +99,11 @@ class Repository ...@@ -99,11 +99,11 @@ class Repository
"#<#{self.class.name}:#{@disk_path}>" "#<#{self.class.name}:#{@disk_path}>"
end end
def commit(ref = 'HEAD') def commit(ref = nil)
return nil unless exists? return nil unless exists?
return ref if ref.is_a?(::Commit) return ref if ref.is_a?(::Commit)
find_commit(ref) find_commit(ref || root_ref)
end end
# Finding a commit by the passed SHA # Finding a commit by the passed SHA
...@@ -283,6 +283,10 @@ class Repository ...@@ -283,6 +283,10 @@ class Repository
) )
end end
def cached_methods
CACHED_METHODS
end
def expire_tags_cache def expire_tags_cache
expire_method_caches(%i(tag_names tag_count)) expire_method_caches(%i(tag_names tag_count))
@tags = nil @tags = nil
...@@ -423,7 +427,7 @@ class Repository ...@@ -423,7 +427,7 @@ class Repository
# Runs code after the HEAD of a repository is changed. # Runs code after the HEAD of a repository is changed.
def after_change_head def after_change_head
expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys) expire_all_method_caches
end end
# Runs code after a repository has been forked/imported. # Runs code after a repository has been forked/imported.
......
...@@ -244,7 +244,7 @@ class User < ActiveRecord::Base ...@@ -244,7 +244,7 @@ class User < ActiveRecord::Base
scope :blocked, -> { with_states(:blocked, :ldap_blocked) } scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) } scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active).non_internal } 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 :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_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')) } scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
......
...@@ -27,6 +27,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -27,6 +27,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def statistics_buttons(show_auto_devops_callout:) def statistics_buttons(show_auto_devops_callout:)
[ [
readme_anchor_data,
changelog_anchor_data, changelog_anchor_data,
license_anchor_data, license_anchor_data,
contribution_guide_anchor_data, contribution_guide_anchor_data,
...@@ -212,11 +213,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -212,11 +213,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
def readme_anchor_data 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, OpenStruct.new(enabled: false,
label: _('Add Readme'), label: _('Add Readme'),
link: add_readme_path) link: add_readme_path)
elsif repository.readme.present? elsif repository.readme
OpenStruct.new(enabled: true, OpenStruct.new(enabled: true,
label: _('Readme'), label: _('Readme'),
link: default_view != 'readme' ? readme_path : '#readme') link: default_view != 'readme' ? readme_path : '#readme')
......
...@@ -35,7 +35,7 @@ class BuildDetailsEntity < JobEntity ...@@ -35,7 +35,7 @@ class BuildDetailsEntity < JobEntity
def build_failed_issue_options def build_failed_issue_options
{ title: "Job Failed ##{build.id}", { 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 end
def current_user def current_user
......
...@@ -58,7 +58,8 @@ module Issues ...@@ -58,7 +58,8 @@ module Issues
def cloneable_label_ids def cloneable_label_ids
params = { params = {
project_id: @new_project.id, 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) LabelsFinder.new(current_user, params).execute.pluck(:id)
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- breadcrumb_title "Dashboard" - breadcrumb_title "Dashboard"
%div{ class: container_class } %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 .admin-dashboard.prepend-top-default
.row .row
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
%h3.text-center %h3.text-center
Users: Users:
= approximate_count_with_delimiters(@counts, User) = approximate_count_with_delimiters(@counts, User)
= render_if_exists 'users_statistics' = render_if_exists 'admin/dashboard/users_statistics'
%hr %hr
= link_to 'New user', new_admin_user_path, class: "btn btn-new" = link_to 'New user', new_admin_user_path, class: "btn btn-new"
.col-sm-4 .col-sm-4
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
%span.light.float-right %span.light.float-right
= boolean_to_icon Gitlab::IncomingEmail.enabled? = boolean_to_icon Gitlab::IncomingEmail.enabled?
= render_if_exists 'elastic_and_geo' = render_if_exists 'admin/dashboard/elastic_and_geo'
- container_reg = "Container Registry" - container_reg = "Container Registry"
%p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") } %p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") }
...@@ -151,7 +151,7 @@ ...@@ -151,7 +151,7 @@
%span.float-right %span.float-right
= Gitlab::Pages::VERSION = Gitlab::Pages::VERSION
= render_if_exists 'geo' = render_if_exists 'admin/dashboard/geo'
%p %p
Ruby Ruby
......
...@@ -7,10 +7,10 @@ ...@@ -7,10 +7,10 @@
- values = Gitlab::Auth::OAuth::Provider.providers.map { |name| ["#{Gitlab::Auth::OAuth::Provider.label_for(name)} (#{name})", name] } - 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' = f.select :provider, values, { allow_blank: false }, class: 'form-control'
.form-group.row .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 .col-sm-10
= f.text_field :extern_uid, class: 'form-control', required: true = f.text_field :extern_uid, class: 'form-control', required: true
.form-actions .form-actions
= f.submit 'Save changes', class: "btn btn-save" = f.submit _('Save changes'), class: "btn btn-save"
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
= identity.extern_uid = identity.extern_uid
%td %td
= link_to edit_admin_user_identity_path(@user, identity), class: 'btn btn-sm btn-grouped' do = 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, = link_to [:admin, @user, identity], method: :delete,
class: 'btn btn-sm btn-danger', class: 'btn btn-sm btn-danger',
data: { confirm: "Are you sure you want to remove this identity?" } do data: { confirm: _("Are you sure you want to remove this identity?") } do
Delete = _('Delete')
- page_title "Edit", @identity.provider, "Identities", @user.name, "Users" - page_title _("Edit"), @identity.provider, _("Identities"), @user.name, _("Users")
%h3.page-title %h3.page-title
Edit identity for #{@user.name} = _('Edit identity for %{user_name}') % { user_name: @user.name }
%hr %hr
= render 'form' = render 'form'
- page_title "Identities", @user.name, "Users" - page_title _("Identities"), @user.name, _("Users")
= render 'admin/users/head' = 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? - if @identities.present?
.table-holder .table-holder
%table.table %table.table
%thead %thead
%tr %tr
%th Provider %th= _('Provider')
%th Identifier %th= _('Identifier')
%th %th
= render @identities = render @identities
- else - else
%h4 This user has no identities %h4= _('This user has no identities')
- page_title "New Identity" - page_title _("New Identity")
%h3.page-title New identity %h3.page-title= _('New identity')
%hr %hr
= render 'form' = render 'form'
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
- @pre_auth.scopes.each do |scope| - @pre_auth.scopes.each do |scope|
%li %li
%strong= t scope, scope: [:doorkeeper, :scopes] %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-actions.text-right
= form_tag oauth_authorization_path, method: :delete, class: 'inline' do = form_tag oauth_authorization_path, method: :delete, class: 'inline' do
= hidden_field_tag :client_id, @pre_auth.client.uid = hidden_field_tag :client_id, @pre_auth.client.uid
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
= render 'shared/issuable/nav', type: :issues = render 'shared/issuable/nav', type: :issues
.nav-controls .nav-controls
= render 'shared/issuable/feed_buttons' = 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 = render 'shared/issuable/search_bar', type: :issues
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment