Commit 13827e10 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into vue-mr-widget-state

* master: (39 commits)
  WebIDE: Fix Commit bugs
  Resolve "Projects API: filter 'with_issues_enabled=true' returns projects with 'issues_enabled=false'"
  Fix incorrect path for gitlab-shell authorized keys helper
  Fix duplicate item in protected branch/tag dropdown
  dynamic imports for groups: pages
  Refactor dispatcher project mr edit and creation diff path
  Refactor dispatcher project mr creations new path
  Rework indexes on redirect_routes to be more effective and enforce a case insensitive unique path
  Upgraded Karma Dependencies
  Fix conflict with ee icon
  Remove trailing space after author name in note header
  Delete conflicting orphaned routes
  Make the 'Edit' links consistent in the issuable sidebar
  Fix a bug calculating artifact size for project statistics
  Update rubocop, rubocop-rspec, and gitlab-styles
  Correctly escape UTF-8 path elements for uploads
  Change cursor to default for disabled buttons
  Fix eslint
  Remove Event listener and make code more readable.
  Update CHANGELOG.md for 10.1.7
  ...
parents 86bc36c5 1b51018e
This diff is collapsed.
...@@ -482,6 +482,10 @@ entry. ...@@ -482,6 +482,10 @@ entry.
- Add Gitaly metrics to the performance bar. - Add Gitaly metrics to the performance bar.
## 10.1.7 (2018-01-18)
- No changes.
## 10.1.6 (2018-01-11) ## 10.1.6 (2018-01-11)
### Security (8 changes, 1 of them is from the community) ### Security (8 changes, 1 of them is from the community)
......
...@@ -341,10 +341,10 @@ group :development, :test do ...@@ -341,10 +341,10 @@ group :development, :test do
gem 'spring-commands-rspec', '~> 1.0.4' gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-spinach', '~> 1.1.0'
gem 'gitlab-styles', '~> 2.2.0', require: false gem 'gitlab-styles', '~> 2.3', require: false
# Pin these dependencies, otherwise a new rule could break the CI pipelines # Pin these dependencies, otherwise a new rule could break the CI pipelines
gem 'rubocop', '~> 0.52.0' gem 'rubocop', '~> 0.52.1'
gem 'rubocop-rspec', '~> 1.20.1' gem 'rubocop-rspec', '~> 1.22.1'
gem 'scss_lint', '~> 0.56.0', require: false gem 'scss_lint', '~> 0.56.0', require: false
gem 'haml_lint', '~> 0.26.0', require: false gem 'haml_lint', '~> 0.26.0', require: false
......
...@@ -304,7 +304,7 @@ GEM ...@@ -304,7 +304,7 @@ GEM
mime-types (>= 1.16) mime-types (>= 1.16)
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab-markup (1.6.3) gitlab-markup (1.6.3)
gitlab-styles (2.2.0) gitlab-styles (2.3.0)
rubocop (~> 0.51) rubocop (~> 0.51)
rubocop-gitlab-security (~> 0.1.0) rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19) rubocop-rspec (~> 1.19)
...@@ -583,7 +583,7 @@ GEM ...@@ -583,7 +583,7 @@ GEM
rubypants (~> 0.2) rubypants (~> 0.2)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (0.9.6) os (0.9.6)
parallel (1.12.0) parallel (1.12.1)
parser (2.4.0.2) parser (2.4.0.2)
ast (~> 2.3) ast (~> 2.3)
parslet (1.5.0) parslet (1.5.0)
...@@ -786,7 +786,7 @@ GEM ...@@ -786,7 +786,7 @@ GEM
pg pg
rails rails
sqlite3 sqlite3
rubocop (0.52.0) rubocop (0.52.1)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.4.0.2, < 3.0) parser (>= 2.4.0.2, < 3.0)
powerpack (~> 0.1) powerpack (~> 0.1)
...@@ -795,8 +795,8 @@ GEM ...@@ -795,8 +795,8 @@ GEM
unicode-display_width (~> 1.0, >= 1.0.1) unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-gitlab-security (0.1.1) rubocop-gitlab-security (0.1.1)
rubocop (>= 0.51) rubocop (>= 0.51)
rubocop-rspec (1.20.1) rubocop-rspec (1.22.1)
rubocop (>= 0.51.0) rubocop (>= 0.52.1)
ruby-fogbugz (0.2.1) ruby-fogbugz (0.2.1)
crack (~> 0.4) crack (~> 0.4)
ruby-prof (0.16.2) ruby-prof (0.16.2)
...@@ -1060,7 +1060,7 @@ DEPENDENCIES ...@@ -1060,7 +1060,7 @@ DEPENDENCIES
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2) gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.2.0) gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4) gitlab_omniauth-ldap (~> 2.0.4)
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4) gollum-rugged_adapter (~> 0.4.4)
...@@ -1162,8 +1162,8 @@ DEPENDENCIES ...@@ -1162,8 +1162,8 @@ DEPENDENCIES
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3) rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5) rspec_profiling (~> 0.0.5)
rubocop (~> 0.52.0) rubocop (~> 0.52.1)
rubocop-rspec (~> 1.20.1) rubocop-rspec (~> 1.22.1)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2) ruby-prof (~> 0.16.2)
ruby_parser (~> 3.8) ruby_parser (~> 3.8)
......
...@@ -218,6 +218,7 @@ const Api = { ...@@ -218,6 +218,7 @@ const Api = {
(jqXHR, textStatus, errorThrown) => { (jqXHR, textStatus, errorThrown) => {
const error = new Error(`${options.url}: ${errorThrown}`); const error = new Error(`${options.url}: ${errorThrown}`);
error.textStatus = textStatus; error.textStatus = textStatus;
if (jqXHR && jqXHR.responseJSON) error.responseJSON = jqXHR.responseJSON;
reject(error); reject(error);
}, },
); );
......
...@@ -65,7 +65,17 @@ export default class CreateItemDropdown { ...@@ -65,7 +65,17 @@ export default class CreateItemDropdown {
getData(term, callback) { getData(term, callback) {
this.getDataOption(term, (data = []) => { this.getDataOption(term, (data = []) => {
callback(data.concat(this.selectedItem || [])); // Ensure the selected item isn't already in the data to avoid duplicates
const alreadyHasSelectedItem = this.selectedItem && data.some(item =>
item.id === this.selectedItem.id,
);
let uniqueData = data;
if (!alreadyHasSelectedItem) {
uniqueData = data.concat(this.selectedItem || []);
}
callback(uniqueData);
}); });
} }
......
This diff is collapsed.
...@@ -10,6 +10,7 @@ const hideFlash = (flashEl, fadeTransition = true) => { ...@@ -10,6 +10,7 @@ const hideFlash = (flashEl, fadeTransition = true) => {
flashEl.addEventListener('transitionend', () => { flashEl.addEventListener('transitionend', () => {
flashEl.remove(); flashEl.remove();
if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown');
}, { }, {
once: true, once: true,
passive: true, passive: true,
...@@ -64,6 +65,7 @@ const createFlash = function createFlash( ...@@ -64,6 +65,7 @@ const createFlash = function createFlash(
parent = document, parent = document,
actionConfig = null, actionConfig = null,
fadeTransition = true, fadeTransition = true,
addBodyClass = false,
) { ) {
const flashContainer = parent.querySelector('.flash-container'); const flashContainer = parent.querySelector('.flash-container');
...@@ -86,6 +88,8 @@ const createFlash = function createFlash( ...@@ -86,6 +88,8 @@ const createFlash = function createFlash(
flashContainer.style.display = 'block'; flashContainer.style.display = 'block';
if (addBodyClass) document.body.classList.add('flash-shown');
return flashContainer; return flashContainer;
}; };
......
...@@ -68,12 +68,8 @@ export default { ...@@ -68,12 +68,8 @@ export default {
this.commitChanges({ payload, newMr: this.startNewMR }) this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => { .then(() => {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
this.$store.dispatch('getTreeData', { this.commitMessage = '';
projectId: this.currentProjectId, this.startNewMR = false;
branch: this.currentBranchId,
endpoint: `/tree/${this.currentBranchId}`,
force: true,
});
}) })
.catch(() => { .catch(() => {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
...@@ -153,6 +149,7 @@ you started editing. Would you like to create a new branch?`)" ...@@ -153,6 +149,7 @@ you started editing. Would you like to create a new branch?`)"
type="submit" type="submit"
:disabled="commitButtonDisabled" :disabled="commitButtonDisabled"
class="btn btn-default btn-sm append-right-10 prepend-left-10" class="btn btn-default btn-sm append-right-10 prepend-left-10"
:class="{ disabled: submitCommitsLoading }"
> >
<i <i
v-if="submitCommitsLoading" v-if="submitCommitsLoading"
......
...@@ -70,7 +70,10 @@ export default { ...@@ -70,7 +70,10 @@ export default {
this.editor.createInstance(this.$refs.editor); this.editor.createInstance(this.$refs.editor);
}) })
.then(() => this.setupEditor()) .then(() => this.setupEditor())
.catch(() => flash('Error setting up monaco. Please try again.')); .catch((err) => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err;
});
}, },
setupEditor() { setupEditor() {
if (!this.activeFile) return; if (!this.activeFile) return;
......
...@@ -35,9 +35,12 @@ ...@@ -35,9 +35,12 @@
return this.file.type === 'tree'; return this.file.type === 'tree';
}, },
levelIndentation() { levelIndentation() {
if (this.file.level > 0) {
return { return {
marginLeft: `${this.file.level * 16}px`, marginLeft: `${this.file.level * 16}px`,
}; };
}
return {};
}, },
shortId() { shortId() {
return this.file.id.substr(0, 8); return this.file.id.substr(0, 8);
...@@ -111,7 +114,7 @@ ...@@ -111,7 +114,7 @@
/> />
<i <i
class="fa" class="fa"
v-if="changedClass" v-if="file.changed || file.tempFile"
:class="changedClass" :class="changedClass"
aria-hidden="true" aria-hidden="true"
> >
......
...@@ -84,13 +84,13 @@ router.beforeEach((to, from, next) => { ...@@ -84,13 +84,13 @@ router.beforeEach((to, from, next) => {
} }
}) })
.catch((e) => { .catch((e) => {
flash('Error while loading the branch files. Please try again.'); flash('Error while loading the branch files. Please try again.', 'alert', document, null, false, true);
throw e; throw e;
}); });
} }
}) })
.catch((e) => { .catch((e) => {
flash('Error while loading the project data. Please try again.'); flash('Error while loading the project data. Please try again.', 'alert', document, null, false, true);
throw e; throw e;
}); });
} }
......
...@@ -55,7 +55,7 @@ export default class Editor { ...@@ -55,7 +55,7 @@ export default class Editor {
attachModel(model) { attachModel(model) {
this.instance.setModel(model.getModel()); this.instance.setModel(model.getModel());
this.dirtyDiffController.attachModel(model); if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
this.currentModel = model; this.currentModel = model;
...@@ -68,7 +68,7 @@ export default class Editor { ...@@ -68,7 +68,7 @@ export default class Editor {
return acc; return acc;
}, {})); }, {}));
this.dirtyDiffController.reDecorate(model); if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
} }
clearEditor() { clearEditor() {
......
...@@ -3,6 +3,7 @@ import { visitUrl } from '../../lib/utils/url_utility'; ...@@ -3,6 +3,7 @@ import { visitUrl } from '../../lib/utils/url_utility';
import flash from '../../flash'; import flash from '../../flash';
import service from '../services'; import service from '../services';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { stripHtml } from '../../lib/utils/text_utility';
export const redirectToUrl = (_, url) => visitUrl(url); export const redirectToUrl = (_, url) => visitUrl(url);
...@@ -81,7 +82,7 @@ export const checkCommitStatus = ({ state }) => ...@@ -81,7 +82,7 @@ export const checkCommitStatus = ({ state }) =>
return false; return false;
}) })
.catch(() => flash('Error checking branch data. Please try again.')); .catch(() => flash('Error checking branch data. Please try again.', 'alert', document, null, false, true));
export const commitChanges = ( export const commitChanges = (
{ commit, state, dispatch, getters }, { commit, state, dispatch, getters },
...@@ -92,7 +93,7 @@ export const commitChanges = ( ...@@ -92,7 +93,7 @@ export const commitChanges = (
.then((data) => { .then((data) => {
const { branch } = payload; const { branch } = payload;
if (!data.short_id) { if (!data.short_id) {
flash(data.message); flash(data.message, 'alert', document, null, false, true);
return; return;
} }
...@@ -105,19 +106,25 @@ export const commitChanges = ( ...@@ -105,19 +106,25 @@ export const commitChanges = (
}, },
}; };
let commitMsg = `Your changes have been committed. Commit ${data.short_id}`;
if (data.stats) {
commitMsg += ` with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`;
}
flash( flash(
`Your changes have been committed. Commit ${data.short_id} with ${ commitMsg,
data.stats.additions
} additions, ${data.stats.deletions} deletions.`,
'notice', 'notice',
); document,
null,
false,
true);
window.dispatchEvent(new Event('resize'));
if (newMr) { if (newMr) {
dispatch('discardAllChanges');
dispatch( dispatch(
'redirectToUrl', 'redirectToUrl',
`${ `${selectedProject.web_url}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
selectedProject.web_url
}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
); );
} else { } else {
commit(types.SET_BRANCH_WORKING_REFERENCE, { commit(types.SET_BRANCH_WORKING_REFERENCE, {
...@@ -134,12 +141,18 @@ export const commitChanges = ( ...@@ -134,12 +141,18 @@ export const commitChanges = (
}); });
dispatch('discardAllChanges'); dispatch('discardAllChanges');
dispatch('closeAllFiles');
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
}) })
.catch(() => flash('Error committing changes. Please try again.')); .catch((err) => {
let errMsg = 'Error committing changes. Please try again.';
if (err.responseJSON && err.responseJSON.message) {
errMsg += ` (${stripHtml(err.responseJSON.message)})`;
}
flash(errMsg, 'alert', document, null, false, true);
window.dispatchEvent(new Event('resize'));
});
export const createTempEntry = ( export const createTempEntry = (
{ state, dispatch }, { state, dispatch },
......
...@@ -17,7 +17,7 @@ export const getBranchData = ( ...@@ -17,7 +17,7 @@ export const getBranchData = (
resolve(data); resolve(data);
}) })
.catch(() => { .catch(() => {
flash('Error loading branch data. Please try again.'); flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
}); });
} else { } else {
......
...@@ -69,7 +69,7 @@ export const getFileData = ({ state, commit, dispatch }, file) => { ...@@ -69,7 +69,7 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
}) })
.catch(() => { .catch(() => {
commit(types.TOGGLE_LOADING, file); commit(types.TOGGLE_LOADING, file);
flash('Error loading file data. Please try again.'); flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
}); });
}; };
...@@ -77,22 +77,28 @@ export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFile ...@@ -77,22 +77,28 @@ export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFile
.then((raw) => { .then((raw) => {
commit(types.SET_FILE_RAW_DATA, { file, raw }); commit(types.SET_FILE_RAW_DATA, { file, raw });
}) })
.catch(() => flash('Error loading file content. Please try again.')); .catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true));
export const changeFileContent = ({ commit }, { file, content }) => { export const changeFileContent = ({ commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content }); commit(types.UPDATE_FILE_CONTENT, { file, content });
}; };
export const setFileLanguage = ({ state, commit }, { fileLanguage }) => { export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
if (state.selectedFile) {
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage }); commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
}
}; };
export const setFileEOL = ({ state, commit }, { eol }) => { export const setFileEOL = ({ state, commit }, { eol }) => {
if (state.selectedFile) {
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol }); commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
}
}; };
export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => { export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
if (state.selectedFile) {
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn }); commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
}
}; };
export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => { export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
...@@ -112,7 +118,7 @@ export const createTempFile = ({ state, commit, dispatch }, { projectId, branchI ...@@ -112,7 +118,7 @@ export const createTempFile = ({ state, commit, dispatch }, { projectId, branchI
url: newUrl, url: newUrl,
}); });
if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`); if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`, 'alert', document, null, false, true);
commit(types.CREATE_TMP_FILE, { commit(types.CREATE_TMP_FILE, {
parent, parent,
......
...@@ -18,7 +18,7 @@ export const getProjectData = ( ...@@ -18,7 +18,7 @@ export const getProjectData = (
resolve(data); resolve(data);
}) })
.catch(() => { .catch(() => {
flash('Error loading project data. Please try again.'); flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Project not loaded ${namespace}/${projectId}`)); reject(new Error(`Project not loaded ${namespace}/${projectId}`));
}); });
} else { } else {
......
...@@ -52,7 +52,7 @@ export const getTreeData = ( ...@@ -52,7 +52,7 @@ export const getTreeData = (
resolve(data); resolve(data);
}) })
.catch((e) => { .catch((e) => {
flash('Error loading tree data. Please try again.'); flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
if (tree) commit(types.TOGGLE_LOADING, tree); if (tree) commit(types.TOGGLE_LOADING, tree);
reject(e); reject(e);
}); });
...@@ -151,7 +151,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s ...@@ -151,7 +151,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
dispatch('getLastCommitData', tree); dispatch('getLastCommitData', tree);
}) })
.catch(() => flash('Error fetching log data.')); .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
}; };
export const updateDirectoryData = ( export const updateDirectoryData = (
......
...@@ -64,7 +64,7 @@ export default { ...@@ -64,7 +64,7 @@ export default {
}, },
[types.DISCARD_FILE_CHANGES](state, file) { [types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, { Object.assign(file, {
content: '', content: file.raw,
changed: false, changed: false,
}); });
}, },
......
...@@ -152,6 +152,13 @@ ...@@ -152,6 +152,13 @@
hasUpdated() { hasUpdated() {
return !!this.state.updatedAt; return !!this.state.updatedAt;
}, },
issueChanged() {
const descriptionChanged =
this.initialDescriptionText !== this.store.formState.description;
const titleChanged =
this.initialTitleText !== this.store.formState.title;
return descriptionChanged || titleChanged;
},
}, },
created() { created() {
this.service = new Service(this.endpoint); this.service = new Service(this.endpoint);
...@@ -176,6 +183,8 @@ ...@@ -176,6 +183,8 @@
} }
}); });
window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
eventHub.$on('delete.issuable', this.deleteIssuable); eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable); eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm); eventHub.$on('close.form', this.closeForm);
...@@ -186,8 +195,17 @@ ...@@ -186,8 +195,17 @@
eventHub.$off('update.issuable', this.updateIssuable); eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm); eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm); eventHub.$off('open.form', this.openForm);
window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent);
}, },
methods: { methods: {
handleBeforeUnloadEvent(e) {
const event = e;
if (this.showForm && this.issueChanged) {
event.returnValue = 'Are you sure you want to lose your issue information?';
}
return undefined;
},
openForm() { openForm() {
if (!this.showForm) { if (!this.showForm) {
this.showForm = true; this.showForm = true;
......
...@@ -72,4 +72,4 @@ export function capitalizeFirstCharacter(text) { ...@@ -72,4 +72,4 @@ export function capitalizeFirstCharacter(text) {
* @param {*} replace * @param {*} replace
* @returns {String} * @returns {String}
*/ */
export const stripeHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace); export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
...@@ -66,9 +66,7 @@ ...@@ -66,9 +66,7 @@
<template> <template>
<div class="note-header-info"> <div class="note-header-info">
<a :href="author.path"> <a :href="author.path">
<span class="note-header-author-name"> <span class="note-header-author-name">{{ author.name }}</span>
{{ author.name }}
</span>
<span class="note-headline-light"> <span class="note-headline-light">
@{{ author.username }} @{{ author.username }}
</span> </span>
......
/* eslint-disable import/prefer-default-export */
export const FILTERED_SEARCH = {
MERGE_REQUESTS: 'merge_requests',
ISSUES: 'issues',
};
import Activities from '~/activities';
export default new Activities();
import groupAvatar from '~/group_avatar';
export default groupAvatar;
/* eslint-disable no-new */
import memberExpirationDate from '~/member_expiration_date';
import Members from '~/members';
import UsersSelect from '~/users_select';
export default () => {
memberExpirationDate();
new Members();
new UsersSelect();
};
import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
export default () => {
initFilteredSearch(FILTERED_SEARCH.ISSUES);
projectSelect();
};
import Labels from '~/labels';
export default new Labels();
import initLabels from '~/init_labels';
export default initLabels;
import Labels from '~/labels';
export default new Labels();
import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
export default () => {
initFilteredSearch(FILTERED_SEARCH.MERGE_REQUESTS);
projectSelect();
};
import BindInOut from '~/behaviors/bind_in_out';
import Group from '~/group';
import groupAvatar from '~/group_avatar';
export default () => {
BindInOut.initAll();
new Group(); // eslint-disable-line no-new
groupAvatar();
};
import SecretValues from '~/behaviors/secret_values';
export default () => {
const secretVariableTable = document.querySelector('.js-secret-variable-table');
if (secretVariableTable) {
const secretVariableTableValues = new SecretValues(secretVariableTable);
secretVariableTableValues.init();
}
};
/* eslint-disable no-new */
import NewGroupChild from '~/groups/new_group_child';
import notificationsDropdown from '~/notifications_dropdown';
import NotificationsForm from '~/notifications_form';
import ProjectsList from '~/projects_list';
import ShortcutsNavigation from '~/shortcuts_navigation';
export default () => {
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
new ShortcutsNavigation();
new NotificationsForm();
notificationsDropdown();
new ProjectsList();
if (newGroupChildWrapper) {
new NewGroupChild(newGroupChildWrapper);
}
};
/* eslint-disable import/prefer-default-export */
export const ISSUABLE_INDEX = {
MERGE_REQUEST: 'merge_request_',
ISSUE: 'issue_',
};
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
import setupProjectEdit from '~/project_edit'; import setupProjectEdit from '~/project_edit';
import ProjectNew from '../shared/project_new';
import projectAvatar from '../shared/project_avatar';
import initProjectPermissionsSettings from '../shared/permissions';
export default () => { export default () => {
new ProjectNew(); // eslint-disable-line no-new
setupProjectEdit(); setupProjectEdit();
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
projectAvatar();
initProjectPermissionsSettings();
}; };
import Project from './project';
import ShortcutsNavigation from '../../shortcuts_navigation';
export default () => {
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
};
/* eslint-disable no-new */ /* eslint-disable no-new */
import IssuableIndex from '~/issuable_index'; import IssuableIndex from '~/issuable_index';
import ShortcutsNavigation from '~/shortcuts_navigation'; import ShortcutsNavigation from '~/shortcuts_navigation';
import UsersSelect from '~/users_select'; import UsersSelect from '~/users_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
export default () => { export default () => {
const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); initFilteredSearch(FILTERED_SEARCH.ISSUES);
if (filteredSearchEnabled) { new IssuableIndex(ISSUABLE_INDEX.ISSUE);
const filteredSearchManager = new gl.FilteredSearchManager('issues');
filteredSearchManager.setup();
}
new IssuableIndex('issue_');
new ShortcutsNavigation(); new ShortcutsNavigation();
new UsersSelect(); new UsersSelect();
......
import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request';
export default initMergeRequest;
import Compare from '~/compare';
import MergeRequest from '~/merge_request';
export default () => {
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
if (mrNewCompareNode) {
new Compare({ // eslint-disable-line no-new
targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl,
sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl,
targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl,
});
} else {
const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
new MergeRequest({ // eslint-disable-line no-new
action: mrNewSubmitNode.dataset.mrSubmitAction,
});
}
};
import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request';
export default initMergeRequest;
import IssuableIndex from '~/issuable_index'; import IssuableIndex from '~/issuable_index';
import ShortcutsNavigation from '~/shortcuts_navigation'; import ShortcutsNavigation from '~/shortcuts_navigation';
import UsersSelect from '~/users_select'; import UsersSelect from '~/users_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
export default () => { export default () => {
const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); initFilteredSearch(FILTERED_SEARCH.MERGE_REQUESTS);
new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new
if (filteredSearchEnabled) {
const filteredSearchManager = new gl.FilteredSearchManager('merge_requests');
filteredSearchManager.setup();
}
new IssuableIndex('merge_request_'); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new
}; };
/* eslint-disable no-new */
import Diff from '~/diff';
import ShortcutsNavigation from '~/shortcuts_navigation';
import GLForm from '~/gl_form';
import IssuableForm from '~/issuable_form';
import LabelsSelect from '~/labels_select';
import MilestoneSelect from '~/milestone_select';
import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
export default () => {
new Diff();
new ShortcutsNavigation();
new GLForm($('.merge-request-form'), true);
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
new MilestoneSelect();
new IssuableTemplateSelectors();
};
import ProjectNew from '../shared/project_new';
import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
export default () => {
new ProjectNew(); // eslint-disable-line no-new
initProjectVisibilitySelector();
initProjectNew.bindEvents();
};
/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { visitUrl } from './lib/utils/url_utility'; import { visitUrl } from '../../lib/utils/url_utility';
import projectSelect from './project_select'; import projectSelect from '../../project_select';
export default class Project { export default class Project {
constructor() { constructor() {
......
<script> <script>
import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue'; import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue';
export default { export default {
components: { components: {
......
<script> <script>
import projectFeatureSetting from './project_feature_setting.vue'; import projectFeatureSetting from './project_feature_setting.vue';
import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue'; import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue';
import projectSettingRow from './project_setting_row.vue'; import projectSettingRow from './project_setting_row.vue';
import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
import { toggleHiddenClassBySelector } from '../external'; import { toggleHiddenClassBySelector } from '../external';
......
/* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/ /* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/
import VisibilitySelect from './visibility_select'; import VisibilitySelect from '../../../visibility_select';
function highlightChanges($elm) { function highlightChanges($elm) {
$elm.addClass('highlight-changes'); $elm.addClass('highlight-changes');
......
import Wikis from './wikis';
import ShortcutsWiki from '../../../shortcuts_wiki';
import ZenMode from '../../../zen_mode';
import GLForm from '../../../gl_form';
export default () => {
new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.wiki-form'), true); // eslint-disable-line no-new
};
import bp from './breakpoints'; import bp from '../../../breakpoints';
import { slugify } from './lib/utils/text_utility'; import { slugify } from '../../../lib/utils/text_utility';
export default class Wikis { export default class Wikis {
constructor() { constructor() {
......
export default (page) => {
const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search');
if (filteredSearchEnabled) {
const filteredSearchManager = new gl.FilteredSearchManager(page);
filteredSearchManager.setup();
}
};
...@@ -50,13 +50,13 @@ ...@@ -50,13 +50,13 @@
Pipeline Pipeline
</div> </div>
<div <div
class="table-section section-25 js-pipeline-commit pipeline-commit" class="table-section section-20 js-pipeline-commit pipeline-commit"
role="rowheader" role="rowheader"
> >
Commit Commit
</div> </div>
<div <div
class="table-section section-15 js-pipeline-stages pipeline-stages" class="table-section section-20 js-pipeline-stages pipeline-stages"
role="rowheader" role="rowheader"
> >
Stages Stages
......
...@@ -239,7 +239,7 @@ ...@@ -239,7 +239,7 @@
:auto-devops-help-path="autoDevopsHelpPath" :auto-devops-help-path="autoDevopsHelpPath"
/> />
<div class="table-section section-25"> <div class="table-section section-20">
<div <div
class="table-mobile-header" class="table-mobile-header"
role="rowheader"> role="rowheader">
...@@ -258,7 +258,7 @@ ...@@ -258,7 +258,7 @@
</div> </div>
</div> </div>
<div class="table-section section-wrap section-15 stage-cell"> <div class="table-section section-wrap section-20 stage-cell">
<div <div
class="table-mobile-header" class="table-mobile-header"
role="rowheader"> role="rowheader">
......
...@@ -100,8 +100,6 @@ const bindEvents = () => { ...@@ -100,8 +100,6 @@ const bindEvents = () => {
$projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl)); $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl));
}; };
document.addEventListener('DOMContentLoaded', bindEvents);
export default { export default {
bindEvents, bindEvents,
deriveProjectPathFromUrl, deriveProjectPathFromUrl,
......
...@@ -13,12 +13,10 @@ Mousetrap.stopCallback = (e, element, combo) => { ...@@ -13,12 +13,10 @@ Mousetrap.stopCallback = (e, element, combo) => {
}; };
export default class Shortcuts { export default class Shortcuts {
constructor(skipResetBindings) { constructor() {
this.onToggleHelp = this.onToggleHelp.bind(this); this.onToggleHelp = this.onToggleHelp.bind(this);
this.enabledHelp = []; this.enabledHelp = [];
if (!skipResetBindings) {
Mousetrap.reset();
}
Mousetrap.bind('?', this.onToggleHelp); Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch); Mousetrap.bind('s', Shortcuts.focusSearch);
Mousetrap.bind('f', this.focusFilter.bind(this)); Mousetrap.bind('f', this.focusFilter.bind(this));
......
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import _ from 'underscore'; import _ from 'underscore';
import Sidebar from './right_sidebar'; import Sidebar from './right_sidebar';
import ShortcutsNavigation from './shortcuts_navigation'; import Shortcuts from './shortcuts';
import { CopyAsGFM } from './behaviors/copy_as_gfm'; import { CopyAsGFM } from './behaviors/copy_as_gfm';
export default class ShortcutsIssuable extends ShortcutsNavigation { export default class ShortcutsIssuable extends Shortcuts {
constructor(isMergeRequest) { constructor(isMergeRequest) {
super(); super();
......
...@@ -39,7 +39,7 @@ export default { ...@@ -39,7 +39,7 @@ export default {
class="js-sidebar-dropdown-toggle edit-link pull-right" class="js-sidebar-dropdown-toggle edit-link pull-right"
href="#" href="#"
> >
Edit {{ __('Edit') }}
</a> </a>
<a <a
v-if="showToggle" v-if="showToggle"
......
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
href="#" href="#"
@click.prevent="toggleForm" @click.prevent="toggleForm"
> >
Edit {{ __('Edit') }}
</a> </a>
</div> </div>
<div class="value sidebar-item-value hide-collapsed"> <div class="value sidebar-item-value hide-collapsed">
......
import statusIcon from '../mr_widget_status_icon';
export default {
name: 'MRWidgetArchived',
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<div class="space-children">
<status-icon status="failed" />
<button
type="button"
class="btn btn-success btn-sm"
disabled="true">
Merge
</button>
</div>
<div class="media-body">
<span class="bold">
This project is archived, write access has been disabled
</span>
</div>
</div>
`,
};
<script>
import statusIcon from '../mr_widget_status_icon';
export default {
name: 'MRWidgetArchived',
components: {
statusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
<div class="space-children">
<status-icon
status="warning"
/>
<button
type="button"
class="btn btn-success btn-sm"
disabled="true"
>
{{ s__("mrWidget|Merge") }}
</button>
</div>
<div class="media-body">
<span class="bold">
{{ s__("mrWidget|This project is archived, write access has been disabled") }}
</span>
</div>
</div>
</template>
...@@ -21,7 +21,7 @@ export { default as FailedToMerge } from './components/states/mr_widget_failed_t ...@@ -21,7 +21,7 @@ export { default as FailedToMerge } from './components/states/mr_widget_failed_t
export { default as ClosedState } from './components/states/mr_widget_closed'; export { default as ClosedState } from './components/states/mr_widget_closed';
export { default as MergingState } from './components/states/mr_widget_merging'; export { default as MergingState } from './components/states/mr_widget_merging';
export { default as WipState } from './components/states/mr_widget_wip'; export { default as WipState } from './components/states/mr_widget_wip';
export { default as ArchivedState } from './components/states/mr_widget_archived'; export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
export { default as ConflictsState } from './components/states/mr_widget_conflicts'; export { default as ConflictsState } from './components/states/mr_widget_conflicts';
export { default as NothingToMergeState } from './components/states/mr_widget_nothing_to_merge'; export { default as NothingToMergeState } from './components/states/mr_widget_nothing_to_merge';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch'; export { default as MissingBranchState } from './components/states/mr_widget_missing_branch';
......
import Project from '~/project'; import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval'; import SmartInterval from '~/smart_interval';
import Flash from '../flash'; import Flash from '../flash';
import { import {
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
<template> <template>
<component <component
:is="rootElementType" :is="rootElementType"
class="text-center"> class="loading-container text-center">
<i <i
class="fa fa-spin fa-spinner" class="fa fa-spin fa-spinner"
:class="cssClass" :class="cssClass"
......
...@@ -174,12 +174,13 @@ ...@@ -174,12 +174,13 @@
&.user-authored { &.user-authored {
cursor: default; cursor: default;
opacity: 0.65; background-color: $gray-light;
border-color: $theme-gray-200;
color: $gl-text-color-disabled;
&:hover, gl-emoji {
&:active { opacity: 0.4;
background-color: $white-light; filter: grayscale(100%);
border-color: $border-color;
} }
} }
......
...@@ -220,14 +220,6 @@ ...@@ -220,14 +220,6 @@
@include btn-with-margin; @include btn-with-margin;
} }
&.disabled {
pointer-events: auto !important;
}
&[disabled] {
pointer-events: none !important;
}
.fa-caret-down, .fa-caret-down,
.fa-chevron-down { .fa-chevron-down {
margin-left: 5px; margin-left: 5px;
...@@ -450,3 +442,28 @@ ...@@ -450,3 +442,28 @@
.btn-svg svg { .btn-svg svg {
@include btn-svg; @include btn-svg;
} }
// All disabled buttons, regardless of color, type, etc
%disabled {
background-color: $gray-light !important;
border-color: $theme-gray-200 !important;
color: $gl-text-color-disabled !important;
opacity: 1 !important;
cursor: default !important;
i {
color: $gl-text-color-disabled !important;
}
}
.btn.disabled,
.btn[disabled],
fieldset[disabled] .btn,
.dropdown-toggle[disabled],
[disabled].dropdown-menu-toggle {
@extend %disabled;
&:hover {
@extend %disabled;
}
}
...@@ -63,11 +63,6 @@ ...@@ -63,11 +63,6 @@
border-radius: $border-radius-base; border-radius: $border-radius-base;
white-space: nowrap; white-space: nowrap;
&[disabled] {
opacity: .65;
cursor: not-allowed;
}
&.no-outline { &.no-outline {
outline: 0; outline: 0;
} }
......
...@@ -164,6 +164,7 @@ $gl-text-color-tertiary: #949494; ...@@ -164,6 +164,7 @@ $gl-text-color-tertiary: #949494;
$gl-text-color-quaternary: #d6d6d6; $gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1); $gl-text-color-inverted: rgba(255, 255, 255, 1);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85); $gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-color-disabled: #919191;
$gl-text-green: $green-600; $gl-text-green: $green-600;
$gl-text-green-hover: $green-700; $gl-text-green-hover: $green-700;
$gl-text-red: $red-500; $gl-text-red: $red-500;
...@@ -258,6 +259,8 @@ $general-hover-transition-duration: 100ms; ...@@ -258,6 +259,8 @@ $general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear; $general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232); $highlight-changes-color: rgb(235, 255, 232);
$performance-bar-height: 35px; $performance-bar-height: 35px;
$flash-height: 52px;
$context-header-height: 60px;
/* /*
* Common component specific colors * Common component specific colors
......
...@@ -391,11 +391,17 @@ ...@@ -391,11 +391,17 @@
.dropdown-toggle { .dropdown-toggle {
float: right; float: right;
.toggle-icon { i {
color: $white-light; color: $white-light;
padding-right: 2px; padding-right: 2px;
margin-top: 2px; margin-top: 2px;
} }
&[disabled] {
i {
color: $gl-text-color-disabled;
}
}
} }
.dropdown-menu { .dropdown-menu {
......
...@@ -228,7 +228,7 @@ ...@@ -228,7 +228,7 @@
.stage-cell { .stage-cell {
&.table-section { &.table-section {
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
min-width: 148px; min-width: 160px; /* Hack alert: Without this the mini graph pipeline won't work properly*/
margin-right: -4px; margin-right: -4px;
} }
} }
......
...@@ -107,6 +107,11 @@ table.table tr td.multi-file-table-name { ...@@ -107,6 +107,11 @@ table.table tr td.multi-file-table-name {
vertical-align: middle; vertical-align: middle;
margin-right: 2px; margin-right: 2px;
} }
.loading-container {
margin-right: 4px;
display: inline-block;
}
} }
.multi-file-table-col-commit-message { .multi-file-table-col-commit-message {
...@@ -247,7 +252,6 @@ table.table tr td.multi-file-table-name { ...@@ -247,7 +252,6 @@ table.table tr td.multi-file-table-name {
display: flex; display: flex;
position: relative; position: relative;
flex-direction: column; flex-direction: column;
height: 100%;
width: 290px; width: 290px;
padding: 0; padding: 0;
background-color: $gray-light; background-color: $gray-light;
...@@ -256,6 +260,11 @@ table.table tr td.multi-file-table-name { ...@@ -256,6 +260,11 @@ table.table tr td.multi-file-table-name {
.projects-sidebar { .projects-sidebar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.context-header {
width: auto;
margin-right: 0;
}
} }
.multi-file-commit-panel-inner { .multi-file-commit-panel-inner {
...@@ -496,19 +505,70 @@ table.table tr td.multi-file-table-name { ...@@ -496,19 +505,70 @@ table.table tr td.multi-file-table-name {
} }
} }
.ide-flash-container.flash-container { .ide.nav-only {
.flash-container {
margin-top: $header-height; margin-top: $header-height;
margin-bottom: 0; margin-bottom: 0;
}
.alert-wrapper .flash-container .flash-alert:last-child,
.alert-wrapper .flash-container .flash-notice:last-child {
margin-bottom: 0;
}
.content {
margin-top: $header-height;
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $context-header-height});
}
&.flash-shown {
.content {
margin-top: 0;
}
.ide-view {
height: calc(100vh - #{$header-height + $flash-height});
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $flash-height + $context-header-height});
}
}
} }
.with-performance-bar { .with-performance-bar .ide.nav-only {
.ide-flash-container.flash-container { .flash-container {
margin-top: $header-height + $performance-bar-height; margin-top: #{$header-height + $performance-bar-height};
}
.content {
margin-top: #{$header-height + $performance-bar-height};
} }
.ide-view { .ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height}); height: calc(100vh - #{$header-height + $performance-bar-height});
} }
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $performance-bar-height + 60});
}
&.flash-shown {
.content {
margin-top: 0;
}
.ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height + $context-header-height});
}
}
} }
......
...@@ -45,7 +45,7 @@ module IssuableActions ...@@ -45,7 +45,7 @@ module IssuableActions
} }
if issuable.edited? if issuable.edited?
response[:updated_at] = issuable.updated_at response[:updated_at] = issuable.last_edited_at.to_time.iso8601
response[:updated_by_name] = issuable.last_edited_by.name response[:updated_by_name] = issuable.last_edited_by.name
response[:updated_by_path] = user_path(issuable.last_edited_by) response[:updated_by_path] = user_path(issuable.last_edited_by)
end end
......
...@@ -71,7 +71,7 @@ class LabelsFinder < UnionFinder ...@@ -71,7 +71,7 @@ class LabelsFinder < UnionFinder
end end
def projects? def projects?
params[:project_ids].present? params[:project_ids]
end end
def only_group_labels? def only_group_labels?
......
...@@ -80,4 +80,20 @@ module EmailsHelper ...@@ -80,4 +80,20 @@ module EmailsHelper
'text-align:center' 'text-align:center'
].join(';') ].join(';')
end end
# "You are receiving this email because #{reason}"
def notification_reason_text(reason)
string = case reason
when NotificationReason::OWN_ACTIVITY
'of your activity'
when NotificationReason::ASSIGNED
'you have been assigned an item'
when NotificationReason::MENTIONED
'you have been mentioned'
else
'of your account'
end
"#{string} on #{Gitlab.config.gitlab.host}"
end
end end
...@@ -241,7 +241,7 @@ module IssuablesHelper ...@@ -241,7 +241,7 @@ module IssuablesHelper
return {} unless issuable.edited? return {} unless issuable.edited?
{ {
updatedAt: issuable.updated_at.to_time.iso8601, updatedAt: issuable.last_edited_at.to_time.iso8601,
updatedBy: { updatedBy: {
name: issuable.last_edited_by.name, name: issuable.last_edited_by.name,
path: user_path(issuable.last_edited_by) path: user_path(issuable.last_edited_by)
...@@ -304,6 +304,12 @@ module IssuablesHelper ...@@ -304,6 +304,12 @@ module IssuablesHelper
issuable.model_name.human.downcase issuable.model_name.human.downcase
end end
def selected_labels
Array(params[:label_name]).map do |label_name|
Label.new(title: label_name)
end
end
private private
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?
......
require 'webpack/rails/manifest' require 'webpack/rails/manifest'
module WebpackHelper module WebpackHelper
def webpack_bundle_tag(bundle) def webpack_bundle_tag(bundle, force_same_domain: false)
javascript_include_tag(*gitlab_webpack_asset_paths(bundle)) javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: true))
end end
# override webpack-rails gem helper until changes can make it upstream # override webpack-rails gem helper until changes can make it upstream
def gitlab_webpack_asset_paths(source, extension: nil) def gitlab_webpack_asset_paths(source, extension: nil, force_same_domain: false)
return "" unless source.present? return "" unless source.present?
paths = Webpack::Rails::Manifest.asset_paths(source) paths = Webpack::Rails::Manifest.asset_paths(source)
...@@ -14,10 +14,12 @@ module WebpackHelper ...@@ -14,10 +14,12 @@ module WebpackHelper
paths.select! { |p| p.ends_with? ".#{extension}" } paths.select! { |p| p.ends_with? ".#{extension}" }
end end
unless force_same_domain
force_host = webpack_public_host force_host = webpack_public_host
if force_host if force_host
paths.map! { |p| "#{force_host}#{p}" } paths.map! { |p| "#{force_host}#{p}" }
end end
end
paths paths
end end
......
module Emails module Emails
module Issues module Issues
def new_issue_email(recipient_id, issue_id) def new_issue_email(recipient_id, issue_id, reason = nil)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id)) mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason))
end end
def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id) def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end end
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_ids, updated_by_user_id) def reassigned_issue_email(recipient_id, issue_id, previous_assignee_ids, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
@previous_assignees = [] @previous_assignees = []
@previous_assignees = User.where(id: previous_assignee_ids) if previous_assignee_ids.any? @previous_assignees = User.where(id: previous_assignee_ids) if previous_assignee_ids.any?
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end end
def closed_issue_email(recipient_id, issue_id, updated_by_user_id) def closed_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
@updated_by = User.find(updated_by_user_id) @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end end
def relabeled_issue_email(recipient_id, issue_id, label_names, updated_by_user_id) def relabeled_issue_email(recipient_id, issue_id, label_names, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
@label_names = label_names @label_names = label_names
@labels_url = project_labels_url(@project) @labels_url = project_labels_url(@project)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end end
def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id) def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
@issue_status = status @issue_status = status
@updated_by = User.find(updated_by_user_id) @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end end
def issue_moved_email(recipient, issue, new_issue, updated_by_user) def issue_moved_email(recipient, issue, new_issue, updated_by_user, reason = nil)
setup_issue_mail(issue.id, recipient.id) setup_issue_mail(issue.id, recipient.id)
@new_issue = new_issue @new_issue = new_issue
@new_project = new_issue.project @new_project = new_issue.project
mail_answer_thread(issue, issue_thread_options(updated_by_user.id, recipient.id)) mail_answer_thread(issue, issue_thread_options(updated_by_user.id, recipient.id, reason))
end end
private private
...@@ -61,11 +61,12 @@ module Emails ...@@ -61,11 +61,12 @@ module Emails
@sent_notification = SentNotification.record(@issue, recipient_id, reply_key) @sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
end end
def issue_thread_options(sender_id, recipient_id) def issue_thread_options(sender_id, recipient_id, reason)
{ {
from: sender(sender_id), from: sender(sender_id),
to: recipient(recipient_id), to: recipient(recipient_id),
subject: subject("#{@issue.title} (##{@issue.iid})") subject: subject("#{@issue.title} (##{@issue.iid})"),
'X-GitLab-NotificationReason' => reason
} }
end end
end end
......
module Emails module Emails
module MergeRequests module MergeRequests
def new_merge_request_email(recipient_id, merge_request_id) def new_merge_request_email(recipient_id, merge_request_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id)) mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id, reason))
end end
def new_mention_in_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) def new_mention_in_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end end
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id) def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end end
def relabeled_merge_request_email(recipient_id, merge_request_id, label_names, updated_by_user_id) def relabeled_merge_request_email(recipient_id, merge_request_id, label_names, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@label_names = label_names @label_names = label_names
@labels_url = project_labels_url(@project) @labels_url = project_labels_url(@project)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end end
def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@updated_by = User.find(updated_by_user_id) @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end end
def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end end
def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id) def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@mr_status = status @mr_status = status
@updated_by = User.find(updated_by_user_id) @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end end
def resolved_all_discussions_email(recipient_id, merge_request_id, resolved_by_user_id) def resolved_all_discussions_email(recipient_id, merge_request_id, resolved_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@resolved_by = User.find(resolved_by_user_id) @resolved_by = User.find(resolved_by_user_id)
mail_answer_thread(@merge_request, merge_request_thread_options(resolved_by_user_id, recipient_id)) mail_answer_thread(@merge_request, merge_request_thread_options(resolved_by_user_id, recipient_id, reason))
end end
private private
...@@ -64,11 +64,12 @@ module Emails ...@@ -64,11 +64,12 @@ module Emails
@sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key) @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
end end
def merge_request_thread_options(sender_id, recipient_id) def merge_request_thread_options(sender_id, recipient_id, reason = nil)
{ {
from: sender(sender_id), from: sender(sender_id),
to: recipient(recipient_id), to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (#{@merge_request.to_reference})") subject: subject("#{@merge_request.title} (#{@merge_request.to_reference})"),
'X-GitLab-NotificationReason' => reason
} }
end end
end end
......
...@@ -112,6 +112,8 @@ class Notify < BaseMailer ...@@ -112,6 +112,8 @@ class Notify < BaseMailer
headers["X-GitLab-#{model.class.name}-ID"] = model.id headers["X-GitLab-#{model.class.name}-ID"] = model.id
headers['X-GitLab-Reply-Key'] = reply_key headers['X-GitLab-Reply-Key'] = reply_key
@reason = headers['X-GitLab-NotificationReason']
if Gitlab::IncomingEmail.enabled? && @sent_notification if Gitlab::IncomingEmail.enabled? && @sent_notification
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)) address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace address.display_name = @project.name_with_namespace
......
# Holds reasons for a notification to have been sent as well as a priority list to select which reason to use
# above the rest
class NotificationReason
OWN_ACTIVITY = 'own_activity'.freeze
ASSIGNED = 'assigned'.freeze
MENTIONED = 'mentioned'.freeze
# Priority list for selecting which reason to return in the notification
REASON_PRIORITY = [
OWN_ACTIVITY,
ASSIGNED,
MENTIONED
].freeze
# returns the priority of a reason as an integer
def self.priority(reason)
REASON_PRIORITY.index(reason) || REASON_PRIORITY.length + 1
end
end
class NotificationRecipient class NotificationRecipient
attr_reader :user, :type attr_reader :user, :type, :reason
def initialize( def initialize(user, type, **opts)
user, type,
custom_action: nil,
target: nil,
acting_user: nil,
project: nil,
group: nil,
skip_read_ability: false
)
unless NotificationSetting.levels.key?(type) || type == :subscription unless NotificationSetting.levels.key?(type) || type == :subscription
raise ArgumentError, "invalid type: #{type.inspect}" raise ArgumentError, "invalid type: #{type.inspect}"
end end
@custom_action = custom_action @custom_action = opts[:custom_action]
@acting_user = acting_user @acting_user = opts[:acting_user]
@target = target @target = opts[:target]
@project = project || default_project @project = opts[:project] || default_project
@group = group || @project&.group @group = opts[:group] || @project&.group
@user = user @user = user
@type = type @type = type
@skip_read_ability = skip_read_ability @reason = opts[:reason]
@skip_read_ability = opts[:skip_read_ability]
end end
def notification_setting def notification_setting
...@@ -77,9 +69,15 @@ class NotificationRecipient ...@@ -77,9 +69,15 @@ class NotificationRecipient
def own_activity? def own_activity?
return false unless @acting_user return false unless @acting_user
return false if @acting_user.notified_of_own_activity?
user == @acting_user if user == @acting_user
# if activity was generated by the same user, change reason to :own_activity
@reason = NotificationReason::OWN_ACTIVITY
# If the user wants to be notified, we must return `false`
!@acting_user.notified_of_own_activity?
else
false
end
end end
def has_access? def has_access?
......
...@@ -314,6 +314,7 @@ class Project < ActiveRecord::Base ...@@ -314,6 +314,7 @@ class Project < ActiveRecord::Base
scope :with_builds_enabled, -> { with_feature_enabled(:builds) } scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) } scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) } scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
......
...@@ -37,7 +37,7 @@ class ProjectStatistics < ActiveRecord::Base ...@@ -37,7 +37,7 @@ class ProjectStatistics < ActiveRecord::Base
def update_build_artifacts_size def update_build_artifacts_size
self.build_artifacts_size = self.build_artifacts_size =
project.builds.sum(:artifacts_size) + project.builds.sum(:artifacts_size) +
Ci::JobArtifact.artifacts_size_for(self) Ci::JobArtifact.artifacts_size_for(self.project)
end end
def update_storage_size def update_storage_size
......
class Route < ActiveRecord::Base class Route < ActiveRecord::Base
include CaseSensitivity
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
validates :source, presence: true validates :source, presence: true
...@@ -10,6 +12,7 @@ class Route < ActiveRecord::Base ...@@ -10,6 +12,7 @@ class Route < ActiveRecord::Base
validate :ensure_permanent_paths, if: :path_changed? validate :ensure_permanent_paths, if: :path_changed?
before_validation :delete_conflicting_orphaned_routes
after_create :delete_conflicting_redirects after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :path_changed? after_update :delete_conflicting_redirects, if: :path_changed?
after_update :create_redirect_for_old_path after_update :create_redirect_for_old_path
...@@ -78,4 +81,13 @@ class Route < ActiveRecord::Base ...@@ -78,4 +81,13 @@ class Route < ActiveRecord::Base
def conflicting_redirect_exists? def conflicting_redirect_exists?
RedirectRoute.permanent.matching_path_and_descendants(path).exists? RedirectRoute.permanent.matching_path_and_descendants(path).exists?
end end
def delete_conflicting_orphaned_routes
conflicting = self.class.iwhere(path: path)
conflicting_orphaned_routes = conflicting.select do |route|
route.source.nil?
end
conflicting_orphaned_routes.each(&:destroy)
end
end end
...@@ -11,11 +11,11 @@ module NotificationRecipientService ...@@ -11,11 +11,11 @@ module NotificationRecipientService
end end
def self.build_recipients(*a) def self.build_recipients(*a)
Builder::Default.new(*a).recipient_users Builder::Default.new(*a).notification_recipients
end end
def self.build_new_note_recipients(*a) def self.build_new_note_recipients(*a)
Builder::NewNote.new(*a).recipient_users Builder::NewNote.new(*a).notification_recipients
end end
module Builder module Builder
...@@ -49,25 +49,24 @@ module NotificationRecipientService ...@@ -49,25 +49,24 @@ module NotificationRecipientService
@recipients ||= [] @recipients ||= []
end end
def <<(pair) def add_recipients(users, type, reason)
users, type = pair
if users.is_a?(ActiveRecord::Relation) if users.is_a?(ActiveRecord::Relation)
users = users.includes(:notification_settings) users = users.includes(:notification_settings)
end end
users = Array(users) users = Array(users)
users.compact! users.compact!
recipients.concat(users.map { |u| make_recipient(u, type) }) recipients.concat(users.map { |u| make_recipient(u, type, reason) })
end end
def user_scope def user_scope
User.includes(:notification_settings) User.includes(:notification_settings)
end end
def make_recipient(user, type) def make_recipient(user, type, reason)
NotificationRecipient.new( NotificationRecipient.new(
user, type, user, type,
reason: reason,
project: project, project: project,
custom_action: custom_action, custom_action: custom_action,
target: target, target: target,
...@@ -75,14 +74,13 @@ module NotificationRecipientService ...@@ -75,14 +74,13 @@ module NotificationRecipientService
) )
end end
def recipient_users def notification_recipients
@recipient_users ||= @notification_recipients ||=
begin begin
build! build!
filter! filter!
users = recipients.map(&:user) recipients = self.recipients.sort_by { |r| NotificationReason.priority(r.reason) }.uniq(&:user)
users.uniq! recipients.freeze
users.freeze
end end
end end
...@@ -95,13 +93,13 @@ module NotificationRecipientService ...@@ -95,13 +93,13 @@ module NotificationRecipientService
def add_participants(user) def add_participants(user)
return unless target.respond_to?(:participants) return unless target.respond_to?(:participants)
self << [target.participants(user), :participating] add_recipients(target.participants(user), :participating, nil)
end end
def add_mentions(user, target:) def add_mentions(user, target:)
return unless target.respond_to?(:mentioned_users) return unless target.respond_to?(:mentioned_users)
self << [target.mentioned_users(user), :mention] add_recipients(target.mentioned_users(user), :mention, NotificationReason::MENTIONED)
end end
# Get project/group users with CUSTOM notification level # Get project/group users with CUSTOM notification level
...@@ -119,11 +117,11 @@ module NotificationRecipientService ...@@ -119,11 +117,11 @@ module NotificationRecipientService
global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global) global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global)
user_ids += user_ids_with_global_level_custom(global_users_ids, custom_action) user_ids += user_ids_with_global_level_custom(global_users_ids, custom_action)
self << [user_scope.where(id: user_ids), :watch] add_recipients(user_scope.where(id: user_ids), :watch, nil)
end end
def add_project_watchers def add_project_watchers
self << [project_watchers, :watch] add_recipients(project_watchers, :watch, nil)
end end
# Get project users with WATCH notification level # Get project users with WATCH notification level
...@@ -144,7 +142,7 @@ module NotificationRecipientService ...@@ -144,7 +142,7 @@ module NotificationRecipientService
def add_subscribed_users def add_subscribed_users
return unless target.respond_to? :subscribers return unless target.respond_to? :subscribers
self << [target.subscribers(project), :subscription] add_recipients(target.subscribers(project), :subscription, nil)
end end
def user_ids_notifiable_on(resource, notification_level = nil) def user_ids_notifiable_on(resource, notification_level = nil)
...@@ -195,7 +193,7 @@ module NotificationRecipientService ...@@ -195,7 +193,7 @@ module NotificationRecipientService
return unless target.respond_to? :labels return unless target.respond_to? :labels
(labels || target.labels).each do |label| (labels || target.labels).each do |label|
self << [label.subscribers(project), :subscription] add_recipients(label.subscribers(project), :subscription, nil)
end end
end end
end end
...@@ -222,12 +220,12 @@ module NotificationRecipientService ...@@ -222,12 +220,12 @@ module NotificationRecipientService
# Re-assign is considered as a mention of the new assignee # Re-assign is considered as a mention of the new assignee
case custom_action case custom_action
when :reassign_merge_request when :reassign_merge_request
self << [previous_assignee, :mention] add_recipients(previous_assignee, :mention, nil)
self << [target.assignee, :mention] add_recipients(target.assignee, :mention, NotificationReason::ASSIGNED)
when :reassign_issue when :reassign_issue
previous_assignees = Array(previous_assignee) previous_assignees = Array(previous_assignee)
self << [previous_assignees, :mention] add_recipients(previous_assignees, :mention, nil)
self << [target.assignees, :mention] add_recipients(target.assignees, :mention, NotificationReason::ASSIGNED)
end end
add_subscribed_users add_subscribed_users
...@@ -238,6 +236,12 @@ module NotificationRecipientService ...@@ -238,6 +236,12 @@ module NotificationRecipientService
# receive them, too. # receive them, too.
add_mentions(current_user, target: target) add_mentions(current_user, target: target)
# Add the assigned users, if any
assignees = custom_action == :new_issue ? target.assignees : target.assignee
# We use the `:participating` notification level in order to match existing legacy behavior as captured
# in existing specs (notification_service_spec.rb ~ line 507)
add_recipients(assignees, :participating, NotificationReason::ASSIGNED) if assignees
add_labels_subscribers add_labels_subscribers
end end
end end
......
...@@ -85,10 +85,11 @@ class NotificationService ...@@ -85,10 +85,11 @@ class NotificationService
recipients.each do |recipient| recipients.each do |recipient|
mailer.send( mailer.send(
:reassigned_issue_email, :reassigned_issue_email,
recipient.id, recipient.user.id,
issue.id, issue.id,
previous_assignee_ids, previous_assignee_ids,
current_user.id current_user.id,
recipient.reason
).deliver_later ).deliver_later
end end
end end
...@@ -176,7 +177,7 @@ class NotificationService ...@@ -176,7 +177,7 @@ class NotificationService
action: "resolve_all_discussions") action: "resolve_all_discussions")
recipients.each do |recipient| recipients.each do |recipient|
mailer.resolved_all_discussions_email(recipient.id, merge_request.id, current_user.id).deliver_later mailer.resolved_all_discussions_email(recipient.user.id, merge_request.id, current_user.id, recipient.reason).deliver_later
end end
end end
...@@ -199,7 +200,7 @@ class NotificationService ...@@ -199,7 +200,7 @@ class NotificationService
recipients = NotificationRecipientService.build_new_note_recipients(note) recipients = NotificationRecipientService.build_new_note_recipients(note)
recipients.each do |recipient| recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later mailer.send(notify_method, recipient.user.id, note.id).deliver_later
end end
end end
...@@ -299,7 +300,7 @@ class NotificationService ...@@ -299,7 +300,7 @@ class NotificationService
recipients = NotificationRecipientService.build_recipients(issue, current_user, action: 'moved') recipients = NotificationRecipientService.build_recipients(issue, current_user, action: 'moved')
recipients.map do |recipient| recipients.map do |recipient|
email = mailer.issue_moved_email(recipient, issue, new_issue, current_user) email = mailer.issue_moved_email(recipient.user, issue, new_issue, current_user, recipient.reason)
email.deliver_later email.deliver_later
email email
end end
...@@ -339,16 +340,16 @@ class NotificationService ...@@ -339,16 +340,16 @@ class NotificationService
recipients = NotificationRecipientService.build_recipients(target, target.author, action: "new") recipients = NotificationRecipientService.build_recipients(target, target.author, action: "new")
recipients.each do |recipient| recipients.each do |recipient|
mailer.send(method, recipient.id, target.id).deliver_later mailer.send(method, recipient.user.id, target.id, recipient.reason).deliver_later
end end
end end
def new_mentions_in_resource_email(target, new_mentioned_users, current_user, method) def new_mentions_in_resource_email(target, new_mentioned_users, current_user, method)
recipients = NotificationRecipientService.build_recipients(target, current_user, action: "new") recipients = NotificationRecipientService.build_recipients(target, current_user, action: "new")
recipients = recipients & new_mentioned_users recipients = recipients.select {|r| new_mentioned_users.include?(r.user) }
recipients.each do |recipient| recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, current_user.id).deliver_later mailer.send(method, recipient.user.id, target.id, current_user.id, recipient.reason).deliver_later
end end
end end
...@@ -363,7 +364,7 @@ class NotificationService ...@@ -363,7 +364,7 @@ class NotificationService
) )
recipients.each do |recipient| recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, current_user.id).deliver_later mailer.send(method, recipient.user.id, target.id, current_user.id, recipient.reason).deliver_later
end end
end end
...@@ -381,10 +382,11 @@ class NotificationService ...@@ -381,10 +382,11 @@ class NotificationService
recipients.each do |recipient| recipients.each do |recipient|
mailer.send( mailer.send(
method, method,
recipient.id, recipient.user.id,
target.id, target.id,
previous_assignee_id, previous_assignee_id,
current_user.id current_user.id,
recipient.reason
).deliver_later ).deliver_later
end end
end end
...@@ -408,7 +410,7 @@ class NotificationService ...@@ -408,7 +410,7 @@ class NotificationService
recipients = NotificationRecipientService.build_recipients(target, current_user, action: "reopen") recipients = NotificationRecipientService.build_recipients(target, current_user, action: "reopen")
recipients.each do |recipient| recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later mailer.send(method, recipient.user.id, target.id, status, current_user.id, recipient.reason).deliver_later
end end
end end
......
- @body_class = 'ide'
- page_title 'IDE' - page_title 'IDE'
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'ide' = webpack_bundle_tag 'ide', force_same_domain: true
.ide-flash-container.flash-container
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} } #ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} }
.text-center .text-center
......
!!! 5 !!! 5
%html{ lang: I18n.locale, class: page_class } %html{ lang: I18n.locale, class: page_class }
= render "layouts/head" = render "layouts/head"
%body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page } } %body{ class: "#{user_application_theme} #{@body_class} nav-only", data: { page: body_data_page } }
= render 'peek/bar' = render 'peek/bar'
= render "layouts/header/default" = render "layouts/header/default"
= render 'shared/outdated_browser' = render 'shared/outdated_browser'
...@@ -10,4 +10,5 @@ ...@@ -10,4 +10,5 @@
= render "layouts/broadcast" = render "layouts/broadcast"
= yield :flash_message = yield :flash_message
= render "layouts/flash" = render "layouts/flash"
.content{ id: "content-body" }
= yield = yield
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
#{link_to "View it on GitLab", @target_url}. #{link_to "View it on GitLab", @target_url}.
%br %br
-# Don't link the host in the line below, one link in the email is easier to quickly click than two. -# Don't link the host in the line below, one link in the email is easier to quickly click than two.
You're receiving this email because of your account on #{Gitlab.config.gitlab.host}. You're receiving this email because #{notification_reason_text(@reason)}.
If you'd like to receive fewer emails, you can If you'd like to receive fewer emails, you can
- if @labels_url - if @labels_url
adjust your #{link_to 'label subscriptions', @labels_url}. adjust your #{link_to 'label subscriptions', @labels_url}.
......
...@@ -9,4 +9,4 @@ ...@@ -9,4 +9,4 @@
<% end -%> <% end -%>
<% end -%> <% end -%>
You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>. <%= "You're receiving this email because #{notification_reason_text(@reason)}." %>
...@@ -4,8 +4,6 @@ ...@@ -4,8 +4,6 @@
- page_title 'New Project' - page_title 'New Project'
- header_title "Projects", dashboard_projects_path - header_title "Projects", dashboard_projects_path
- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility - visibility_level = params.dig(:project, :visibility_level) || default_project_visibility
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'project_new'
.project-edit-container .project-edit-container
.project-edit-errors .project-edit-errors
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
= render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true
.filter-item.inline.labels-filter .filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" } = render "shared/issuable/label_dropdown", selected: selected_labels, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
- if issuable_filter_present? - if issuable_filter_present?
.filter-item.inline.reset-filters .filter-item.inline.reset-filters
......
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
Milestone Milestone
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable - if can_edit_issuable
= link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed .value.hide-collapsed
- if issuable.milestone - if issuable.milestone
= link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_title(issuable.milestone), data: { container: "body", html: 1 } = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_title(issuable.milestone), data: { container: "body", html: 1 }
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
Due date Due date
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed .value.hide-collapsed
%span.value-content %span.value-content
- if issuable.due_date - if issuable.due_date
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
Labels Labels
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable - if can_edit_issuable
= link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) } .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any? - if selected_labels.any?
- selected_labels.each do |label| - selected_labels.each do |label|
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
Assignee Assignee
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable - if can_edit_issuable
= link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
- if !signed_in - if !signed_in
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" } %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
= sidebar_gutter_toggle_icon = sidebar_gutter_toggle_icon
......
---
title: Set standard disabled state for all buttons
merge_request:
author:
type: other
---
title: Fix the Projects API with_issues_enabled filter behaving incorrectly
any user
merge_request: 12724
author: Jan Christophersen
type: fixed
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