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