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.
- Add Gitaly metrics to the performance bar.
## 10.1.7 (2018-01-18)
- No changes.
## 10.1.6 (2018-01-11)
### Security (8 changes, 1 of them is from the community)
......
......@@ -341,10 +341,10 @@ group :development, :test do
gem 'spring-commands-rspec', '~> 1.0.4'
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
gem 'rubocop', '~> 0.52.0'
gem 'rubocop-rspec', '~> 1.20.1'
gem 'rubocop', '~> 0.52.1'
gem 'rubocop-rspec', '~> 1.22.1'
gem 'scss_lint', '~> 0.56.0', require: false
gem 'haml_lint', '~> 0.26.0', require: false
......
......@@ -304,7 +304,7 @@ GEM
mime-types (>= 1.16)
posix-spawn (~> 0.3)
gitlab-markup (1.6.3)
gitlab-styles (2.2.0)
gitlab-styles (2.3.0)
rubocop (~> 0.51)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19)
......@@ -583,7 +583,7 @@ GEM
rubypants (~> 0.2)
orm_adapter (0.5.0)
os (0.9.6)
parallel (1.12.0)
parallel (1.12.1)
parser (2.4.0.2)
ast (~> 2.3)
parslet (1.5.0)
......@@ -786,7 +786,7 @@ GEM
pg
rails
sqlite3
rubocop (0.52.0)
rubocop (0.52.1)
parallel (~> 1.10)
parser (>= 2.4.0.2, < 3.0)
powerpack (~> 0.1)
......@@ -795,8 +795,8 @@ GEM
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-gitlab-security (0.1.1)
rubocop (>= 0.51)
rubocop-rspec (1.20.1)
rubocop (>= 0.51.0)
rubocop-rspec (1.22.1)
rubocop (>= 0.52.1)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-prof (0.16.2)
......@@ -1060,7 +1060,7 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.2.0)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4)
......@@ -1162,8 +1162,8 @@ DEPENDENCIES
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
rubocop (~> 0.52.0)
rubocop-rspec (~> 1.20.1)
rubocop (~> 0.52.1)
rubocop-rspec (~> 1.22.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
ruby_parser (~> 3.8)
......
......@@ -218,6 +218,7 @@ const Api = {
(jqXHR, textStatus, errorThrown) => {
const error = new Error(`${options.url}: ${errorThrown}`);
error.textStatus = textStatus;
if (jqXHR && jqXHR.responseJSON) error.responseJSON = jqXHR.responseJSON;
reject(error);
},
);
......
......@@ -65,7 +65,17 @@ export default class CreateItemDropdown {
getData(term, callback) {
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) => {
flashEl.addEventListener('transitionend', () => {
flashEl.remove();
if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown');
}, {
once: true,
passive: true,
......@@ -64,6 +65,7 @@ const createFlash = function createFlash(
parent = document,
actionConfig = null,
fadeTransition = true,
addBodyClass = false,
) {
const flashContainer = parent.querySelector('.flash-container');
......@@ -86,6 +88,8 @@ const createFlash = function createFlash(
flashContainer.style.display = 'block';
if (addBodyClass) document.body.classList.add('flash-shown');
return flashContainer;
};
......
......@@ -68,12 +68,8 @@ export default {
this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => {
this.submitCommitsLoading = false;
this.$store.dispatch('getTreeData', {
projectId: this.currentProjectId,
branch: this.currentBranchId,
endpoint: `/tree/${this.currentBranchId}`,
force: true,
});
this.commitMessage = '';
this.startNewMR = false;
})
.catch(() => {
this.submitCommitsLoading = false;
......@@ -153,6 +149,7 @@ you started editing. Would you like to create a new branch?`)"
type="submit"
:disabled="commitButtonDisabled"
class="btn btn-default btn-sm append-right-10 prepend-left-10"
:class="{ disabled: submitCommitsLoading }"
>
<i
v-if="submitCommitsLoading"
......
......@@ -70,7 +70,10 @@ export default {
this.editor.createInstance(this.$refs.editor);
})
.then(() => this.setupEditor())
.catch(() => flash('Error setting up monaco. Please try again.'));
.catch((err) => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err;
});
},
setupEditor() {
if (!this.activeFile) return;
......
......@@ -35,9 +35,12 @@
return this.file.type === 'tree';
},
levelIndentation() {
return {
marginLeft: `${this.file.level * 16}px`,
};
if (this.file.level > 0) {
return {
marginLeft: `${this.file.level * 16}px`,
};
}
return {};
},
shortId() {
return this.file.id.substr(0, 8);
......@@ -111,7 +114,7 @@
/>
<i
class="fa"
v-if="changedClass"
v-if="file.changed || file.tempFile"
:class="changedClass"
aria-hidden="true"
>
......
......@@ -84,13 +84,13 @@ router.beforeEach((to, from, next) => {
}
})
.catch((e) => {
flash('Error while loading the branch files. Please try again.');
flash('Error while loading the branch files. Please try again.', 'alert', document, null, false, true);
throw e;
});
}
})
.catch((e) => {
flash('Error while loading the project data. Please try again.');
flash('Error while loading the project data. Please try again.', 'alert', document, null, false, true);
throw e;
});
}
......
......@@ -55,7 +55,7 @@ export default class Editor {
attachModel(model) {
this.instance.setModel(model.getModel());
this.dirtyDiffController.attachModel(model);
if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
this.currentModel = model;
......@@ -68,7 +68,7 @@ export default class Editor {
return acc;
}, {}));
this.dirtyDiffController.reDecorate(model);
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
}
clearEditor() {
......
......@@ -3,6 +3,7 @@ import { visitUrl } from '../../lib/utils/url_utility';
import flash from '../../flash';
import service from '../services';
import * as types from './mutation_types';
import { stripHtml } from '../../lib/utils/text_utility';
export const redirectToUrl = (_, url) => visitUrl(url);
......@@ -81,7 +82,7 @@ export const checkCommitStatus = ({ state }) =>
return false;
})
.catch(() => flash('Error checking branch data. Please try again.'));
.catch(() => flash('Error checking branch data. Please try again.', 'alert', document, null, false, true));
export const commitChanges = (
{ commit, state, dispatch, getters },
......@@ -92,7 +93,7 @@ export const commitChanges = (
.then((data) => {
const { branch } = payload;
if (!data.short_id) {
flash(data.message);
flash(data.message, 'alert', document, null, false, true);
return;
}
......@@ -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(
`Your changes have been committed. Commit ${data.short_id} with ${
data.stats.additions
} additions, ${data.stats.deletions} deletions.`,
commitMsg,
'notice',
);
document,
null,
false,
true);
window.dispatchEvent(new Event('resize'));
if (newMr) {
dispatch('discardAllChanges');
dispatch(
'redirectToUrl',
`${
selectedProject.web_url
}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
`${selectedProject.web_url}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
);
} else {
commit(types.SET_BRANCH_WORKING_REFERENCE, {
......@@ -134,12 +141,18 @@ export const commitChanges = (
});
dispatch('discardAllChanges');
dispatch('closeAllFiles');
window.scrollTo(0, 0);
}
})
.catch(() => flash('Error committing changes. Please try again.'));
.catch((err) => {
let errMsg = 'Error committing changes. Please try again.';
if (err.responseJSON && err.responseJSON.message) {
errMsg += ` (${stripHtml(err.responseJSON.message)})`;
}
flash(errMsg, 'alert', document, null, false, true);
window.dispatchEvent(new Event('resize'));
});
export const createTempEntry = (
{ state, dispatch },
......
......@@ -17,7 +17,7 @@ export const getBranchData = (
resolve(data);
})
.catch(() => {
flash('Error loading branch data. Please try again.');
flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
......
......@@ -69,7 +69,7 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
})
.catch(() => {
commit(types.TOGGLE_LOADING, file);
flash('Error loading file data. Please try again.');
flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
});
};
......@@ -77,22 +77,28 @@ export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFile
.then((raw) => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
})
.catch(() => flash('Error loading file content. Please try again.'));
.catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true));
export const changeFileContent = ({ commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content });
};
export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
if (state.selectedFile) {
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
}
};
export const setFileEOL = ({ state, commit }, { eol }) => {
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
if (state.selectedFile) {
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
}
};
export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
if (state.selectedFile) {
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
}
};
export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
......@@ -112,7 +118,7 @@ export const createTempFile = ({ state, commit, dispatch }, { projectId, branchI
url: newUrl,
});
if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`, 'alert', document, null, false, true);
commit(types.CREATE_TMP_FILE, {
parent,
......
......@@ -18,7 +18,7 @@ export const getProjectData = (
resolve(data);
})
.catch(() => {
flash('Error loading project data. Please try again.');
flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
});
} else {
......
......@@ -52,7 +52,7 @@ export const getTreeData = (
resolve(data);
})
.catch((e) => {
flash('Error loading tree data. Please try again.');
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
if (tree) commit(types.TOGGLE_LOADING, tree);
reject(e);
});
......@@ -151,7 +151,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
dispatch('getLastCommitData', tree);
})
.catch(() => flash('Error fetching log data.'));
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
};
export const updateDirectoryData = (
......
......@@ -64,7 +64,7 @@ export default {
},
[types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, {
content: '',
content: file.raw,
changed: false,
});
},
......
......@@ -152,6 +152,13 @@
hasUpdated() {
return !!this.state.updatedAt;
},
issueChanged() {
const descriptionChanged =
this.initialDescriptionText !== this.store.formState.description;
const titleChanged =
this.initialTitleText !== this.store.formState.title;
return descriptionChanged || titleChanged;
},
},
created() {
this.service = new Service(this.endpoint);
......@@ -176,6 +183,8 @@
}
});
window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm);
......@@ -186,8 +195,17 @@
eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm);
window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent);
},
methods: {
handleBeforeUnloadEvent(e) {
const event = e;
if (this.showForm && this.issueChanged) {
event.returnValue = 'Are you sure you want to lose your issue information?';
}
return undefined;
},
openForm() {
if (!this.showForm) {
this.showForm = true;
......
......@@ -72,4 +72,4 @@ export function capitalizeFirstCharacter(text) {
* @param {*} replace
* @returns {String}
*/
export const stripeHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
......@@ -66,9 +66,7 @@
<template>
<div class="note-header-info">
<a :href="author.path">
<span class="note-header-author-name">
{{ author.name }}
</span>
<span class="note-header-author-name">{{ author.name }}</span>
<span class="note-headline-light">
@{{ author.username }}
</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 setupProjectEdit from '~/project_edit';
import ProjectNew from '../shared/project_new';
import projectAvatar from '../shared/project_avatar';
import initProjectPermissionsSettings from '../shared/permissions';
export default () => {
new ProjectNew(); // eslint-disable-line no-new
setupProjectEdit();
// Initialize expandable settings panels
initSettingsPanels();
projectAvatar();
initProjectPermissionsSettings();
};
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 */
import IssuableIndex from '~/issuable_index';
import ShortcutsNavigation from '~/shortcuts_navigation';
import UsersSelect from '~/users_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
export default () => {
const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search');
if (filteredSearchEnabled) {
const filteredSearchManager = new gl.FilteredSearchManager('issues');
filteredSearchManager.setup();
}
new IssuableIndex('issue_');
initFilteredSearch(FILTERED_SEARCH.ISSUES);
new IssuableIndex(ISSUABLE_INDEX.ISSUE);
new ShortcutsNavigation();
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 ShortcutsNavigation from '~/shortcuts_navigation';
import UsersSelect from '~/users_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
export default () => {
const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search');
if (filteredSearchEnabled) {
const filteredSearchManager = new gl.FilteredSearchManager('merge_requests');
filteredSearchManager.setup();
}
new IssuableIndex('merge_request_'); // eslint-disable-line no-new
initFilteredSearch(FILTERED_SEARCH.MERGE_REQUESTS);
new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
};
/* 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 */
import Cookies from 'js-cookie';
import { visitUrl } from './lib/utils/url_utility';
import projectSelect from './project_select';
import { visitUrl } from '../../lib/utils/url_utility';
import projectSelect from '../../project_select';
export default class Project {
constructor() {
......
<script>
import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue';
import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue';
export default {
components: {
......
<script>
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 { visibilityOptions, visibilityLevelDescriptions } from '../constants';
import { toggleHiddenClassBySelector } from '../external';
......
/* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/
import VisibilitySelect from './visibility_select';
import VisibilitySelect from '../../../visibility_select';
function highlightChanges($elm) {
$elm.addClass('highlight-changes');
......
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 { slugify } from './lib/utils/text_utility';
import bp from '../../../breakpoints';
import { slugify } from '../../../lib/utils/text_utility';
export default class Wikis {
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 @@
Pipeline
</div>
<div
class="table-section section-25 js-pipeline-commit pipeline-commit"
class="table-section section-20 js-pipeline-commit pipeline-commit"
role="rowheader"
>
Commit
</div>
<div
class="table-section section-15 js-pipeline-stages pipeline-stages"
class="table-section section-20 js-pipeline-stages pipeline-stages"
role="rowheader"
>
Stages
......
......@@ -239,7 +239,7 @@
:auto-devops-help-path="autoDevopsHelpPath"
/>
<div class="table-section section-25">
<div class="table-section section-20">
<div
class="table-mobile-header"
role="rowheader">
......@@ -258,7 +258,7 @@
</div>
</div>
<div class="table-section section-wrap section-15 stage-cell">
<div class="table-section section-wrap section-20 stage-cell">
<div
class="table-mobile-header"
role="rowheader">
......
......@@ -100,8 +100,6 @@ const bindEvents = () => {
$projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl));
};
document.addEventListener('DOMContentLoaded', bindEvents);
export default {
bindEvents,
deriveProjectPathFromUrl,
......
......@@ -13,12 +13,10 @@ Mousetrap.stopCallback = (e, element, combo) => {
};
export default class Shortcuts {
constructor(skipResetBindings) {
constructor() {
this.onToggleHelp = this.onToggleHelp.bind(this);
this.enabledHelp = [];
if (!skipResetBindings) {
Mousetrap.reset();
}
Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch);
Mousetrap.bind('f', this.focusFilter.bind(this));
......
import Mousetrap from 'mousetrap';
import _ from 'underscore';
import Sidebar from './right_sidebar';
import ShortcutsNavigation from './shortcuts_navigation';
import Shortcuts from './shortcuts';
import { CopyAsGFM } from './behaviors/copy_as_gfm';
export default class ShortcutsIssuable extends ShortcutsNavigation {
export default class ShortcutsIssuable extends Shortcuts {
constructor(isMergeRequest) {
super();
......
......@@ -39,7 +39,7 @@ export default {
class="js-sidebar-dropdown-toggle edit-link pull-right"
href="#"
>
Edit
{{ __('Edit') }}
</a>
<a
v-if="showToggle"
......
......@@ -65,7 +65,7 @@
href="#"
@click.prevent="toggleForm"
>
Edit
{{ __('Edit') }}
</a>
</div>
<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
export { default as ClosedState } from './components/states/mr_widget_closed';
export { default as MergingState } from './components/states/mr_widget_merging';
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 NothingToMergeState } from './components/states/mr_widget_nothing_to_merge';
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 Flash from '../flash';
import {
......
......@@ -33,7 +33,7 @@
<template>
<component
:is="rootElementType"
class="text-center">
class="loading-container text-center">
<i
class="fa fa-spin fa-spinner"
:class="cssClass"
......
......@@ -174,12 +174,13 @@
&.user-authored {
cursor: default;
opacity: 0.65;
background-color: $gray-light;
border-color: $theme-gray-200;
color: $gl-text-color-disabled;
&:hover,
&:active {
background-color: $white-light;
border-color: $border-color;
gl-emoji {
opacity: 0.4;
filter: grayscale(100%);
}
}
......
......@@ -220,14 +220,6 @@
@include btn-with-margin;
}
&.disabled {
pointer-events: auto !important;
}
&[disabled] {
pointer-events: none !important;
}
.fa-caret-down,
.fa-chevron-down {
margin-left: 5px;
......@@ -450,3 +442,28 @@
.btn-svg 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 @@
border-radius: $border-radius-base;
white-space: nowrap;
&[disabled] {
opacity: .65;
cursor: not-allowed;
}
&.no-outline {
outline: 0;
}
......
......@@ -164,6 +164,7 @@ $gl-text-color-tertiary: #949494;
$gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-color-disabled: #919191;
$gl-text-green: $green-600;
$gl-text-green-hover: $green-700;
$gl-text-red: $red-500;
......@@ -258,6 +259,8 @@ $general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232);
$performance-bar-height: 35px;
$flash-height: 52px;
$context-header-height: 60px;
/*
* Common component specific colors
......
......@@ -391,11 +391,17 @@
.dropdown-toggle {
float: right;
.toggle-icon {
i {
color: $white-light;
padding-right: 2px;
margin-top: 2px;
}
&[disabled] {
i {
color: $gl-text-color-disabled;
}
}
}
.dropdown-menu {
......
......@@ -228,7 +228,7 @@
.stage-cell {
&.table-section {
@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;
}
}
......
......@@ -107,6 +107,11 @@ table.table tr td.multi-file-table-name {
vertical-align: middle;
margin-right: 2px;
}
.loading-container {
margin-right: 4px;
display: inline-block;
}
}
.multi-file-table-col-commit-message {
......@@ -247,7 +252,6 @@ table.table tr td.multi-file-table-name {
display: flex;
position: relative;
flex-direction: column;
height: 100%;
width: 290px;
padding: 0;
background-color: $gray-light;
......@@ -256,6 +260,11 @@ table.table tr td.multi-file-table-name {
.projects-sidebar {
display: flex;
flex-direction: column;
.context-header {
width: auto;
margin-right: 0;
}
}
.multi-file-commit-panel-inner {
......@@ -496,19 +505,70 @@ table.table tr td.multi-file-table-name {
}
}
.ide-flash-container.flash-container {
margin-top: $header-height;
margin-bottom: 0;
.ide.nav-only {
.flash-container {
margin-top: $header-height;
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 {
.ide-flash-container.flash-container {
margin-top: $header-height + $performance-bar-height;
.with-performance-bar .ide.nav-only {
.flash-container {
margin-top: #{$header-height + $performance-bar-height};
}
.content {
margin-top: #{$header-height + $performance-bar-height};
}
.ide-view {
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
}
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_path] = user_path(issuable.last_edited_by)
end
......
......@@ -71,7 +71,7 @@ class LabelsFinder < UnionFinder
end
def projects?
params[:project_ids].present?
params[:project_ids]
end
def only_group_labels?
......
......@@ -80,4 +80,20 @@ module EmailsHelper
'text-align:center'
].join(';')
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
......@@ -241,7 +241,7 @@ module IssuablesHelper
return {} unless issuable.edited?
{
updatedAt: issuable.updated_at.to_time.iso8601,
updatedAt: issuable.last_edited_at.to_time.iso8601,
updatedBy: {
name: issuable.last_edited_by.name,
path: user_path(issuable.last_edited_by)
......@@ -304,6 +304,12 @@ module IssuablesHelper
issuable.model_name.human.downcase
end
def selected_labels
Array(params[:label_name]).map do |label_name|
Label.new(title: label_name)
end
end
private
def sidebar_gutter_collapsed?
......
require 'webpack/rails/manifest'
module WebpackHelper
def webpack_bundle_tag(bundle)
javascript_include_tag(*gitlab_webpack_asset_paths(bundle))
def webpack_bundle_tag(bundle, force_same_domain: false)
javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: true))
end
# 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?
paths = Webpack::Rails::Manifest.asset_paths(source)
......@@ -14,9 +14,11 @@ module WebpackHelper
paths.select! { |p| p.ends_with? ".#{extension}" }
end
force_host = webpack_public_host
if force_host
paths.map! { |p| "#{force_host}#{p}" }
unless force_same_domain
force_host = webpack_public_host
if force_host
paths.map! { |p| "#{force_host}#{p}" }
end
end
paths
......
module Emails
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)
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
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)
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
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)
@previous_assignees = []
@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
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)
@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
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)
@label_names = label_names
@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
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)
@issue_status = status
@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
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)
@new_issue = new_issue
@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
private
......@@ -61,11 +61,12 @@ module Emails
@sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
end
def issue_thread_options(sender_id, recipient_id)
def issue_thread_options(sender_id, recipient_id, reason)
{
from: sender(sender_id),
to: recipient(recipient_id),
subject: subject("#{@issue.title} (##{@issue.iid})")
subject: subject("#{@issue.title} (##{@issue.iid})"),
'X-GitLab-NotificationReason' => reason
}
end
end
......
module Emails
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)
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
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)
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
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)
@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
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)
@label_names = label_names
@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
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)
@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
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)
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
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)
@mr_status = status
@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
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)
@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
private
......@@ -64,11 +64,12 @@ module Emails
@sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
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),
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
......
......@@ -112,6 +112,8 @@ class Notify < BaseMailer
headers["X-GitLab-#{model.class.name}-ID"] = model.id
headers['X-GitLab-Reply-Key'] = reply_key
@reason = headers['X-GitLab-NotificationReason']
if Gitlab::IncomingEmail.enabled? && @sent_notification
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
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
attr_reader :user, :type
def initialize(
user, type,
custom_action: nil,
target: nil,
acting_user: nil,
project: nil,
group: nil,
skip_read_ability: false
)
attr_reader :user, :type, :reason
def initialize(user, type, **opts)
unless NotificationSetting.levels.key?(type) || type == :subscription
raise ArgumentError, "invalid type: #{type.inspect}"
end
@custom_action = custom_action
@acting_user = acting_user
@target = target
@project = project || default_project
@group = group || @project&.group
@custom_action = opts[:custom_action]
@acting_user = opts[:acting_user]
@target = opts[:target]
@project = opts[:project] || default_project
@group = opts[:group] || @project&.group
@user = user
@type = type
@skip_read_ability = skip_read_ability
@reason = opts[:reason]
@skip_read_ability = opts[:skip_read_ability]
end
def notification_setting
......@@ -77,9 +69,15 @@ class NotificationRecipient
def own_activity?
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
def has_access?
......
......@@ -314,6 +314,7 @@ class Project < ActiveRecord::Base
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
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) }
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
......
......@@ -37,7 +37,7 @@ class ProjectStatistics < ActiveRecord::Base
def update_build_artifacts_size
self.build_artifacts_size =
project.builds.sum(:artifacts_size) +
Ci::JobArtifact.artifacts_size_for(self)
Ci::JobArtifact.artifacts_size_for(self.project)
end
def update_storage_size
......
class Route < ActiveRecord::Base
include CaseSensitivity
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
validates :source, presence: true
......@@ -10,6 +12,7 @@ class Route < ActiveRecord::Base
validate :ensure_permanent_paths, if: :path_changed?
before_validation :delete_conflicting_orphaned_routes
after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :path_changed?
after_update :create_redirect_for_old_path
......@@ -78,4 +81,13 @@ class Route < ActiveRecord::Base
def conflicting_redirect_exists?
RedirectRoute.permanent.matching_path_and_descendants(path).exists?
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
......@@ -11,11 +11,11 @@ module NotificationRecipientService
end
def self.build_recipients(*a)
Builder::Default.new(*a).recipient_users
Builder::Default.new(*a).notification_recipients
end
def self.build_new_note_recipients(*a)
Builder::NewNote.new(*a).recipient_users
Builder::NewNote.new(*a).notification_recipients
end
module Builder
......@@ -49,25 +49,24 @@ module NotificationRecipientService
@recipients ||= []
end
def <<(pair)
users, type = pair
def add_recipients(users, type, reason)
if users.is_a?(ActiveRecord::Relation)
users = users.includes(:notification_settings)
end
users = Array(users)
users.compact!
recipients.concat(users.map { |u| make_recipient(u, type) })
recipients.concat(users.map { |u| make_recipient(u, type, reason) })
end
def user_scope
User.includes(:notification_settings)
end
def make_recipient(user, type)
def make_recipient(user, type, reason)
NotificationRecipient.new(
user, type,
reason: reason,
project: project,
custom_action: custom_action,
target: target,
......@@ -75,14 +74,13 @@ module NotificationRecipientService
)
end
def recipient_users
@recipient_users ||=
def notification_recipients
@notification_recipients ||=
begin
build!
filter!
users = recipients.map(&:user)
users.uniq!
users.freeze
recipients = self.recipients.sort_by { |r| NotificationReason.priority(r.reason) }.uniq(&:user)
recipients.freeze
end
end
......@@ -95,13 +93,13 @@ module NotificationRecipientService
def add_participants(user)
return unless target.respond_to?(:participants)
self << [target.participants(user), :participating]
add_recipients(target.participants(user), :participating, nil)
end
def add_mentions(user, target:)
return unless target.respond_to?(:mentioned_users)
self << [target.mentioned_users(user), :mention]
add_recipients(target.mentioned_users(user), :mention, NotificationReason::MENTIONED)
end
# Get project/group users with CUSTOM notification level
......@@ -119,11 +117,11 @@ module NotificationRecipientService
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)
self << [user_scope.where(id: user_ids), :watch]
add_recipients(user_scope.where(id: user_ids), :watch, nil)
end
def add_project_watchers
self << [project_watchers, :watch]
add_recipients(project_watchers, :watch, nil)
end
# Get project users with WATCH notification level
......@@ -144,7 +142,7 @@ module NotificationRecipientService
def add_subscribed_users
return unless target.respond_to? :subscribers
self << [target.subscribers(project), :subscription]
add_recipients(target.subscribers(project), :subscription, nil)
end
def user_ids_notifiable_on(resource, notification_level = nil)
......@@ -195,7 +193,7 @@ module NotificationRecipientService
return unless target.respond_to? :labels
(labels || target.labels).each do |label|
self << [label.subscribers(project), :subscription]
add_recipients(label.subscribers(project), :subscription, nil)
end
end
end
......@@ -222,12 +220,12 @@ module NotificationRecipientService
# Re-assign is considered as a mention of the new assignee
case custom_action
when :reassign_merge_request
self << [previous_assignee, :mention]
self << [target.assignee, :mention]
add_recipients(previous_assignee, :mention, nil)
add_recipients(target.assignee, :mention, NotificationReason::ASSIGNED)
when :reassign_issue
previous_assignees = Array(previous_assignee)
self << [previous_assignees, :mention]
self << [target.assignees, :mention]
add_recipients(previous_assignees, :mention, nil)
add_recipients(target.assignees, :mention, NotificationReason::ASSIGNED)
end
add_subscribed_users
......@@ -238,6 +236,12 @@ module NotificationRecipientService
# receive them, too.
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
end
end
......
......@@ -85,10 +85,11 @@ class NotificationService
recipients.each do |recipient|
mailer.send(
:reassigned_issue_email,
recipient.id,
recipient.user.id,
issue.id,
previous_assignee_ids,
current_user.id
current_user.id,
recipient.reason
).deliver_later
end
end
......@@ -176,7 +177,7 @@ class NotificationService
action: "resolve_all_discussions")
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
......@@ -199,7 +200,7 @@ class NotificationService
recipients = NotificationRecipientService.build_new_note_recipients(note)
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
......@@ -299,7 +300,7 @@ class NotificationService
recipients = NotificationRecipientService.build_recipients(issue, current_user, action: 'moved')
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
end
......@@ -339,16 +340,16 @@ class NotificationService
recipients = NotificationRecipientService.build_recipients(target, target.author, action: "new")
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
def new_mentions_in_resource_email(target, new_mentioned_users, current_user, method)
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|
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
......@@ -363,7 +364,7 @@ class NotificationService
)
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
......@@ -381,10 +382,11 @@ class NotificationService
recipients.each do |recipient|
mailer.send(
method,
recipient.id,
recipient.user.id,
target.id,
previous_assignee_id,
current_user.id
current_user.id,
recipient.reason
).deliver_later
end
end
......@@ -408,7 +410,7 @@ class NotificationService
recipients = NotificationRecipientService.build_recipients(target, current_user, action: "reopen")
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
......
- @body_class = 'ide'
- page_title 'IDE'
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'ide'
.ide-flash-container.flash-container
= webpack_bundle_tag 'ide', force_same_domain: true
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} }
.text-center
......
!!! 5
%html{ lang: I18n.locale, class: page_class }
= 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 "layouts/header/default"
= render 'shared/outdated_browser'
......@@ -10,4 +10,5 @@
= render "layouts/broadcast"
= yield :flash_message
= render "layouts/flash"
= yield
.content{ id: "content-body" }
= yield
......@@ -20,7 +20,7 @@
#{link_to "View it on GitLab", @target_url}.
%br
-# 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 @labels_url
adjust your #{link_to 'label subscriptions', @labels_url}.
......
......@@ -9,4 +9,4 @@
<% 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 @@
- page_title 'New Project'
- header_title "Projects", dashboard_projects_path
- 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-errors
......
......@@ -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
.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?
.filter-item.inline.reset-filters
......
......@@ -34,7 +34,7 @@
Milestone
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- 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
- 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 }
......@@ -60,7 +60,7 @@
Due date
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- 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
%span.value-content
- if issuable.due_date
......@@ -95,7 +95,7 @@
Labels
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- 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?) }
- if selected_labels.any?
- selected_labels.each do |label|
......
......@@ -13,7 +13,7 @@
Assignee
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- 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
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
= 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