Commit a90a22a7 authored by Jacob Schatz's avatar Jacob Schatz

Merge branch 'master' into 'boards-bundle-refactor'

# Conflicts:
#   config/webpack.config.js
parents 11aa990d 296a4e68
This diff is collapsed.
10.5.0-pre 10.6.0-pre
...@@ -9,7 +9,7 @@ const Api = { ...@@ -9,7 +9,7 @@ const Api = {
projectsPath: '/api/:version/projects.json', projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id', projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels', projectLabelsPath: '/:namespace_path/:project_path/labels',
groupLabelsPath: '/groups/:namespace_path/labels', groupLabelsPath: '/groups/:namespace_path/-/labels',
licensePath: '/api/:version/templates/licenses/:key', licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key', gitignorePath: '/api/:version/templates/gitignores/:key',
gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key', gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key',
...@@ -32,7 +32,7 @@ const Api = { ...@@ -32,7 +32,7 @@ const Api = {
}, },
// Return groups list. Filtered by query // Return groups list. Filtered by query
groups(query, options, callback) { groups(query, options, callback = $.noop) {
const url = Api.buildUrl(Api.groupsPath); const url = Api.buildUrl(Api.groupsPath);
return axios.get(url, { return axios.get(url, {
params: Object.assign({ params: Object.assign({
......
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
mixins: [ mixins: [
pipelinesMixin, pipelinesMixin,
], ],
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
> >
<div class="item-details"> <div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility --> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> <user-avatar-image :img-src="mergeRequest.author.avatarUrl" />
<h5 class="item-title merge-merquest-title"> <h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url"> <a :href="mergeRequest.url">
{{ mergeRequest.title }} {{ mergeRequest.title }}
......
import Vue from 'vue'; import Vue from 'vue';
import deployKeysApp from './components/app.vue'; import deployKeysApp from './components/app.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({ export default () => new Vue({
el: document.getElementById('js-deploy-keys'), el: document.getElementById('js-deploy-keys'),
components: { components: {
deployKeysApp, deployKeysApp,
...@@ -18,4 +18,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -18,4 +18,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
}, },
}); });
}, },
})); });
...@@ -71,7 +71,7 @@ export default () => { ...@@ -71,7 +71,7 @@ export default () => {
el: '#resolve-count-app', el: '#resolve-count-app',
components: { components: {
'resolve-count': ResolveCount 'resolve-count': ResolveCount
} },
}); });
$(window).trigger('resize.nav'); $(window).trigger('resize.nav');
......
...@@ -42,190 +42,34 @@ var Dispatcher; ...@@ -42,190 +42,34 @@ var Dispatcher;
}); });
}); });
switch (page) { const shortcutHandlerPages = [
case 'projects:merge_requests:index': 'projects:activity',
case 'projects:issues:index': 'projects:artifacts:browse',
case 'projects:issues:show': 'projects:artifacts:file',
case 'projects:issues:new': 'projects:blame:show',
case 'projects:issues:edit': 'projects:blob:show',
case 'projects:merge_requests:creations:new': 'projects:commit:show',
case 'projects:merge_requests:creations:diffs': 'projects:commits:show',
case 'projects:merge_requests:edit': 'projects:find_file:show',
case 'projects:merge_requests:show': 'projects:issues:edit',
case 'projects:commit:show': 'projects:issues:index',
case 'projects:activity': 'projects:issues:new',
case 'projects:commits:show': 'projects:issues:show',
case 'projects:show': 'projects:merge_requests:creations:diffs',
shortcut_handler = true; 'projects:merge_requests:creations:new',
break; 'projects:merge_requests:edit',
case 'groups:activity': 'projects:merge_requests:index',
import('./pages/groups/activity') 'projects:merge_requests:show',
.then(callDefault) 'projects:network:show',
.catch(fail); 'projects:show',
break; 'projects:tree:show',
case 'groups:show': 'groups:show',
shortcut_handler = true; ];
break;
case 'groups:group_members:index': if (shortcutHandlerPages.indexOf(page) !== -1) {
import('./pages/groups/group_members/index') shortcut_handler = true;
.then(callDefault)
.catch(fail);
break;
case 'projects:project_members:index':
import('./pages/projects/project_members')
.then(callDefault)
.catch(fail);
break;
case 'groups:create':
case 'groups:new':
import('./pages/groups/new')
.then(callDefault)
.catch(fail);
break;
case 'groups:edit':
import('./pages/groups/edit')
.then(callDefault)
.catch(fail);
break;
case 'admin:groups:create':
case 'admin:groups:new':
import('./pages/admin/groups/new')
.then(callDefault)
.catch(fail);
break;
case 'admin:groups:edit':
import('./pages/admin/groups/edit')
.then(callDefault)
.catch(fail);
break;
case 'projects:tree:show':
import('./pages/projects/tree/show')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:find_file:show':
import('./pages/projects/find_file/show')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:blob:show':
import('./pages/projects/blob/show')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:blame:show':
import('./pages/projects/blame/show')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'groups:labels:new':
import('./pages/groups/labels/new')
.then(callDefault)
.catch(fail);
break;
case 'groups:labels:edit':
import('./pages/groups/labels/edit')
.then(callDefault)
.catch(fail);
break;
case 'projects:labels:new':
import('./pages/projects/labels/new')
.then(callDefault)
.catch(fail);
break;
case 'projects:labels:edit':
import('./pages/projects/labels/edit')
.then(callDefault)
.catch(fail);
break;
case 'groups:labels:index':
import('./pages/groups/labels/index')
.then(callDefault)
.catch(fail);
break;
case 'projects:labels:index':
import('./pages/projects/labels/index')
.then(callDefault)
.catch(fail);
break;
case 'projects:network:show':
// Ensure we don't create a particular shortcut handler here. This is
// already created, where the network graph is created.
shortcut_handler = true;
break;
case 'projects:forks:new':
import('./pages/projects/forks/new')
.then(callDefault)
.catch(fail);
break;
case 'projects:artifacts:browse':
import('./pages/projects/artifacts/browse')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:artifacts:file':
import('./pages/projects/artifacts/file')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'search:show':
import('./pages/search/show')
.then(callDefault)
.catch(fail);
break;
case 'projects:settings:repository:show':
import('./pages/projects/settings/repository/show')
.then(callDefault)
.catch(fail);
break;
case 'projects:settings:ci_cd:show':
import('./pages/projects/settings/ci_cd/show')
.then(callDefault)
.catch(fail);
break;
case 'groups:settings:ci_cd:show':
import('./pages/groups/settings/ci_cd/show')
.then(callDefault)
.catch(fail);
break;
case 'ci:lints:create':
case 'ci:lints:show':
import('./pages/ci/lints')
.then(callDefault)
.catch(fail);
break;
case 'admin:conversational_development_index:show':
import('./pages/admin/conversational_development_index/show')
.then(callDefault)
.catch(fail);
break;
case 'import:fogbugz:new_user_map':
import('./pages/import/fogbugz/new_user_map')
.then(callDefault)
.catch(fail);
break;
case 'profiles:personal_access_tokens:index':
import('./pages/profiles/personal_access_tokens')
.then(callDefault)
.catch(fail);
break;
case 'admin:impersonation_tokens:index':
import('./pages/admin/impersonation_tokens')
.then(callDefault)
.catch(fail);
break;
case 'dashboard:groups:index':
import('./pages/dashboard/groups/index')
.then(callDefault)
.catch(fail);
break;
} }
switch (path[0]) { switch (path[0]) {
case 'admin': case 'admin':
switch (path[1]) { switch (path[1]) {
......
...@@ -14,7 +14,6 @@ export default class DropdownUser extends FilteredSearchDropdown { ...@@ -14,7 +14,6 @@ export default class DropdownUser extends FilteredSearchDropdown {
endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
searchKey: 'search', searchKey: 'search',
params: { params: {
per_page: 20,
active: true, active: true,
group_id: this.getGroupId(), group_id: this.getGroupId(),
project_id: this.getProjectId(), project_id: this.getProjectId(),
......
...@@ -111,6 +111,9 @@ export default class FilteredSearchDropdown { ...@@ -111,6 +111,9 @@ export default class FilteredSearchDropdown {
if (hook) { if (hook) {
const data = hook.list.data || []; const data = hook.list.data || [];
if (!data) return;
const results = data.map((o) => { const results = data.map((o) => {
const updated = o; const updated = o;
updated.droplab_hidden = false; updated.droplab_hidden = false;
......
...@@ -607,7 +607,20 @@ GitLabDropdown = (function() { ...@@ -607,7 +607,20 @@ GitLabDropdown = (function() {
}; };
GitLabDropdown.prototype.renderItem = function(data, group, index) { GitLabDropdown.prototype.renderItem = function(data, group, index) {
var field, fieldName, html, selected, text, url, value; var field, fieldName, html, selected, text, url, value, rowHidden;
if (!this.options.renderRow) {
value = this.options.id ? this.options.id(data) : data.id;
if (value) {
value = value.toString().replace(/'/g, '\\\'');
}
}
// Hide element
if (this.options.hideRow && this.options.hideRow(value)) {
rowHidden = true;
}
if (group == null) { if (group == null) {
group = false; group = false;
} }
...@@ -616,6 +629,7 @@ GitLabDropdown = (function() { ...@@ -616,6 +629,7 @@ GitLabDropdown = (function() {
index = false; index = false;
} }
html = document.createElement('li'); html = document.createElement('li');
if (data === 'divider' || data === 'separator') { if (data === 'divider' || data === 'separator') {
html.className = data; html.className = data;
return html; return html;
...@@ -631,11 +645,9 @@ GitLabDropdown = (function() { ...@@ -631,11 +645,9 @@ GitLabDropdown = (function() {
html = this.options.renderRow.call(this.options, data, this); html = this.options.renderRow.call(this.options, data, this);
} else { } else {
if (!selected) { if (!selected) {
value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName; fieldName = this.options.fieldName;
if (value) { if (value) {
value = value.toString().replace(/'/g, '\\\'');
field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`); field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`);
if (field.length) { if (field.length) {
selected = true; selected = true;
......
...@@ -30,11 +30,11 @@ ...@@ -30,11 +30,11 @@
default: 'bottom', default: 'bottom',
}, },
/** /**
* value could either be number or string * value could either be number or string
* as `memberCount` is always passed as string * as `memberCount` is always passed as string
* while `subgroupCount` & `projectCount` * while `subgroupCount` & `projectCount`
* are always number * are always number
*/ */
value: { value: {
type: [Number, String], type: [Number, String],
required: false, required: false,
......
...@@ -316,9 +316,9 @@ export default class LabelsSelect { ...@@ -316,9 +316,9 @@ export default class LabelsSelect {
}, },
multiSelect: $dropdown.hasClass('js-multiselect'), multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(options) { clicked: function (clickEvent) {
const { $el, e, isMarking } = options; const { $el, e, isMarking } = clickEvent;
const label = options.selectedObj; const label = clickEvent.selectedObj;
var isIssueIndex, isMRIndex, page, boardsModel; var isIssueIndex, isMRIndex, page, boardsModel;
var fadeOutLoader = () => { var fadeOutLoader = () => {
......
...@@ -418,6 +418,16 @@ export const convertObjectPropsToCamelCase = (obj = {}) => { ...@@ -418,6 +418,16 @@ export const convertObjectPropsToCamelCase = (obj = {}) => {
export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`; export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => {
// Click a .js-select-on-focus field, select the contents
// Prevent a mouseup event from deselecting the input
$(selector).on('focusin', function selectOnFocusCallback() {
$(this).select().one('mouseup', (e) => {
e.preventDefault();
});
});
};
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.utils = { window.gl.utils = {
...(window.gl.utils || {}), ...(window.gl.utils || {}),
......
...@@ -10,7 +10,7 @@ window.jQuery = jQuery; ...@@ -10,7 +10,7 @@ window.jQuery = jQuery;
window.$ = jQuery; window.$ = jQuery;
// lib/utils // lib/utils
import { handleLocationHash } from './lib/utils/common_utils'; import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime_utility'; import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility'; import { getLocationHash, visitUrl } from './lib/utils/url_utility';
...@@ -104,13 +104,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -104,13 +104,7 @@ document.addEventListener('DOMContentLoaded', () => {
return true; return true;
}); });
// Click a .js-select-on-focus field, select the contents addSelectOnFocusBehaviour('.js-select-on-focus');
// Prevent a mouseup event from deselecting the input
$('.js-select-on-focus').on('focusin', function selectOnFocusCallback() {
$(this).select().one('mouseup', (e) => {
e.preventDefault();
});
});
$('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() { $('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() {
$(this).tooltip('destroy') $(this).tooltip('destroy')
......
import UserCallout from '../../../../user_callout'; import UserCallout from '~/user_callout';
export default () => new UserCallout(); document.addEventListener('DOMContentLoaded', () => new UserCallout());
import groupAvatar from '../../../../group_avatar'; import groupAvatar from '~/group_avatar';
export default () => groupAvatar(); document.addEventListener('DOMContentLoaded', groupAvatar);
...@@ -2,8 +2,8 @@ import BindInOut from '../../../../behaviors/bind_in_out'; ...@@ -2,8 +2,8 @@ import BindInOut from '../../../../behaviors/bind_in_out';
import Group from '../../../../group'; import Group from '../../../../group';
import groupAvatar from '../../../../group_avatar'; import groupAvatar from '../../../../group_avatar';
export default () => { document.addEventListener('DOMContentLoaded', () => {
BindInOut.initAll(); BindInOut.initAll();
new Group(); // eslint-disable-line no-new new Group(); // eslint-disable-line no-new
groupAvatar(); groupAvatar();
}; });
import DueDateSelectors from '../../../due_date_select'; import DueDateSelectors from '~/due_date_select';
export default () => new DueDateSelectors(); document.addEventListener('DOMContentLoaded', () => new DueDateSelectors());
import CILintEditor from '../ci_lint_editor';
document.addEventListener('DOMContentLoaded', () => new CILintEditor());
import CILintEditor from './ci_lint_editor';
export default () => new CILintEditor();
import CILintEditor from '../ci_lint_editor';
document.addEventListener('DOMContentLoaded', () => new CILintEditor());
import initGroupsList from '~/groups'; import initGroupsList from '~/groups';
export default initGroupsList; document.addEventListener('DOMContentLoaded', initGroupsList);
import Activities from '~/activities'; import Activities from '~/activities';
export default () => new Activities(); document.addEventListener('DOMContentLoaded', () => new Activities());
import groupAvatar from '~/group_avatar'; import groupAvatar from '~/group_avatar';
import TransferDropdown from '~/groups/transfer_dropdown'; import TransferDropdown from '~/groups/transfer_dropdown';
export default () => { document.addEventListener('DOMContentLoaded', () => {
groupAvatar(); groupAvatar();
new TransferDropdown(); // eslint-disable-line no-new new TransferDropdown(); // eslint-disable-line no-new
}; });
...@@ -4,8 +4,8 @@ import memberExpirationDate from '~/member_expiration_date'; ...@@ -4,8 +4,8 @@ import memberExpirationDate from '~/member_expiration_date';
import Members from '~/members'; import Members from '~/members';
import UsersSelect from '~/users_select'; import UsersSelect from '~/users_select';
export default () => { document.addEventListener('DOMContentLoaded', () => {
memberExpirationDate(); memberExpirationDate();
new Members(); new Members();
new UsersSelect(); new UsersSelect();
}; });
import Labels from '~/labels'; import Labels from '~/labels';
export default () => new Labels(); document.addEventListener('DOMContentLoaded', () => new Labels());
import initLabels from '~/init_labels'; import initLabels from '~/init_labels';
export default initLabels; document.addEventListener('DOMContentLoaded', initLabels);
import Labels from '~/labels'; import Labels from '~/labels';
export default () => new Labels(); document.addEventListener('DOMContentLoaded', () => new Labels());
...@@ -2,8 +2,8 @@ import BindInOut from '~/behaviors/bind_in_out'; ...@@ -2,8 +2,8 @@ import BindInOut from '~/behaviors/bind_in_out';
import Group from '~/group'; import Group from '~/group';
import groupAvatar from '~/group_avatar'; import groupAvatar from '~/group_avatar';
export default () => { document.addEventListener('DOMContentLoaded', () => {
BindInOut.initAll(); BindInOut.initAll();
new Group(); // eslint-disable-line no-new new Group(); // eslint-disable-line no-new
groupAvatar(); groupAvatar();
}; });
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
export default () => { document.addEventListener('DOMContentLoaded', () => {
const variableListEl = document.querySelector('.js-ci-variable-list-section'); const variableListEl = document.querySelector('.js-ci-variable-list-section');
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new AjaxVariableList({ new AjaxVariableList({
...@@ -9,4 +9,4 @@ export default () => { ...@@ -9,4 +9,4 @@ export default () => {
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint, saveEndpoint: variableListEl.dataset.saveEndpoint,
}); });
}; });
import UsersSelect from '../../../../users_select'; import UsersSelect from '~/users_select';
export default () => new UsersSelect(); document.addEventListener('DOMContentLoaded', () => new UsersSelect());
import DueDateSelectors from '../../../due_date_select'; import DueDateSelectors from '~/due_date_select';
export default () => new DueDateSelectors(); document.addEventListener('DOMContentLoaded', () => new DueDateSelectors());
import BuildArtifacts from '~/build_artifacts'; import BuildArtifacts from '~/build_artifacts';
import ShortcutsNavigation from '~/shortcuts_navigation'; import ShortcutsNavigation from '~/shortcuts_navigation';
export default function () { document.addEventListener('DOMContentLoaded', () => {
new ShortcutsNavigation(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
new BuildArtifacts(); // eslint-disable-line no-new new BuildArtifacts(); // eslint-disable-line no-new
} });
import BlobViewer from '~/blob/viewer/index'; import BlobViewer from '~/blob/viewer/index';
import ShortcutsNavigation from '~/shortcuts_navigation'; import ShortcutsNavigation from '~/shortcuts_navigation';
export default function () { document.addEventListener('DOMContentLoaded', () => {
new ShortcutsNavigation(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new
} });
import initBlob from '~/pages/projects/init_blob'; import initBlob from '~/pages/projects/init_blob';
export default initBlob; document.addEventListener('DOMContentLoaded', initBlob);
import BlobViewer from '~/blob/viewer/index'; import BlobViewer from '~/blob/viewer/index';
import initBlob from '~/pages/projects/init_blob'; import initBlob from '~/pages/projects/init_blob';
export default () => { document.addEventListener('DOMContentLoaded', () => {
new BlobViewer(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new
initBlob(); initBlob();
}; });
import ProjectFindFile from '~/project_find_file'; import ProjectFindFile from '~/project_find_file';
import ShortcutsFindFile from '~/shortcuts_find_file'; import ShortcutsFindFile from '~/shortcuts_find_file';
export default () => { document.addEventListener('DOMContentLoaded', () => {
const findElement = document.querySelector('.js-file-finder'); const findElement = document.querySelector('.js-file-finder');
const projectFindFile = new ProjectFindFile($('.file-finder-holder'), { const projectFindFile = new ProjectFindFile($('.file-finder-holder'), {
url: findElement.dataset.fileFindUrl, url: findElement.dataset.fileFindUrl,
...@@ -9,4 +9,4 @@ export default () => { ...@@ -9,4 +9,4 @@ export default () => {
blobUrlTemplate: findElement.dataset.blobUrlTemplate, blobUrlTemplate: findElement.dataset.blobUrlTemplate,
}); });
new ShortcutsFindFile(projectFindFile); // eslint-disable-line no-new new ShortcutsFindFile(projectFindFile); // eslint-disable-line no-new
}; });
import ProjectFork from '~/project_fork'; import ProjectFork from '~/project_fork';
export default () => { document.addEventListener('DOMContentLoaded', () => new ProjectFork());
new ProjectFork(); // eslint-disable-line no-new
};
...@@ -3,6 +3,7 @@ import Issue from '~/issue'; ...@@ -3,6 +3,7 @@ import Issue from '~/issue';
import ShortcutsIssuable from '~/shortcuts_issuable'; import ShortcutsIssuable from '~/shortcuts_issuable';
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
import '~/notes/index'; import '~/notes/index';
import '~/issue_show/index';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new Issue(); // eslint-disable-line no-new new Issue(); // eslint-disable-line no-new
......
import Labels from '~/labels'; import Labels from '~/labels';
export default () => new Labels(); document.addEventListener('DOMContentLoaded', () => new Labels());
import initLabels from '~/init_labels'; import initLabels from '~/init_labels';
export default initLabels; document.addEventListener('DOMContentLoaded', initLabels);
import Labels from '~/labels'; import Labels from '~/labels';
export default () => new Labels(); document.addEventListener('DOMContentLoaded', () => new Labels());
...@@ -50,7 +50,7 @@ export default class Project { ...@@ -50,7 +50,7 @@ export default class Project {
Project.projectSelectDropdown(); Project.projectSelectDropdown();
} }
static projectSelectDropdown () { static projectSelectDropdown() {
projectSelect(); projectSelect();
$('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val())); $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val()));
} }
......
...@@ -3,10 +3,10 @@ import UsersSelect from '../../../users_select'; ...@@ -3,10 +3,10 @@ import UsersSelect from '../../../users_select';
import groupsSelect from '../../../groups_select'; import groupsSelect from '../../../groups_select';
import Members from '../../../members'; import Members from '../../../members';
export default () => { document.addEventListener('DOMContentLoaded', () => {
memberExpirationDate('.js-access-expiration-date-groups'); memberExpirationDate('.js-access-expiration-date-groups');
groupsSelect(); groupsSelect();
memberExpirationDate(); memberExpirationDate();
new Members(); // eslint-disable-line no-new new Members(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new
}; });
...@@ -2,7 +2,7 @@ import initSettingsPanels from '~/settings_panels'; ...@@ -2,7 +2,7 @@ import initSettingsPanels from '~/settings_panels';
import SecretValues from '~/behaviors/secret_values'; import SecretValues from '~/behaviors/secret_values';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
export default function () { document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
...@@ -22,4 +22,4 @@ export default function () { ...@@ -22,4 +22,4 @@ export default function () {
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint, saveEndpoint: variableListEl.dataset.saveEndpoint,
}); });
} });
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
import initDeployKeys from '~/deploy_keys';
export default initSettingsPanels; document.addEventListener('DOMContentLoaded', () => {
initDeployKeys();
initSettingsPanels();
});
...@@ -7,7 +7,7 @@ import BlobViewer from '../../../../blob/viewer'; ...@@ -7,7 +7,7 @@ import BlobViewer from '../../../../blob/viewer';
import NewCommitForm from '../../../../new_commit_form'; import NewCommitForm from '../../../../new_commit_form';
import { ajaxGet } from '../../../../lib/utils/common_utils'; import { ajaxGet } from '../../../../lib/utils/common_utils';
export default () => { document.addEventListener('DOMContentLoaded', () => {
new ShortcutsNavigation(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
new TreeView(); // eslint-disable-line no-new new TreeView(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new
...@@ -35,5 +35,4 @@ export default () => { ...@@ -35,5 +35,4 @@ export default () => {
}, },
}); });
} }
}; });
import Search from './search'; import Search from './search';
export default () => new Search(); document.addEventListener('DOMContentLoaded', () => new Search());
...@@ -31,10 +31,14 @@ ...@@ -31,10 +31,14 @@
type: String, type: String,
required: true, required: true,
}, },
id: { pipelineId: {
type: Number, type: Number,
required: true, required: true,
}, },
type: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -46,17 +50,27 @@ ...@@ -46,17 +50,27 @@
return `btn ${this.cssClass}`; return `btn ${this.cssClass}`;
}, },
}, },
created() {
// We're using eventHub to listen to the modal here instead of
// using props because it would would make the parent components
// much more complex to keep track of the loading state of each button
eventHub.$on('postAction', this.setLoading);
},
beforeDestroy() {
eventHub.$off('postAction', this.setLoading);
},
methods: { methods: {
onClick() { onClick() {
eventHub.$emit('actionConfirmationModal', { eventHub.$emit('openConfirmationModal', {
id: this.id, pipelineId: this.pipelineId,
callback: this.makeRequest, endpoint: this.endpoint,
type: this.type,
}); });
}, },
makeRequest() { setLoading(endpoint) {
this.isLoading = true; if (endpoint === this.endpoint) {
this.isLoading = true;
eventHub.$emit('postAction', this.endpoint); }
}, },
}, },
}; };
......
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
jobComponent, jobComponent,
dropdownJobComponent, dropdownJobComponent,
}, },
props: { props: {
title: { title: {
type: String, type: String,
......
<script> <script>
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
import pipelinesTableRowComponent from './pipelines_table_row.vue'; import pipelinesTableRowComponent from './pipelines_table_row.vue';
import stopConfirmationModal from './stop_confirmation_modal.vue'; import eventHub from '../event_hub';
import retryConfirmationModal from './retry_confirmation_modal.vue';
/** /**
* Pipelines Table Component. * Pipelines Table Component.
...@@ -11,8 +12,7 @@ ...@@ -11,8 +12,7 @@
export default { export default {
components: { components: {
pipelinesTableRowComponent, pipelinesTableRowComponent,
stopConfirmationModal, modal,
retryConfirmationModal,
}, },
props: { props: {
pipelines: { pipelines: {
...@@ -33,6 +33,52 @@ ...@@ -33,6 +33,52 @@
required: true, required: true,
}, },
}, },
data() {
return {
pipelineId: '',
endpoint: '',
type: '',
};
},
computed: {
modalTitle() {
return this.type === 'stop' ?
sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), {
pipelineId: `'${this.pipelineId}'`,
}, false) :
sprintf(s__('Pipeline|Retry pipeline #%{pipelineId}?'), {
pipelineId: `'${this.pipelineId}'`,
}, false);
},
modalText() {
return this.type === 'stop' ?
sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), {
pipelineId: `<strong>#${this.pipelineId}</strong>`,
}, false) :
sprintf(s__('Pipeline|You’re about to retry pipeline %{pipelineId}.'), {
pipelineId: `<strong>#${this.pipelineId}</strong>`,
}, false);
},
primaryButtonLabel() {
return this.type === 'stop' ? s__('Pipeline|Stop pipeline') : s__('Pipeline|Retry pipeline');
},
},
created() {
eventHub.$on('openConfirmationModal', this.setModalData);
},
beforeDestroy() {
eventHub.$off('openConfirmationModal', this.setModalData);
},
methods: {
setModalData(data) {
this.pipelineId = data.pipelineId;
this.endpoint = data.endpoint;
this.type = data.type;
},
onSubmit() {
eventHub.$emit('postAction', this.endpoint);
},
},
}; };
</script> </script>
<template> <template>
...@@ -74,7 +120,20 @@ ...@@ -74,7 +120,20 @@
:auto-devops-help-path="autoDevopsHelpPath" :auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType" :view-type="viewType"
/> />
<stop-confirmation-modal /> <modal
<retry-confirmation-modal /> id="confirmation-modal"
:title="modalTitle"
:text="modalText"
kind="danger"
:primary-button-label="primaryButtonLabel"
@submit="onSubmit"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
</template>
</modal>
</div> </div>
</template> </template>
...@@ -223,7 +223,8 @@ ...@@ -223,7 +223,8 @@
<div class="table-section section-10 commit-link"> <div class="table-section section-10 commit-link">
<div <div
class="table-mobile-header" class="table-mobile-header"
role="rowheader"> role="rowheader"
>
Status Status
</div> </div>
<div class="table-mobile-content"> <div class="table-mobile-content">
...@@ -305,9 +306,10 @@ ...@@ -305,9 +306,10 @@
css-class="js-pipelines-retry-button btn-default btn-retry" css-class="js-pipelines-retry-button btn-default btn-retry"
title="Retry" title="Retry"
icon="repeat" icon="repeat"
:id="pipeline.id" :pipeline-id="pipeline.id"
data-toggle="modal" data-toggle="modal"
data-target="#retry-confirmation-modal" data-target="#confirmation-modal"
type="retry"
/> />
<async-button-component <async-button-component
...@@ -316,9 +318,10 @@ ...@@ -316,9 +318,10 @@
css-class="js-pipelines-cancel-button btn-remove" css-class="js-pipelines-cancel-button btn-remove"
title="Cancel" title="Cancel"
icon="close" icon="close"
:id="pipeline.id" :pipeline-id="pipeline.id"
data-toggle="modal" data-toggle="modal"
data-target="#stop-confirmation-modal" data-target="#confirmation-modal"
type="stop"
/> />
</div> </div>
</div> </div>
......
<script>
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
modal,
},
data() {
return {
id: '',
callback: () => {},
};
},
computed: {
title() {
return sprintf(s__('Pipeline|Retry pipeline #%{id}?'), {
id: `'${this.id}'`,
}, false);
},
text() {
return sprintf(s__('Pipeline|You’re about to retry pipeline %{id}.'), {
id: `<strong>#${this.id}</strong>`,
}, false);
},
primaryButtonLabel() {
return s__('Pipeline|Retry pipeline');
},
},
created() {
eventHub.$on('actionConfirmationModal', this.updateModal);
},
beforeDestroy() {
eventHub.$off('actionConfirmationModal', this.updateModal);
},
methods: {
updateModal(action) {
this.id = action.id;
this.callback = action.callback;
},
onSubmit() {
this.callback();
},
},
};
</script>
<template>
<modal
id="retry-confirmation-modal"
:title="title"
:text="text"
kind="danger"
:primary-button-label="primaryButtonLabel"
@submit="onSubmit"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
</template>
</modal>
</template>
...@@ -50,9 +50,7 @@ ...@@ -50,9 +50,7 @@
computed: { computed: {
dropdownClass() { dropdownClass() {
return this.dropdownContent.length > 0 ? return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading';
'js-builds-dropdown-container' :
'js-builds-dropdown-loading';
}, },
triggerButtonClass() { triggerButtonClass() {
......
<script>
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
modal,
},
data() {
return {
id: '',
callback: () => {},
};
},
computed: {
title() {
return sprintf(s__('Pipeline|Stop pipeline #%{id}?'), {
id: `'${this.id}'`,
}, false);
},
text() {
return sprintf(s__('Pipeline|You’re about to stop pipeline %{id}.'), {
id: `<strong>#${this.id}</strong>`,
}, false);
},
primaryButtonLabel() {
return s__('Pipeline|Stop pipeline');
},
},
created() {
eventHub.$on('actionConfirmationModal', this.updateModal);
},
beforeDestroy() {
eventHub.$off('actionConfirmationModal', this.updateModal);
},
methods: {
updateModal(action) {
this.id = action.id;
this.callback = action.callback;
},
onSubmit() {
this.callback();
},
},
};
</script>
<template>
<modal
id="stop-confirmation-modal"
:title="title"
:text="text"
kind="danger"
:primary-button-label="primaryButtonLabel"
@submit="onSubmit"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
</template>
</modal>
</template>
...@@ -73,7 +73,7 @@ export default class ProjectFindFile { ...@@ -73,7 +73,7 @@ export default class ProjectFindFile {
// find file // find file
} }
// files pathes load // files pathes load
load(url) { load(url) {
axios.get(url) axios.get(url)
.then(({ data }) => { .then(({ data }) => {
...@@ -85,7 +85,7 @@ export default class ProjectFindFile { ...@@ -85,7 +85,7 @@ export default class ProjectFindFile {
.catch(() => flash(__('An error occurred while loading filenames'))); .catch(() => flash(__('An error occurred while loading filenames')));
} }
// render result // render result
renderList(filePaths, searchText) { renderList(filePaths, searchText) {
var blobItemUrl, filePath, html, i, j, len, matches, results; var blobItemUrl, filePath, html, i, j, len, matches, results;
this.element.find(".tree-table > tbody").empty(); this.element.find(".tree-table > tbody").empty();
......
import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils';
let hasUserDefinedProjectPath = false; let hasUserDefinedProjectPath = false;
const deriveProjectPathFromUrl = ($projectImportUrl) => { const deriveProjectPathFromUrl = ($projectImportUrl) => {
...@@ -36,6 +38,7 @@ const bindEvents = () => { ...@@ -36,6 +38,7 @@ const bindEvents = () => {
const $changeTemplateBtn = $('.change-template'); const $changeTemplateBtn = $('.change-template');
const $selectedIcon = $('.selected-icon svg'); const $selectedIcon = $('.selected-icon svg');
const $templateProjectNameInput = $('#template-project-name #project_path'); const $templateProjectNameInput = $('#template-project-name #project_path');
const $pushNewProjectTipTrigger = $('.push-new-project-tip');
if ($newProjectForm.length !== 1) { if ($newProjectForm.length !== 1) {
return; return;
...@@ -55,6 +58,34 @@ const bindEvents = () => { ...@@ -55,6 +58,34 @@ const bindEvents = () => {
$('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`); $('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`);
}); });
if ($pushNewProjectTipTrigger) {
$pushNewProjectTipTrigger
.removeAttr('rel')
.removeAttr('target')
.on('click', (e) => { e.preventDefault(); })
.popover({
title: $pushNewProjectTipTrigger.data('title'),
placement: 'auto bottom',
html: 'true',
content: $('.push-new-project-tip-template').html(),
})
.on('shown.bs.popover', () => {
$(document).on('click.popover touchstart.popover', (event) => {
if ($(event.target).closest('.popover').length === 0) {
$pushNewProjectTipTrigger.trigger('click');
}
});
const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find('.js-select-on-focus');
addSelectOnFocusBehaviour(target);
target.focus();
})
.on('hide.bs.popover', () => {
$(document).off('click.popover touchstart.popover');
});
}
function chooseTemplate() { function chooseTemplate() {
$('.template-option').hide(); $('.template-option').hide();
$projectFieldsForm.addClass('selected'); $projectFieldsForm.addClass('selected');
......
import renderMath from './render_math'; import renderMath from './render_math';
import renderMermaid from './render_mermaid'; import renderMermaid from './render_mermaid';
import syntaxHighlight from './syntax_highlight'; import syntaxHighlight from './syntax_highlight';
// Render Gitlab flavoured Markdown // Render Gitlab flavoured Markdown
// //
// Delegates to syntax highlight and render math & mermaid diagrams. // Delegates to syntax highlight and render math & mermaid diagrams.
......
<script> <script>
import Flash from '../../../flash'; import Flash from '~/flash';
import editForm from './edit_form.vue'; import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable'; import issuableMixin from '../../../vue_shared/mixins/issuable';
import Icon from '../../../vue_shared/components/icon.vue'; import Icon from '../../../vue_shared/components/icon.vue';
...@@ -53,8 +53,7 @@ ...@@ -53,8 +53,7 @@
discussion_locked: locked, discussion_locked: locked,
}) })
.then(() => location.reload()) .then(() => location.reload())
.catch(() => Flash(this.__(`Something went wrong trying to .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName}`)));
change the locked state of this ${this.issuableDisplayName}`)));
}, },
}, },
}; };
......
...@@ -39,7 +39,6 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -39,7 +39,6 @@ function UsersSelect(currentUser, els, options = {}) {
options.showCurrentUser = $dropdown.data('currentUser'); options.showCurrentUser = $dropdown.data('currentUser');
options.todoFilter = $dropdown.data('todoFilter'); options.todoFilter = $dropdown.data('todoFilter');
options.todoStateFilter = $dropdown.data('todoStateFilter'); options.todoStateFilter = $dropdown.data('todoStateFilter');
options.perPage = $dropdown.data('perPage');
showNullUser = $dropdown.data('nullUser'); showNullUser = $dropdown.data('nullUser');
defaultNullUser = $dropdown.data('nullUserDefault'); defaultNullUser = $dropdown.data('nullUserDefault');
showMenuAbove = $dropdown.data('showMenuAbove'); showMenuAbove = $dropdown.data('showMenuAbove');
...@@ -669,7 +668,6 @@ UsersSelect.prototype.users = function(query, options, callback) { ...@@ -669,7 +668,6 @@ UsersSelect.prototype.users = function(query, options, callback) {
const url = this.buildUrl(this.usersPath); const url = this.buildUrl(this.usersPath);
const params = { const params = {
search: query, search: query,
per_page: options.perPage || 20,
active: true, active: true,
project_id: options.projectId || null, project_id: options.projectId || null,
group_id: options.groupId || null, group_id: options.groupId || null,
......
...@@ -107,7 +107,8 @@ ...@@ -107,7 +107,8 @@
<template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest"> <template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest">
<div <div
class="accept-merge-holder clearfix class="accept-merge-holder clearfix
js-toggle-container accept-action media space-children"> js-toggle-container accept-action media space-children"
>
<button <button
type="button" type="button"
class="btn btn-sm btn-reopen btn-success" class="btn btn-sm btn-reopen btn-success"
......
...@@ -96,9 +96,7 @@ export default { ...@@ -96,9 +96,7 @@ export default {
cb.call(null, data); cb.call(null, data);
} }
}) })
.catch(() => { .catch(() => new Flash('Something went wrong. Please try again.'));
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
}, },
initPolling() { initPolling() {
this.pollingInterval = new SmartInterval({ this.pollingInterval = new SmartInterval({
...@@ -146,9 +144,7 @@ export default { ...@@ -146,9 +144,7 @@ export default {
Project.initRefSwitcher(); Project.initRefSwitcher();
} }
}) })
.catch(() => { .catch(() => new Flash('Something went wrong. Please try again.'));
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
}, },
handleNotification(data) { handleNotification(data) {
if (data.ci_status === this.mr.ciStatus) return; if (data.ci_status === this.mr.ciStatus) return;
......
...@@ -4,7 +4,6 @@ import { stateKey } from './state_maps'; ...@@ -4,7 +4,6 @@ import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility'; import { formatDate } from '../../lib/utils/datetime_utility';
export default class MergeRequestStore { export default class MergeRequestStore {
constructor(data) { constructor(data) {
this.sha = data.diff_head_sha; this.sha = data.diff_head_sha;
this.gitlabLogo = data.gitlabLogo; this.gitlabLogo = data.gitlabLogo;
...@@ -169,5 +168,4 @@ export default class MergeRequestStore { ...@@ -169,5 +168,4 @@ export default class MergeRequestStore {
return timeagoInstance.format(date); return timeagoInstance.format(date);
} }
} }
...@@ -6,12 +6,12 @@ ...@@ -6,12 +6,12 @@
import userAvatarImage from './user_avatar/user_avatar_image.vue'; import userAvatarImage from './user_avatar/user_avatar_image.vue';
/** /**
* Renders header component for job and pipeline page based on UI mockups * Renders header component for job and pipeline page based on UI mockups
* *
* Used in: * Used in:
* - job show page * - job show page
* - pipeline show page * - pipeline show page
*/ */
export default { export default {
components: { components: {
ciIconBadge, ciIconBadge,
...@@ -118,7 +118,8 @@ ...@@ -118,7 +118,8 @@
<section <section
class="header-action-buttons" class="header-action-buttons"
v-if="actions.length"> v-if="actions.length"
>
<template <template
v-for="(action, i) in actions" v-for="(action, i) in actions"
> >
......
<script> <script>
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
/* This is a re-usable vue component for rendering a button /* This is a re-usable vue component for rendering a button
that will probably be sending off ajax requests and need that will probably be sending off ajax requests and need
to show the loading status by setting the `loading` option. to show the loading status by setting the `loading` option.
......
...@@ -65,7 +65,8 @@ ...@@ -65,7 +65,8 @@
</li> </li>
<li <li
class="md-header-tab" class="md-header-tab"
:class="{ active: previewMarkdown }"> :class="{ active: previewMarkdown }"
>
<a <a
class="js-preview-link" class="js-preview-link"
href="#md-preview-holder" href="#md-preview-holder"
......
...@@ -13,6 +13,12 @@ ...@@ -13,6 +13,12 @@
props: { props: {
/** /**
This function will take the information given by the pagination component This function will take the information given by the pagination component
Here is an example `change` method:
change(pagenum) {
gl.utils.visitUrl(`?page=${pagenum}`);
},
*/ */
change: { change: {
type: Function, type: Function,
......
...@@ -444,6 +444,19 @@ ...@@ -444,6 +444,19 @@
} }
} }
.btn-missing {
color: $notes-light-color;
border: 1px dashed $border-gray-normal-dashed;
border-radius: $border-radius-default;
&:hover,
&:active,
&:focus {
color: $notes-light-color;
background-color: $white-normal;
}
}
.btn-svg svg { .btn-svg svg {
@include btn-svg; @include btn-svg;
} }
......
...@@ -63,10 +63,6 @@ ...@@ -63,10 +63,6 @@
} }
} }
.project-stats {
display: none;
}
.group-buttons { .group-buttons {
display: none; display: none;
} }
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
transition: padding $sidebar-transition-duration; transition: padding $sidebar-transition-duration;
.container-fluid { .container-fluid {
background: $white-light;
padding: 0 $gl-padding; padding: 0 $gl-padding;
&.container-blank { &.container-blank {
......
...@@ -296,7 +296,7 @@ body { ...@@ -296,7 +296,7 @@ body {
line-height: 1.3; line-height: 1.3;
font-size: 1.25em; font-size: 1.25em;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
margin: 12px 7px; margin: 12px 0;
} }
h1, h1,
...@@ -333,6 +333,10 @@ a > code { ...@@ -333,6 +333,10 @@ a > code {
font-family: $monospace_font; font-family: $monospace_font;
} }
.weight-normal {
font-weight: $gl-font-weight-normal;
}
.commit-sha, .commit-sha,
.ref-name { .ref-name {
@extend .monospace; @extend .monospace;
......
...@@ -215,8 +215,8 @@ $tooltip-font-size: 12px; ...@@ -215,8 +215,8 @@ $tooltip-font-size: 12px;
*/ */
$gl-padding: 16px; $gl-padding: 16px;
$gl-padding-8: 8px; $gl-padding-8: 8px;
$gl-padding-4: 4px;
$gl-col-padding: 15px; $gl-col-padding: 15px;
$gl-btn-padding: 10px;
$gl-input-padding: 10px; $gl-input-padding: 10px;
$gl-vert-padding: 6px; $gl-vert-padding: 6px;
$gl-padding-top: 10px; $gl-padding-top: 10px;
...@@ -377,6 +377,10 @@ $inactive-badge-background: rgba(0, 0, 0, .08); ...@@ -377,6 +377,10 @@ $inactive-badge-background: rgba(0, 0, 0, .08);
$btn-active-gray: #ececec; $btn-active-gray: #ececec;
$btn-active-gray-light: e4e7ed; $btn-active-gray-light: e4e7ed;
$btn-white-active: #848484; $btn-white-active: #848484;
$gl-btn-padding: 10px;
$gl-btn-line-height: 16px;
$gl-btn-vert-padding: 8px;
$gl-btn-horz-padding: 12px;
/* /*
* Badges * Badges
......
...@@ -678,6 +678,9 @@ a.deploy-project-label { ...@@ -678,6 +678,9 @@ a.deploy-project-label {
} }
} }
.project-empty-note-panel {
border-bottom: 1px solid $border-color;
}
.project-stats { .project-stats {
font-size: 0; font-size: 0;
...@@ -686,11 +689,13 @@ a.deploy-project-label { ...@@ -686,11 +689,13 @@ a.deploy-project-label {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
.nav { .nav {
padding-top: 12px; margin-top: $gl-padding-8;
padding-bottom: 12px; margin-bottom: $gl-padding-8;
> li { > li {
display: inline-block; display: inline-block;
margin-top: $gl-padding-4;
margin-bottom: $gl-padding-4;
&:not(:last-child) { &:not(:last-child) {
margin-right: $gl-padding; margin-right: $gl-padding;
...@@ -704,36 +709,32 @@ a.deploy-project-label { ...@@ -704,36 +709,32 @@ a.deploy-project-label {
float: right; float: right;
} }
} }
}
> a { .stat-text,
padding: 0; .stat-link {
background-color: transparent; padding: $gl-btn-vert-padding 0;
font-size: 14px; background-color: transparent;
line-height: 29px; font-size: $gl-font-size;
color: $notes-light-color; line-height: $gl-btn-line-height;
color: $notes-light-color;
}
&:hover, .stat-link {
&:focus { &:hover,
color: $gl-text-color; &:focus {
text-decoration: underline; color: $gl-text-color;
} text-decoration: underline;
} }
} }
}
li.missing { .btn {
border: 1px dashed $border-gray-normal-dashed; padding: $gl-btn-vert-padding $gl-btn-horz-padding;
border-radius: $border-radius-default; line-height: $gl-btn-line-height;
a {
padding-left: 10px;
padding-right: 10px;
color: $notes-light-color;
display: block;
} }
&:hover { .btn-missing {
background-color: $gray-normal; @extend .btn-missing;
} }
} }
} }
...@@ -743,7 +744,7 @@ pre.light-well { ...@@ -743,7 +744,7 @@ pre.light-well {
} }
.git-empty { .git-empty {
margin: 0 7px 7px; margin-bottom: 7px;
h5 { h5 {
color: $gl-text-color; color: $gl-text-color;
...@@ -895,6 +896,12 @@ pre.light-well { ...@@ -895,6 +896,12 @@ pre.light-well {
} }
} }
.project-tip-command {
> .input-group-btn:first-child {
width: auto;
}
}
.protected-branches-list, .protected-branches-list,
.protected-tags-list { .protected-tags-list {
margin-bottom: 30px; margin-bottom: 30px;
......
...@@ -126,10 +126,15 @@ class ApplicationController < ActionController::Base ...@@ -126,10 +126,15 @@ class ApplicationController < ActionController::Base
Ability.allowed?(object, action, subject) Ability.allowed?(object, action, subject)
end end
def access_denied! def access_denied!(message = nil)
respond_to do |format| respond_to do |format|
format.json { head :not_found } format.any { head :not_found }
format.any { render "errors/access_denied", layout: "errors", status: 404 } format.html do
render "errors/access_denied",
layout: "errors",
status: 404,
locals: { message: message }
end
end end
end end
......
...@@ -55,7 +55,7 @@ module Boards ...@@ -55,7 +55,7 @@ module Boards
end end
def issue def issue
@issue ||= issues_finder.execute.find(params[:id]) @issue ||= issues_finder.find(params[:id])
end end
def filter_params def filter_params
......
module ControllerWithCrossProjectAccessCheck
extend ActiveSupport::Concern
included do
extend Gitlab::CrossProjectAccess::ClassMethods
before_action :cross_project_check
end
def cross_project_check
if Gitlab::CrossProjectAccess.find_check(self)&.should_run?(self)
authorize_cross_project_page!
end
end
def authorize_cross_project_page!
return if can?(current_user, :read_cross_project)
rejection_message = _(
"This page is unavailable because you are not allowed to read information "\
"across multiple projects."
)
access_denied!(rejection_message)
end
end
...@@ -3,16 +3,20 @@ module RoutableActions ...@@ -3,16 +3,20 @@ module RoutableActions
def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil) def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil)
routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?) routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?)
if routable_authorized?(routable, extra_authorization_proc) if routable_authorized?(routable, extra_authorization_proc)
ensure_canonical_path(routable, requested_full_path) ensure_canonical_path(routable, requested_full_path)
routable routable
else else
route_not_found handle_not_found_or_authorized(routable)
nil nil
end end
end end
# This is overridden in gitlab-ee.
def handle_not_found_or_authorized(_routable)
route_not_found
end
def routable_authorized?(routable, extra_authorization_proc) def routable_authorized?(routable, extra_authorization_proc)
action = :"read_#{routable.class.to_s.underscore}" action = :"read_#{routable.class.to_s.underscore}"
return false unless can?(current_user, action, routable) return false unless can?(current_user, action, routable)
......
...@@ -24,7 +24,7 @@ module UploadsActions ...@@ -24,7 +24,7 @@ module UploadsActions
# - or redirect to its URL # - or redirect to its URL
# #
def show def show
return render_404 unless uploader.exists? return render_404 unless uploader&.exists?
if uploader.file_storage? if uploader.file_storage?
disposition = uploader.image_or_video? ? 'inline' : 'attachment' disposition = uploader.image_or_video? ? 'inline' : 'attachment'
...@@ -71,6 +71,9 @@ module UploadsActions ...@@ -71,6 +71,9 @@ module UploadsActions
def build_uploader_from_params def build_uploader_from_params
uploader = uploader_class.new(model, secret: params[:secret]) uploader = uploader_class.new(model, secret: params[:secret])
return nil unless uploader.model_valid?
uploader.retrieve_from_store!(params[:filename]) uploader.retrieve_from_store!(params[:filename])
uploader uploader
end end
......
class Dashboard::ApplicationController < ApplicationController class Dashboard::ApplicationController < ApplicationController
include ControllerWithCrossProjectAccessCheck
layout 'dashboard' layout 'dashboard'
requires_cross_project_access
private private
def projects def projects
......
class Dashboard::GroupsController < Dashboard::ApplicationController class Dashboard::GroupsController < Dashboard::ApplicationController
include GroupTree include GroupTree
skip_cross_project_access_check :index
def index def index
groups = GroupsFinder.new(current_user, all_available: false).execute groups = GroupsFinder.new(current_user, all_available: false).execute
render_group_tree(groups) render_group_tree(groups)
......
...@@ -4,6 +4,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -4,6 +4,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
before_action :set_non_archived_param before_action :set_non_archived_param
before_action :default_sorting before_action :default_sorting
skip_cross_project_access_check :index, :starred
def index def index
@projects = load_projects(params.merge(non_public: true)).page(params[:page]) @projects = load_projects(params.merge(non_public: true)).page(params[:page])
......
class Dashboard::SnippetsController < Dashboard::ApplicationController class Dashboard::SnippetsController < Dashboard::ApplicationController
skip_cross_project_access_check :index
def index def index
@snippets = SnippetsFinder.new( @snippets = SnippetsFinder.new(
current_user, current_user,
......
class Groups::ApplicationController < ApplicationController class Groups::ApplicationController < ApplicationController
include RoutableActions include RoutableActions
include ControllerWithCrossProjectAccessCheck
layout 'group' layout 'group'
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
before_action :group before_action :group
requires_cross_project_access
private private
......
class Groups::AvatarsController < Groups::ApplicationController class Groups::AvatarsController < Groups::ApplicationController
before_action :authorize_admin_group! before_action :authorize_admin_group!
skip_cross_project_access_check :destroy
def destroy def destroy
@group.remove_avatar! @group.remove_avatar!
@group.save @group.save
......
module Groups module Groups
class ChildrenController < Groups::ApplicationController class ChildrenController < Groups::ApplicationController
before_action :group before_action :group
skip_cross_project_access_check :index
def index def index
parent = if params[:parent_id].present? parent = if params[:parent_id].present?
......
...@@ -6,6 +6,10 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -6,6 +6,10 @@ class Groups::GroupMembersController < Groups::ApplicationController
# Authorize # Authorize
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access] before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
skip_cross_project_access_check :index, :create, :update, :destroy, :request_access,
:approve_access_request, :leave, :resend_invite,
:override
def index def index
@sort = params[:sort].presence || sort_value_name @sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id] @project = @group.projects.find(params[:project_id]) if params[:project_id]
......
module Groups module Groups
module Settings module Settings
class CiCdController < Groups::ApplicationController class CiCdController < Groups::ApplicationController
skip_cross_project_access_check :show
before_action :authorize_admin_pipeline! before_action :authorize_admin_pipeline!
def show def show
......
...@@ -2,6 +2,8 @@ module Groups ...@@ -2,6 +2,8 @@ module Groups
class VariablesController < Groups::ApplicationController class VariablesController < Groups::ApplicationController
before_action :authorize_admin_build! before_action :authorize_admin_build!
skip_cross_project_access_check :show, :update
def show def show
respond_to do |format| respond_to do |format|
format.json do format.json do
......
...@@ -19,6 +19,12 @@ class GroupsController < Groups::ApplicationController ...@@ -19,6 +19,12 @@ class GroupsController < Groups::ApplicationController
before_action :user_actions, only: [:show, :subgroups] before_action :user_actions, only: [:show, :subgroups]
skip_cross_project_access_check :index, :new, :create, :edit, :update,
:destroy, :projects
# When loading show as an atom feed, we render events that could leak cross
# project information
skip_cross_project_access_check :show, if: -> { request.format.html? }
layout :determine_layout layout :determine_layout
def index def index
......
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::GonHelper include Gitlab::GonHelper
include Gitlab::Allowable
include PageLayoutHelper include PageLayoutHelper
include OauthApplications include OauthApplications
...@@ -8,6 +9,8 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController ...@@ -8,6 +9,8 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
before_action :add_gon_variables before_action :add_gon_variables
before_action :load_scopes, only: [:index, :create, :edit] before_action :load_scopes, only: [:index, :create, :edit]
helper_method :can?
layout 'profile' layout 'profile'
def index def index
......
...@@ -34,9 +34,9 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController ...@@ -34,9 +34,9 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
def target def target
case params[:type]&.downcase case params[:type]&.downcase
when 'issue' when 'issue'
IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id]) IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:type_id])
when 'mergerequest' when 'mergerequest'
MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id]) MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:type_id])
when 'commit' when 'commit'
@project.commit(params[:type_id]) @project.commit(params[:type_id])
end end
......
...@@ -133,7 +133,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -133,7 +133,7 @@ class Projects::BlobController < Projects::ApplicationController
end end
def after_edit_path def after_edit_path
from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid]) from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:from_merge_request_iid])
if from_merge_request && @branch_name == @ref if from_merge_request && @branch_name == @ref
diffs_project_merge_request_path(from_merge_request.target_project, from_merge_request) + diffs_project_merge_request_path(from_merge_request.target_project, from_merge_request) +
"##{hexdigest(@path)}" "##{hexdigest(@path)}"
......
...@@ -40,9 +40,9 @@ class Projects::Clusters::GcpController < Projects::ApplicationController ...@@ -40,9 +40,9 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
def verify_billing def verify_billing
case google_project_billing_status case google_project_billing_status
when nil when nil
flash[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.') flash.now[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
when false when false
flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" } flash.now[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
when true when true
return return
end end
......
...@@ -75,7 +75,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -75,7 +75,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
def branch_to def branch_to
@target_project = selected_target_project @target_project = selected_target_project
if params[:ref].present? if @target_project && params[:ref].present?
@ref = params[:ref] @ref = params[:ref]
@commit = @target_project.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref) @commit = @target_project.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref)
end end
...@@ -85,7 +85,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -85,7 +85,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
def update_branches def update_branches
@target_project = selected_target_project @target_project = selected_target_project
@target_branches = @target_project.repository.branch_names @target_branches = @target_project ? @target_project.repository.branch_names : []
render layout: false render layout: false
end end
...@@ -121,7 +121,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -121,7 +121,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
@project @project
elsif params[:target_project_id].present? elsif params[:target_project_id].present?
MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: @project) MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: @project)
.execute.find(params[:target_project_id]) .find_by(id: params[:target_project_id])
else else
@project.forked_from_project @project.forked_from_project
end end
......
...@@ -3,7 +3,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController ...@@ -3,7 +3,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController
before_action :require_pages_enabled! before_action :require_pages_enabled!
before_action :authorize_update_pages!, except: [:show] before_action :authorize_update_pages!, except: [:show]
before_action :domain, only: [:show, :destroy] before_action :domain, only: [:show, :destroy, :verify]
def show def show
end end
...@@ -12,11 +12,23 @@ class Projects::PagesDomainsController < Projects::ApplicationController ...@@ -12,11 +12,23 @@ class Projects::PagesDomainsController < Projects::ApplicationController
@domain = @project.pages_domains.new @domain = @project.pages_domains.new
end end
def verify
result = VerifyPagesDomainService.new(@domain).execute
if result[:status] == :success
flash[:notice] = 'Successfully verified domain ownership'
else
flash[:alert] = 'Failed to verify domain ownership'
end
redirect_to project_pages_domain_path(@project, @domain)
end
def create def create
@domain = @project.pages_domains.create(pages_domain_params) @domain = @project.pages_domains.create(pages_domain_params)
if @domain.valid? if @domain.valid?
redirect_to project_pages_path(@project) redirect_to project_pages_domain_path(@project, @domain)
else else
render 'new' render 'new'
end end
...@@ -46,6 +58,6 @@ class Projects::PagesDomainsController < Projects::ApplicationController ...@@ -46,6 +58,6 @@ class Projects::PagesDomainsController < Projects::ApplicationController
end end
def domain def domain
@domain ||= @project.pages_domains.find_by(domain: params[:id].to_s) @domain ||= @project.pages_domains.find_by!(domain: params[:id].to_s)
end end
end end
module Projects
module Prometheus
class MetricsController < Projects::ApplicationController
before_action :authorize_admin_project!
def active_common
respond_to do |format|
format.json do
matched_metrics = prometheus_service.matched_metrics || {}
if matched_metrics.any?
render json: matched_metrics
else
head :no_content
end
end
end
end
private
def prometheus_service
@prometheus_service ||= project.find_or_initialize_service('prometheus')
end
end
end
end
class Projects::PrometheusController < Projects::ApplicationController
before_action :authorize_read_project!
before_action :require_prometheus_metrics!
def active_metrics
respond_to do |format|
format.json do
matched_metrics = project.prometheus_service.matched_metrics || {}
if matched_metrics.any?
render json: matched_metrics
else
head :no_content
end
end
end
end
private
def require_prometheus_metrics!
render_404 unless project.prometheus_service.present?
end
end
...@@ -45,7 +45,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -45,7 +45,7 @@ class ProjectsController < Projects::ApplicationController
notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name } notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name }
) )
else else
render 'new' render 'new', locals: { active_tab: ('import' if project_params[:import_url].present?) }
end end
end end
...@@ -114,6 +114,8 @@ class ProjectsController < Projects::ApplicationController ...@@ -114,6 +114,8 @@ class ProjectsController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
@notification_setting = current_user.notification_settings_for(@project) if current_user @notification_setting = current_user.notification_settings_for(@project) if current_user
@project = @project.present(current_user: current_user)
render_landing_page render_landing_page
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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