Commit 1bbeafc3 authored by Kamil Trzcinski's avatar Kamil Trzcinski

Merge remote-tracking branch 'origin/master' into 38464-k8s-apps

parents 4477f7bb d51ad1ea
7.5 9.0.0
\ No newline at end of file
## Contributor license agreement ## Developer Certificate of Origin + License
By submitting code as an individual you agree to the By contributing to GitLab B.V., You accept and agree to the following terms and
[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md). conditions for Your present and future Contributions submitted to GitLab B.V.
By submitting code as an entity you agree to the Except for the license granted herein to GitLab B.V. and recipients of software
[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md). distributed by GitLab B.V., You reserve all right, title, and interest in and to
Your Contributions. All Contributions are subject to the following DCO + License
terms.
[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md)
_This notice should stay as the first item in the CONTRIBUTING.md file._ _This notice should stay as the first item in the CONTRIBUTING.md file._
...@@ -100,8 +104,7 @@ the remaining issues on the GitHub issue tracker. ...@@ -100,8 +104,7 @@ the remaining issues on the GitHub issue tracker.
## I want to contribute! ## I want to contribute!
If you want to contribute to GitLab, but are not sure where to start, If you want to contribute to GitLab, [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight] is a great place to start. Issues with a lower weight (1 or 2) are deemed suitable for beginners.
look for [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight].
These issues will be of reasonable size and challenge, for anyone to start These issues will be of reasonable size and challenge, for anyone to start
contributing to GitLab. contributing to GitLab.
......
...@@ -291,7 +291,7 @@ GEM ...@@ -291,7 +291,7 @@ GEM
diff-lcs (~> 1.1) diff-lcs (~> 1.1)
mime-types (>= 1.16) mime-types (>= 1.16)
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab-markup (1.6.2) gitlab-markup (1.6.3)
gitlab_omniauth-ldap (2.0.4) gitlab_omniauth-ldap (2.0.4)
net-ldap (~> 0.16) net-ldap (~> 0.16)
omniauth (~> 1.3) omniauth (~> 1.3)
......
/* eslint-disable comma-dangle, space-before-function-paren, no-new */ /* eslint-disable comma-dangle, space-before-function-paren, no-new */
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global LabelsSelect */
/* global Sidebar */ /* global Sidebar */
import Vue from 'vue'; import Vue from 'vue';
...@@ -11,6 +10,7 @@ import Assignees from '../../sidebar/components/assignees/assignees'; ...@@ -11,6 +10,7 @@ import Assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select'; import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue'; import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context'; import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
import IssuableIndex from './issuable_index'; import IssuableIndex from './issuable_index';
/* global Milestone */ /* global Milestone */
import IssuableForm from './issuable_form'; import IssuableForm from './issuable_form';
/* global LabelsSelect */ import LabelsSelect from './labels_select';
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global NewBranchForm */ /* global NewBranchForm */
/* global NotificationsForm */ /* global NotificationsForm */
...@@ -16,7 +16,7 @@ import CILintEditor from './ci_lint_editor'; ...@@ -16,7 +16,7 @@ import CILintEditor from './ci_lint_editor';
import groupsSelect from './groups_select'; import groupsSelect from './groups_select';
/* global Search */ /* global Search */
/* global Admin */ /* global Admin */
/* global NamespaceSelects */ import NamespaceSelect from './namespace_select';
/* global NewCommitForm */ /* global NewCommitForm */
/* global NewBranchForm */ /* global NewBranchForm */
/* global Project */ /* global Project */
...@@ -575,7 +575,8 @@ import Diff from './diff'; ...@@ -575,7 +575,8 @@ import Diff from './diff';
new UsersSelect(); new UsersSelect();
break; break;
case 'projects': case 'projects':
new NamespaceSelects(); document.querySelectorAll('.js-namespace-select')
.forEach(dropdown => new NamespaceSelect({ dropdown }));
break; break;
case 'labels': case 'labels':
switch (path[2]) { switch (path[2]) {
......
...@@ -30,7 +30,7 @@ const utils = { ...@@ -30,7 +30,7 @@ const utils = {
}, },
isDropDownParts(target) { isDropDownParts(target) {
if (!target || target.tagName === 'HTML') return false; if (!target || !target.hasAttribute || target.tagName === 'HTML') return false;
return target.hasAttribute(DATA_TRIGGER) || target.hasAttribute(DATA_DROPDOWN); return target.hasAttribute(DATA_TRIGGER) || target.hasAttribute(DATA_DROPDOWN);
}, },
}; };
......
...@@ -119,11 +119,9 @@ export default function dropzoneInput(form) { ...@@ -119,11 +119,9 @@ export default function dropzoneInput(form) {
// removeAllFiles(true) stops uploading files (if any) // removeAllFiles(true) stops uploading files (if any)
// and remove them from dropzone files queue. // and remove them from dropzone files queue.
$cancelButton.on('click', (e) => { $cancelButton.on('click', (e) => {
const target = e.target.closest('.js-main-target-form').querySelector('.div-dropzone');
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
Dropzone.forElement(target).removeAllFiles(true); Dropzone.forElement($formDropzone.get(0)).removeAllFiles(true);
}); });
// If 'error' event is fired, we store a failed files, // If 'error' event is fired, we store a failed files,
......
...@@ -421,7 +421,11 @@ export default { ...@@ -421,7 +421,11 @@ export default {
</script> </script>
<template> <template>
<div <div
:class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }" class="gl-responsive-table-row"
:class="{
'js-child-row environment-child-row': model.isChildren,
'folder-row': model.isFolder,
}"
role="row"> role="row">
<div class="table-section section-10" role="gridcell"> <div class="table-section section-10" role="gridcell">
<div <div
...@@ -495,15 +499,16 @@ export default { ...@@ -495,15 +499,16 @@ export default {
</a> </a>
</div> </div>
<div class="table-section section-25" role="gridcell">
<div <div
v-if="!model.isFolder" v-if="!model.isFolder"
class="table-section section-25" role="gridcell">
<div
role="rowheader" role="rowheader"
class="table-mobile-header"> class="table-mobile-header">
Commit Commit
</div> </div>
<div <div
v-if="!model.isFolder && hasLastDeploymentKey" v-if="hasLastDeploymentKey"
class="js-commit-component table-mobile-content"> class="js-commit-component table-mobile-content">
<commit-component <commit-component
:tag="commitTag" :tag="commitTag"
...@@ -514,21 +519,22 @@ export default { ...@@ -514,21 +519,22 @@ export default {
:author="commitAuthor"/> :author="commitAuthor"/>
</div> </div>
<div <div
v-if="!model.isFolder && !hasLastDeploymentKey" v-if="!hasLastDeploymentKey"
class="commit-title table-mobile-content"> class="commit-title table-mobile-content">
No deployments yet No deployments yet
</div> </div>
</div> </div>
<div class="table-section section-10" role="gridcell">
<div <div
v-if="!model.isFolder" v-if="!model.isFolder"
class="table-section section-10" role="gridcell">
<div
role="rowheader" role="rowheader"
class="table-mobile-header"> class="table-mobile-header">
Updated Updated
</div> </div>
<span <span
v-if="!model.isFolder && canShowDate" v-if="canShowDate"
class="environment-created-date-timeago table-mobile-content"> class="environment-created-date-timeago table-mobile-content">
{{createdDate}} {{createdDate}}
</span> </span>
......
/* eslint-disable func-names, no-underscore-dangle, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */ /* eslint-disable func-names, no-underscore-dangle, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */ /* global fuzzaldrinPlus */
import _ from 'underscore'; import _ from 'underscore';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { isObject } from './lib/utils/type_utility'; import { isObject } from './lib/utils/type_utility';
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput; var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput;
......
...@@ -4,6 +4,7 @@ import _ from 'underscore'; ...@@ -4,6 +4,7 @@ import _ from 'underscore';
import d3 from 'd3'; import d3 from 'd3';
import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph';
import ContributorsStatGraphUtil from './stat_graph_contributors_util'; import ContributorsStatGraphUtil from './stat_graph_contributors_util';
import { n__ } from '../locale';
export default (function() { export default (function() {
function ContributorsStatGraph() {} function ContributorsStatGraph() {}
...@@ -44,7 +45,7 @@ export default (function() { ...@@ -44,7 +45,7 @@ export default (function() {
commits = $('<span/>', { commits = $('<span/>', {
"class": 'graph-author-commits-count' "class": 'graph-author-commits-count'
}); });
commits.text(author.commits + " commits"); commits.text(n__('%d commit', '%d commits', author.commits));
return $('<span/>').append(commits); return $('<span/>').append(commits);
}; };
......
/* eslint-disable no-new */ /* eslint-disable no-new */
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global LabelsSelect */ import LabelsSelect from './labels_select';
import IssuableContext from './issuable_context'; import IssuableContext from './issuable_context';
/* global Sidebar */ /* global Sidebar */
......
/* eslint-disable no-new */ /* eslint-disable no-new */
/* global LabelsSelect */ import LabelsSelect from './labels_select';
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global SubscriptionSelect */ /* global SubscriptionSelect */
......
/* eslint-disable class-methods-use-this, no-new */ /* eslint-disable class-methods-use-this, no-new */
/* global LabelsSelect */
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global SubscriptionSelect */ /* global SubscriptionSelect */
...@@ -7,7 +6,7 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; ...@@ -7,7 +6,7 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import './milestone_select'; import './milestone_select';
import issueStatusSelect from './issue_status_select'; import issueStatusSelect from './issue_status_select';
import './subscription_select'; import './subscription_select';
import './labels_select'; import LabelsSelect from './labels_select';
const HIDDEN_CLASS = 'hidden'; const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content'; const DISABLED_CONTENT_CLASS = 'disabled-content';
......
...@@ -6,9 +6,8 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; ...@@ -6,9 +6,8 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils'; import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label'; import CreateLabelDropdown from './create_label';
(function() { export default class LabelsSelect {
this.LabelsSelect = (function() { constructor(els) {
function LabelsSelect(els) {
var _this, $els; var _this, $els;
_this = this; _this = this;
...@@ -408,22 +407,22 @@ import CreateLabelDropdown from './create_label'; ...@@ -408,22 +407,22 @@ import CreateLabelDropdown from './create_label';
this.bindEvents(); this.bindEvents();
} }
LabelsSelect.prototype.bindEvents = function() { bindEvents() {
return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue); return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue);
}; }
// eslint-disable-next-line class-methods-use-this
LabelsSelect.prototype.onSelectCheckboxIssue = function() { onSelectCheckboxIssue() {
if ($('.selected_issue:checked').length) { if ($('.selected_issue:checked').length) {
return; return;
} }
return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label'); return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label');
}; }
// eslint-disable-next-line class-methods-use-this
LabelsSelect.prototype.enableBulkLabelDropdown = function() { enableBulkLabelDropdown() {
IssuableBulkUpdateActions.willUpdateLabels = true; IssuableBulkUpdateActions.willUpdateLabels = true;
}; }
// eslint-disable-next-line class-methods-use-this
LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) { setDropdownData($dropdown, isMarking, value) {
var i, markedIds, unmarkedIds, indeterminateIds; var i, markedIds, unmarkedIds, indeterminateIds;
markedIds = $dropdown.data('marked') || []; markedIds = $dropdown.data('marked') || [];
...@@ -464,16 +463,13 @@ import CreateLabelDropdown from './create_label'; ...@@ -464,16 +463,13 @@ import CreateLabelDropdown from './create_label';
$dropdown.data('marked', markedIds); $dropdown.data('marked', markedIds);
$dropdown.data('unmarked', unmarkedIds); $dropdown.data('unmarked', unmarkedIds);
$dropdown.data('indeterminate', indeterminateIds); $dropdown.data('indeterminate', indeterminateIds);
}; }
// eslint-disable-next-line class-methods-use-this
LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) { setOriginalDropdownData($container, $dropdown) {
var labels = []; const labels = [];
$container.find('[name="label_name[]"]').map(function() { $container.find('[name="label_name[]"]').map(function() {
return labels.push(this.value); return labels.push(this.value);
}); });
$dropdown.data('marked', labels); $dropdown.data('marked', labels);
}; }
}
return LabelsSelect;
})();
}).call(window);
/* eslint-disable one-export, one-var, one-var-declaration-per-line */
import _ from 'underscore'; import _ from 'underscore';
export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
...@@ -21,8 +19,11 @@ export default class LazyLoader { ...@@ -21,8 +19,11 @@ export default class LazyLoader {
} }
searchLazyImages() { searchLazyImages() {
this.lazyImages = [].slice.call(document.querySelectorAll('.lazy')); this.lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
if (this.lazyImages.length) {
this.checkElementsInView(); this.checkElementsInView();
} }
}
startContentObserver() { startContentObserver() {
const contentNode = document.querySelector(this.observerNode) || document.querySelector('body'); const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
...@@ -45,15 +46,13 @@ export default class LazyLoader { ...@@ -45,15 +46,13 @@ export default class LazyLoader {
checkElementsInView() { checkElementsInView() {
const scrollTop = pageYOffset; const scrollTop = pageYOffset;
const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD; const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD;
let imgBoundRect, imgTop, imgBound;
// Loading Images which are in the current viewport or close to them // Loading Images which are in the current viewport or close to them
this.lazyImages = this.lazyImages.filter((selectedImage) => { this.lazyImages = this.lazyImages.filter((selectedImage) => {
if (selectedImage.getAttribute('data-src')) { if (selectedImage.getAttribute('data-src')) {
imgBoundRect = selectedImage.getBoundingClientRect(); const imgBoundRect = selectedImage.getBoundingClientRect();
const imgTop = scrollTop + imgBoundRect.top;
imgTop = scrollTop + imgBoundRect.top; const imgBound = imgTop + imgBoundRect.height;
imgBound = imgTop + imgBoundRect.height;
if (scrollTop < imgBound && visHeight > imgTop) { if (scrollTop < imgBound && visHeight > imgTop) {
LazyLoader.loadImage(selectedImage); LazyLoader.loadImage(selectedImage);
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */ export default function initLogoAnimation() {
window.addEventListener('beforeunload', () => {
(function() {
window.addEventListener('beforeunload', function() {
$('.tanuki-logo').addClass('animate'); $('.tanuki-logo').addClass('animate');
}); });
}).call(window); }
...@@ -12,7 +12,6 @@ import svg4everybody from 'svg4everybody'; ...@@ -12,7 +12,6 @@ import svg4everybody from 'svg4everybody';
// libraries with import side-effects // libraries with import side-effects
import 'mousetrap'; import 'mousetrap';
import 'mousetrap/plugins/pause/mousetrap-pause'; import 'mousetrap/plugins/pause/mousetrap-pause';
import 'vendor/fuzzaldrin-plus';
// expose common libraries as globals (TODO: remove these) // expose common libraries as globals (TODO: remove these)
window.jQuery = jQuery; window.jQuery = jQuery;
...@@ -56,11 +55,10 @@ import './gl_field_errors'; ...@@ -56,11 +55,10 @@ import './gl_field_errors';
import './gl_form'; import './gl_form';
import initTodoToggle from './header'; import initTodoToggle from './header';
import initImporterStatus from './importer_status'; import initImporterStatus from './importer_status';
import './labels_select';
import './layout_nav'; import './layout_nav';
import LazyLoader from './lazy_loader'; import LazyLoader from './lazy_loader';
import './line_highlighter'; import './line_highlighter';
import './logo'; import initLogoAnimation from './logo';
import './merge_request'; import './merge_request';
import './merge_request_tabs'; import './merge_request_tabs';
import './milestone'; import './milestone';
...@@ -135,6 +133,7 @@ $(function () { ...@@ -135,6 +133,7 @@ $(function () {
initBreadcrumbs(); initBreadcrumbs();
initImporterStatus(); initImporterStatus();
initTodoToggle(); initTodoToggle();
initLogoAnimation();
// Set the default path for all cookies to GitLab's root directory // Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/'; Cookies.defaults.path = gon.relative_url_root || '/';
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, no-param-reassign, no-cond-assign, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */
import Api from './api'; import Api from './api';
import './lib/utils/url_utility';
(function() { export default class NamespaceSelect {
window.NamespaceSelect = (function() { constructor(opts) {
function NamespaceSelect(opts) { const isFilter = opts.dropdown.dataset.isFilter === 'true';
this.onSelectItem = this.onSelectItem.bind(this); const fieldName = opts.dropdown.dataset.fieldName || 'namespace_id';
var fieldName, showAny;
this.dropdown = opts.dropdown; $(opts.dropdown).glDropdown({
showAny = true;
fieldName = 'namespace_id';
if (this.dropdown.attr('data-field-name')) {
fieldName = this.dropdown.data('fieldName');
}
if (this.dropdown.attr('data-show-any')) {
showAny = this.dropdown.data('showAny');
}
this.dropdown.glDropdown({
filterable: true, filterable: true,
selectable: true, selectable: true,
filterRemote: true, filterRemote: true,
...@@ -32,9 +24,8 @@ import Api from './api'; ...@@ -32,9 +24,8 @@ import Api from './api';
}, },
data: function(term, dataCallback) { data: function(term, dataCallback) {
return Api.namespaces(term, function(namespaces) { return Api.namespaces(term, function(namespaces) {
var anyNamespace; if (isFilter) {
if (showAny) { const anyNamespace = {
anyNamespace = {
text: 'Any namespace', text: 'Any namespace',
id: null id: null
}; };
...@@ -52,34 +43,15 @@ import Api from './api'; ...@@ -52,34 +43,15 @@ import Api from './api';
} }
}, },
renderRow: this.renderRow, renderRow: this.renderRow,
clicked: this.onSelectItem clicked(options) {
}); if (!isFilter) {
}
NamespaceSelect.prototype.onSelectItem = function(options) {
const { e } = options; const { e } = options;
return e.preventDefault(); e.preventDefault();
};
return NamespaceSelect;
})();
window.NamespaceSelects = (function() {
function NamespaceSelects(opts) {
var ref;
if (opts == null) {
opts = {};
} }
this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-namespace-select'); },
this.$dropdowns.each(function(i, dropdown) { url(namespace) {
var $dropdown; return gl.utils.mergeUrlParams({ [fieldName]: namespace.id }, window.location.href);
$dropdown = $(dropdown); },
return new window.NamespaceSelect({
dropdown: $dropdown
});
}); });
} }
}
return NamespaceSelects;
})();
}).call(window);
...@@ -122,7 +122,9 @@ ...@@ -122,7 +122,9 @@
// we need to do this to prevent noteForm inconsistent content warning // we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content // this is something we intentionally do so we need to recover the content
this.note.note = noteText; this.note.note = noteText;
if (this.$refs.noteBody) {
this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better
}
}, },
}, },
created() { created() {
......
<script> <script>
import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import icon from '../../../vue_shared/components/icon.vue';
/** /**
* Renders either a cancel, retry or play icon pointing to the given path. * Renders either a cancel, retry or play icon pointing to the given path.
...@@ -29,17 +29,18 @@ ...@@ -29,17 +29,18 @@
}, },
}, },
components: {
icon,
},
directives: { directives: {
tooltip, tooltip,
}, },
computed: { computed: {
actionIconSvg() {
return getActionIcon(this.actionIcon);
},
cssClass() { cssClass() {
return `js-${gl.text.dasherize(this.actionIcon)}`; const actionIconDash = gl.text.dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`;
}, },
}, },
}; };
...@@ -50,14 +51,9 @@ ...@@ -50,14 +51,9 @@
:data-method="actionMethod" :data-method="actionMethod"
:title="tooltipText" :title="tooltipText"
:href="link" :href="link"
class="ci-action-icon-container" class="ci-action-icon-container ci-action-icon-wrapper"
data-container="body">
<i
class="ci-action-icon-wrapper"
:class="cssClass" :class="cssClass"
v-html="actionIconSvg" data-container="body">
aria-hidden="true" <icon :name="actionIcon"/>
/>
</a> </a>
</template> </template>
<script> <script>
import getActionIcon from '../../../vue_shared/ci_action_icons'; import icon from '../../../vue_shared/components/icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
/** /**
...@@ -29,14 +29,12 @@ ...@@ -29,14 +29,12 @@
}, },
}, },
directives: { components: {
tooltip, icon,
}, },
computed: { directives: {
actionIconSvg() { tooltip,
return getActionIcon(this.actionIcon);
},
}, },
}; };
</script> </script>
...@@ -49,7 +47,7 @@ ...@@ -49,7 +47,7 @@
rel="nofollow" rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon" class="ci-action-icon-wrapper js-ci-status-icon"
data-container="body" data-container="body"
v-html="actionIconSvg"
aria-label="Job's action"> aria-label="Job's action">
<icon :name="actionIcon"/>
</a> </a>
</template> </template>
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
* "group": "success", * "group": "success",
* "details_path": "/root/ci-mock/builds/4256", * "details_path": "/root/ci-mock/builds/4256",
* "action": { * "action": {
* "icon": "icon_action_retry", * "icon": "retry",
* "title": "Retry", * "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry", * "path": "/root/ci-mock/builds/4256/retry",
* "method": "post" * "method": "post"
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
* "group": "success", * "group": "success",
* "details_path": "/root/ci-mock/builds/4256", * "details_path": "/root/ci-mock/builds/4256",
* "action": { * "action": {
* "icon": "icon_action_retry", * "icon": "retry",
* "title": "Retry", * "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry", * "path": "/root/ci-mock/builds/4256/retry",
* "method": "post" * "method": "post"
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
*/ */
import Flash from '../../flash'; import Flash from '../../flash';
import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons'; import icon from '../../vue_shared/components/icon.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
...@@ -45,6 +45,7 @@ export default { ...@@ -45,6 +45,7 @@ export default {
components: { components: {
loadingIcon, loadingIcon,
icon,
}, },
updated() { updated() {
...@@ -122,8 +123,8 @@ export default { ...@@ -122,8 +123,8 @@ export default {
return `ci-status-icon-${this.stage.status.group}`; return `ci-status-icon-${this.stage.status.group}`;
}, },
svgIcon() { borderlessIcon() {
return borderlessStatusIconEntityMap[this.stage.status.icon]; return `${this.stage.status.icon}_borderless`;
}, },
}, },
}; };
...@@ -145,9 +146,10 @@ export default { ...@@ -145,9 +146,10 @@ export default {
aria-expanded="false"> aria-expanded="false">
<span <span
v-html="svgIcon"
aria-hidden="true" aria-hidden="true"
:aria-label="stage.title"> :aria-label="stage.title">
<icon
:name="borderlessIcon"/>
</span> </span>
<i <i
......
...@@ -98,7 +98,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), ...@@ -98,7 +98,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
@toggle="toggleOpen" @toggle="toggleOpen"
@submit="onSubmit"> @submit="onSubmit">
<template slot="body" scope="props"> <template slot="body" slot-scope="props">
<p v-html="props.text"></p> <p v-html="props.text"></p>
<form <form
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, prefer-template, no-unused-vars, no-return-assign */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, prefer-template, no-unused-vars, no-return-assign */
/* global fuzzaldrinPlus */
import fuzzaldrinPlus from 'fuzzaldrin-plus';
(function() { (function() {
this.ProjectFindFile = (function() { this.ProjectFindFile = (function() {
......
...@@ -27,6 +27,8 @@ export default { ...@@ -27,6 +27,8 @@ export default {
'changeFileContent', 'changeFileContent',
]), ]),
initMonaco() { initMonaco() {
if (this.shouldHideEditor) return;
if (this.monacoInstance) { if (this.monacoInstance) {
this.monacoInstance.setModel(null); this.monacoInstance.setModel(null);
} }
...@@ -94,8 +96,12 @@ export default { ...@@ -94,8 +96,12 @@ export default {
<template> <template>
<div <div
id="ide" id="ide"
v-if='!shouldHideEditor'
class="blob-viewer-container blob-editor-container" class="blob-viewer-container blob-editor-container"
> >
<div
v-if="shouldHideEditor"
v-html="activeFile.html"
>
</div>
</div> </div>
</template> </template>
function expandSectionParent($section, $content) {
$section.addClass('expanded');
$content.off('animationend.expandSectionParent');
}
function expandSection($section) { function expandSection($section) {
$section.find('.js-settings-toggle').text('Collapse'); $section.find('.js-settings-toggle').text('Collapse');
$section.find('.settings-content').off('scroll.expandSection').scrollTop(0);
const $content = $section.find('.settings-content'); $section.addClass('expanded');
$content.addClass('expanded').off('scroll.expandSection').scrollTop(0); if (!$section.hasClass('no-animate')) {
$section.addClass('animating')
if ($content.hasClass('no-animate')) { .one('animationend.animateSection', () => $section.removeClass('animating'));
expandSectionParent($section, $content);
} else {
$content.on('animationend.expandSectionParent', () => expandSectionParent($section, $content));
} }
} }
function closeSection($section) { function closeSection($section) {
$section.find('.js-settings-toggle').text('Expand'); $section.find('.js-settings-toggle').text('Expand');
$section.find('.settings-content').on('scroll.expandSection', () => expandSection($section));
const $content = $section.find('.settings-content');
$content.removeClass('expanded').on('scroll.expandSection', () => expandSection($section));
$section.removeClass('expanded'); $section.removeClass('expanded');
if (!$section.hasClass('no-animate')) {
$section.addClass('animating')
.one('animationend.animateSection', () => $section.removeClass('animating'));
}
} }
function toggleSection($section) { function toggleSection($section) {
const $content = $section.find('.settings-content'); $section.removeClass('no-animate');
$content.removeClass('no-animate'); if ($section.hasClass('expanded')) {
if ($content.hasClass('expanded')) {
closeSection($section); closeSection($section);
} else { } else {
expandSection($section); expandSection($section);
...@@ -39,10 +31,19 @@ export default function initSettingsPanels() { ...@@ -39,10 +31,19 @@ export default function initSettingsPanels() {
$('.settings').each((i, elm) => { $('.settings').each((i, elm) => {
const $section = $(elm); const $section = $(elm);
$section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section)); $section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section));
$section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section));
if (!$section.hasClass('expanded')) {
$section.find('.settings-content').on('scroll.expandSection', () => {
$section.removeClass('no-animate');
expandSection($section);
});
}
}); });
if (location.hash) { if (location.hash) {
expandSection($(location.hash)); const $target = $(location.hash);
if ($target.length && $target.hasClass('.settings')) {
expandSection($target);
}
} }
} }
import PipelineStage from '../../pipelines/components/stage.vue'; import PipelineStage from '../../pipelines/components/stage.vue';
import ciIcon from '../../vue_shared/components/ci_icon.vue'; import ciIcon from '../../vue_shared/components/ci_icon.vue';
import { statusIconEntityMap } from '../../vue_shared/ci_status_icons'; import icon from '../../vue_shared/components/icon.vue';
export default { export default {
name: 'MRWidgetPipeline', name: 'MRWidgetPipeline',
...@@ -10,6 +10,7 @@ export default { ...@@ -10,6 +10,7 @@ export default {
components: { components: {
'pipeline-stage': PipelineStage, 'pipeline-stage': PipelineStage,
ciIcon, ciIcon,
icon,
}, },
computed: { computed: {
hasPipeline() { hasPipeline() {
...@@ -20,9 +21,6 @@ export default { ...@@ -20,9 +21,6 @@ export default {
return hasCI && !ciStatus; return hasCI && !ciStatus;
}, },
svg() {
return statusIconEntityMap.icon_status_failed;
},
stageText() { stageText() {
return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage'; return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage';
}, },
...@@ -38,8 +36,10 @@ export default { ...@@ -38,8 +36,10 @@ export default {
<template v-if="hasCIError"> <template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10"> <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
<span <span
v-html="svg" aria-hidden="true">
aria-hidden="true"></span> <icon
name="status_failed"/>
</span>
</div> </div>
<div class="media-body"> <div class="media-body">
Could not connect to the CI server. Please check your settings and try again Could not connect to the CI server. Please check your settings and try again
......
import cancelSVG from 'icons/_icon_action_cancel.svg';
import retrySVG from 'icons/_icon_action_retry.svg';
import playSVG from 'icons/_icon_action_play.svg';
import stopSVG from 'icons/_icon_action_stop.svg';
/**
* For the provided action returns the respective SVG
*
* @param {String} action
* @return {SVG|String}
*/
export default function getActionIcon(action) {
const icons = {
icon_action_cancel: cancelSVG,
icon_action_play: playSVG,
icon_action_retry: retrySVG,
icon_action_stop: stopSVG,
};
return icons[action] || '';
}
import BORDERLESS_CANCELED_SVG from 'icons/_icon_status_canceled_borderless.svg';
import BORDERLESS_CREATED_SVG from 'icons/_icon_status_created_borderless.svg';
import BORDERLESS_FAILED_SVG from 'icons/_icon_status_failed_borderless.svg';
import BORDERLESS_MANUAL_SVG from 'icons/_icon_status_manual_borderless.svg';
import BORDERLESS_PENDING_SVG from 'icons/_icon_status_pending_borderless.svg';
import BORDERLESS_RUNNING_SVG from 'icons/_icon_status_running_borderless.svg';
import BORDERLESS_SKIPPED_SVG from 'icons/_icon_status_skipped_borderless.svg';
import BORDERLESS_SUCCESS_SVG from 'icons/_icon_status_success_borderless.svg';
import BORDERLESS_WARNING_SVG from 'icons/_icon_status_warning_borderless.svg';
import CANCELED_SVG from 'icons/_icon_status_canceled.svg';
import CREATED_SVG from 'icons/_icon_status_created.svg';
import FAILED_SVG from 'icons/_icon_status_failed.svg';
import MANUAL_SVG from 'icons/_icon_status_manual.svg';
import PENDING_SVG from 'icons/_icon_status_pending.svg';
import RUNNING_SVG from 'icons/_icon_status_running.svg';
import SKIPPED_SVG from 'icons/_icon_status_skipped.svg';
import SUCCESS_SVG from 'icons/_icon_status_success.svg';
import WARNING_SVG from 'icons/_icon_status_warning.svg';
export const borderlessStatusIconEntityMap = {
icon_status_canceled: BORDERLESS_CANCELED_SVG,
icon_status_created: BORDERLESS_CREATED_SVG,
icon_status_failed: BORDERLESS_FAILED_SVG,
icon_status_manual: BORDERLESS_MANUAL_SVG,
icon_status_pending: BORDERLESS_PENDING_SVG,
icon_status_running: BORDERLESS_RUNNING_SVG,
icon_status_skipped: BORDERLESS_SKIPPED_SVG,
icon_status_success: BORDERLESS_SUCCESS_SVG,
icon_status_warning: BORDERLESS_WARNING_SVG,
};
export const statusIconEntityMap = {
icon_status_canceled: CANCELED_SVG,
icon_status_created: CREATED_SVG,
icon_status_failed: FAILED_SVG,
icon_status_manual: MANUAL_SVG,
icon_status_pending: PENDING_SVG,
icon_status_running: RUNNING_SVG,
icon_status_skipped: SKIPPED_SVG,
icon_status_success: SUCCESS_SVG,
icon_status_warning: WARNING_SVG,
};
...@@ -43,7 +43,6 @@ ...@@ -43,7 +43,6 @@
computed: { computed: {
cssClass() { cssClass() {
const className = this.status.group; const className = this.status.group;
return className ? `ci-status ci-${className}` : 'ci-status'; return className ? `ci-status ci-${className}` : 'ci-status';
}, },
}, },
......
<script> <script>
import { statusIconEntityMap } from '../ci_status_icons'; import icon from '../../vue_shared/components/icon.vue';
/** /**
* Renders CI icon based on API response shared between all places where it is used. * Renders CI icon based on API response shared between all places where it is used.
...@@ -30,11 +30,11 @@ ...@@ -30,11 +30,11 @@
}, },
}, },
computed: { components: {
statusIconSvg() { icon,
return statusIconEntityMap[this.status.icon];
}, },
computed: {
cssClass() { cssClass() {
const status = this.status.group; const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`; return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
...@@ -44,7 +44,8 @@ ...@@ -44,7 +44,8 @@
</script> </script>
<template> <template>
<span <span
:class="cssClass" :class="cssClass">
v-html="statusIconSvg"> <icon
:name="status.icon"/>
</span> </span>
</template> </template>
<script>
/* This is a re-usable vue component for rendering a svg sprite
icon
Sample configuration:
<icon
:img-src="userAvatarSrc"
:img-alt="tooltipText"
:tooltip-text="tooltipText"
tooltip-placement="top"
/>
*/
export default {
props: {
name: {
type: String,
required: true,
},
size: {
type: Number,
required: false,
default: 0,
},
cssClasses: {
type: String,
required: false,
default: '',
},
},
computed: {
spriteHref() {
return `${gon.sprite_icons}#${this.name}`;
},
iconSizeClass() {
return this.size ? `s${this.size}` : '';
},
},
};
</script>
<template>
<svg
:class="[iconSizeClass, cssClasses]">
<use
v-bind="{'xlink:href':spriteHref}"/>
</svg>
</template>
...@@ -56,4 +56,4 @@ ...@@ -56,4 +56,4 @@
@import "framework/icons"; @import "framework/icons";
@import "framework/snippets"; @import "framework/snippets";
@import "framework/memory_graph"; @import "framework/memory_graph";
@import "framework/responsive-tables"; @import "framework/responsive_tables";
...@@ -23,11 +23,16 @@ ...@@ -23,11 +23,16 @@
@include webkit-prefix(animation-duration, 2s); @include webkit-prefix(animation-duration, 2s);
} }
&.spin { &.spin-cw {
transform-origin: center; transform-origin: center;
animation: spin 4s linear infinite; animation: spin 4s linear infinite;
} }
&.spin-ccw {
transform-origin: center;
animation: spin 4s linear infinite reverse;
}
&.flipOutX, &.flipOutX,
&.flipOutY, &.flipOutY,
&.bounceIn, &.bounceIn,
......
...@@ -209,7 +209,6 @@ ...@@ -209,7 +209,6 @@
padding: 24px 0 0; padding: 24px 0 0;
.nav-links { .nav-links {
justify-content: center;
width: 100%; width: 100%;
float: none; float: none;
...@@ -217,6 +216,14 @@ ...@@ -217,6 +216,14 @@
float: none; float: none;
} }
} }
li:first-child {
margin-left: auto;
}
li:last-child {
margin-right: auto;
}
} }
.group-info { .group-info {
......
...@@ -5,32 +5,6 @@ ...@@ -5,32 +5,6 @@
.cgreen { color: $common-green; } .cgreen { color: $common-green; }
.cdark { color: $common-gray-dark; } .cdark { color: $common-gray-dark; }
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-5 { margin-top: 5px; }
.prepend-top-10 { margin-top: 10px; }
.prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top: 20px; }
.prepend-left-4 { margin-left: 4px; }
.prepend-left-5 { margin-left: 5px; }
.prepend-left-10 { margin-left: 10px; }
.prepend-left-default { margin-left: $gl-padding; }
.prepend-left-20 { margin-left: 20px; }
.append-right-5 { margin-right: 5px; }
.append-right-8 { margin-right: 8px; }
.append-right-10 { margin-right: 10px; }
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; }
.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; }
.append-bottom-default { margin-bottom: $gl-padding; }
.inline { display: inline-block; }
.center { text-align: center; }
.vertical-align-middle { vertical-align: middle; }
.underlined-link { text-decoration: underline; } .underlined-link { text-decoration: underline; }
.hint { font-style: italic; color: $hint-color; } .hint { font-style: italic; color: $hint-color; }
.light { color: $common-gray; } .light { color: $common-gray; }
...@@ -448,3 +422,30 @@ table { ...@@ -448,3 +422,30 @@ table {
pointer-events: none; pointer-events: none;
opacity: .5; opacity: .5;
} }
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-5 { margin-top: 5px; }
.prepend-top-10 { margin-top: 10px; }
.prepend-top-15 { margin-top: 15px; }
.prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top: 20px; }
.prepend-left-4 { margin-left: 4px; }
.prepend-left-5 { margin-left: 5px; }
.prepend-left-10 { margin-left: 10px; }
.prepend-left-default { margin-left: $gl-padding; }
.prepend-left-20 { margin-left: 20px; }
.append-right-5 { margin-right: 5px; }
.append-right-8 { margin-right: 8px; }
.append-right-10 { margin-right: 10px; }
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; }
.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; }
.append-bottom-default { margin-bottom: $gl-padding; }
.inline { display: inline-block; }
.center { text-align: center; }
.vertical-align-middle { vertical-align: middle; }
...@@ -3,23 +3,40 @@ ...@@ -3,23 +3,40 @@
max-width: #{$max + '%'}; max-width: #{$max + '%'};
} }
.gl-responsive-table-row-layout {
width: 100%;
@media (min-width: $screen-md-min) {
display: flex;
align-items: center;
& > &:not(:first-child) {
margin-top: $gl-padding;
}
}
}
.gl-responsive-table-row { .gl-responsive-table-row {
@extend .gl-responsive-table-row-layout;
margin-top: 10px; margin-top: 10px;
border: 1px solid $border-color; border: 1px solid $border-color;
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
padding: 15px 0;
margin: 0; margin: 0;
display: flex; padding: $gl-padding 0;
align-items: center;
border: none; border: none;
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $white-normal;
} }
}
.table-section { .gl-responsive-table-row-col-span {
flex-wrap: wrap;
}
.table-section {
white-space: nowrap; white-space: nowrap;
$section-widths: 10 15 20 25 30 40; $section-widths: 10 15 20 25 30 40 100;
@each $width in $section-widths { @each $width in $section-widths {
&.section-#{$width} { &.section-#{$width} {
flex: 0 0 #{$width + '%'}; flex: 0 0 #{$width + '%'};
...@@ -30,7 +47,6 @@ ...@@ -30,7 +47,6 @@
} }
} }
&:not(.table-button-footer) {
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
display: flex; display: flex;
align-self: stretch; align-self: stretch;
...@@ -38,11 +54,10 @@ ...@@ -38,11 +54,10 @@
align-items: center; align-items: center;
min-height: 62px; min-height: 62px;
&:not(:first-of-type) { &:not(:first-child) {
border-top: 1px solid $white-normal; border-top: 1px solid $white-normal;
} }
} }
}
&.section-wrap { &.section-wrap {
white-space: normal; white-space: normal;
...@@ -51,22 +66,25 @@ ...@@ -51,22 +66,25 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
} }
&.section-align-top {
align-self: flex-start;
} }
} }
.table-button-footer { .table-button-footer {
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
text-align: right; text-align: right;
} }
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
background-color: $gray-normal; display: block;
align-self: stretch; align-self: stretch;
min-height: 0;
background-color: $gray-normal;
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
.table-action-buttons { .table-action-buttons {
padding: 10px 5px;
display: flex; display: flex;
.btn { .btn {
...@@ -77,7 +95,14 @@ ...@@ -77,7 +95,14 @@
> .external-url, > .external-url,
> .btn { > .btn {
flex: 1 1 28px; flex: 1 1 28px;
margin: 0 5px;
&:not(:first-child) {
margin-left: 5px;
}
&:not(:last-child) {
margin-right: 5px;
}
} }
.dropdown-new { .dropdown-new {
......
...@@ -333,8 +333,10 @@ ...@@ -333,8 +333,10 @@
svg { svg {
position: relative; position: relative;
top: 2px; top: 3px;
margin-right: 3px; margin-right: 3px;
width: 14px;
height: 14px;
} }
} }
...@@ -348,9 +350,10 @@ ...@@ -348,9 +350,10 @@
svg { svg {
position: relative; position: relative;
top: 2px; top: 3px;
margin-right: 3px; margin-right: 3px;
height: 13px; height: 14px;
width: 14px;
} }
a { a {
...@@ -369,7 +372,7 @@ ...@@ -369,7 +372,7 @@
.build-job { .build-job {
position: relative; position: relative;
.fa-arrow-right { .icon-arrow-right {
position: absolute; position: absolute;
left: 15px; left: 15px;
top: 20px; top: 20px;
...@@ -379,7 +382,7 @@ ...@@ -379,7 +382,7 @@
&.active { &.active {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
.fa-arrow-right { .icon-arrow-right {
display: block; display: block;
} }
} }
...@@ -392,8 +395,7 @@ ...@@ -392,8 +395,7 @@
background-color: $row-hover; background-color: $row-hover;
} }
.fa-refresh { .icon-retry {
font-size: 13px;
margin-left: 3px; margin-left: 3px;
} }
} }
......
...@@ -2,8 +2,4 @@ ...@@ -2,8 +2,4 @@
.clipboard-addon { .clipboard-addon {
background-color: $white-light; background-color: $white-light;
} }
.alert-block {
margin-bottom: 10px;
}
} }
...@@ -133,12 +133,11 @@ ...@@ -133,12 +133,11 @@
} }
.folder-row { .folder-row {
padding: 15px 0; border-left: none;
border-bottom: 1px solid $white-normal; border-right: none;
@media (max-width: $screen-sm-max) { @media (min-width: $screen-sm-max) {
border-top: 1px solid $white-normal; border-top: none;
margin-top: 10px;
} }
} }
......
...@@ -165,8 +165,9 @@ ...@@ -165,8 +165,9 @@
z-index: 300; z-index: 300;
} }
.ci-action-icon-wrapper { .ci-action-icon-wrapper svg {
line-height: 16px; width: 16px;
height: 16px;
} }
} }
......
...@@ -31,7 +31,6 @@ ...@@ -31,7 +31,6 @@
} }
.pipeline-actions { .pipeline-actions {
padding-right: 0;
min-width: 170px; //Guarantees buttons don't break in several lines. min-width: 170px; //Guarantees buttons don't break in several lines.
.btn-default { .btn-default {
...@@ -452,7 +451,7 @@ ...@@ -452,7 +451,7 @@
} }
// Action Icons in big pipeline-graph nodes // Action Icons in big pipeline-graph nodes
.ci-action-icon-container .ci-action-icon-wrapper { .ci-action-icon-container.ci-action-icon-wrapper {
height: 30px; height: 30px;
width: 30px; width: 30px;
background: $white-light; background: $white-light;
...@@ -468,8 +467,18 @@ ...@@ -468,8 +467,18 @@
svg { svg {
fill: $gl-text-color-secondary; fill: $gl-text-color-secondary;
position: relative; position: relative;
left: -1px; left: 5px;
top: -1px; top: 2px;
width: 18px;
height: 18px;
}
&.play {
svg {
width: #{$ci-action-icon-size - 8};
height: #{$ci-action-icon-size - 8};
left: 8px;
}
} }
&:hover svg { &:hover svg {
...@@ -721,17 +730,49 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -721,17 +730,49 @@ button.mini-pipeline-graph-dropdown-toggle {
svg { svg {
fill: $gl-text-color-secondary; fill: $gl-text-color-secondary;
width: $ci-action-icon-size; width: #{$ci-action-icon-size - 6};
height: $ci-action-icon-size; height: #{$ci-action-icon-size - 6};
left: -6px; left: -3px;
position: relative; position: relative;
top: -3px; top: -2px;
} }
&:hover svg, &:hover svg,
&:focus svg { &:focus svg {
fill: $gl-text-color; fill: $gl-text-color;
} }
&.icon-action-retry,
&.icon-action-play {
svg {
width: #{$ci-action-icon-size - 6};
height: #{$ci-action-icon-size - 6};
left: 8px;
}
}
svg.icon-action-stop,
svg.icon-action-cancel {
width: 12px;
height: 12px;
top: 1px;
left: -1px;
}
svg.icon-action-play {
width: 11px;
height: 11px;
top: 1px;
left: 1px;
}
svg.icon-action-retry {
width: 16px;
height: 16px;
top: 0;
left: -3px;
}
} }
// link to the build // link to the build
......
...@@ -50,3 +50,10 @@ ...@@ -50,3 +50,10 @@
font-size: 11px; font-size: 11px;
} }
} }
@media (max-width: $screen-md-max) {
.runners-content {
width: 100%;
overflow: auto;
}
}
...@@ -23,15 +23,14 @@ ...@@ -23,15 +23,14 @@
} }
.settings { .settings {
overflow: hidden;
border-bottom: 1px solid $gray-darker; border-bottom: 1px solid $gray-darker;
&:first-of-type { &:first-of-type {
margin-top: 10px; margin-top: 10px;
} }
&.expanded { &.animating {
overflow: visible; overflow: hidden;
} }
} }
...@@ -56,14 +55,18 @@ ...@@ -56,14 +55,18 @@
overflow-y: scroll; overflow-y: scroll;
padding-right: 110px; padding-right: 110px;
animation: collapseMaxHeight 300ms ease-out; animation: collapseMaxHeight 300ms ease-out;
// Keep the section from expanding when we scroll over it
pointer-events: none;
&.expanded { .settings.expanded & {
max-height: none; max-height: none;
overflow-y: visible; overflow-y: visible;
animation: expandMaxHeight 300ms ease-in; animation: expandMaxHeight 300ms ease-in;
// Reset and allow clicks again when expanded
pointer-events: auto;
} }
&.no-animate { .settings.no-animate & {
animation: none; animation: none;
} }
......
...@@ -44,7 +44,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController ...@@ -44,7 +44,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
end end
def set_index_vars def set_index_vars
@scopes = Gitlab::Auth::API_SCOPES @scopes = Gitlab::Auth.available_scopes(current_user)
@impersonation_token ||= finder.build @impersonation_token ||= finder.build
@inactive_impersonation_tokens = finder(state: 'inactive').execute @inactive_impersonation_tokens = finder(state: 'inactive').execute
......
...@@ -11,7 +11,7 @@ class ApplicationController < ActionController::Base ...@@ -11,7 +11,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication include EnforcesTwoFactorAuthentication
include WithPerformanceBar include WithPerformanceBar
before_action :authenticate_user_from_private_token! before_action :authenticate_user_from_personal_access_token!
before_action :authenticate_user_from_rss_token! before_action :authenticate_user_from_rss_token!
before_action :authenticate_user! before_action :authenticate_user!
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
...@@ -100,13 +100,12 @@ class ApplicationController < ActionController::Base ...@@ -100,13 +100,12 @@ class ApplicationController < ActionController::Base
return try(:authenticated_user) return try(:authenticated_user)
end end
# This filter handles both private tokens and personal access tokens def authenticate_user_from_personal_access_token!
def authenticate_user_from_private_token!
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
return unless token.present? return unless token.present?
user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) user = User.find_by_personal_access_token(token)
sessionless_sign_in(user) sessionless_sign_in(user)
end end
......
...@@ -7,6 +7,54 @@ module IssuableActions ...@@ -7,6 +7,54 @@ module IssuableActions
before_action :authorize_admin_issuable!, only: :bulk_update before_action :authorize_admin_issuable!, only: :bulk_update
end end
def show
respond_to do |format|
format.html do
render show_view
end
format.json do
render json: serializer.represent(issuable, serializer: params[:serializer])
end
end
end
def update
@issuable = update_service.execute(issuable)
respond_to do |format|
format.html do
recaptcha_check_with_fallback { render :edit }
end
format.json do
render_entity_json
end
end
rescue ActiveRecord::StaleObjectError
render_conflict_response
end
def realtime_changes
Gitlab::PollingInterval.set_header(response, interval: 3_000)
response = {
title: view_context.markdown_field(issuable, :title),
title_text: issuable.title,
description: view_context.markdown_field(issuable, :description),
description_text: issuable.description,
task_status: issuable.task_status
}
if issuable.edited?
response[:updated_at] = issuable.updated_at
response[:updated_by_name] = issuable.last_edited_by.name
response[:updated_by_path] = user_path(issuable.last_edited_by)
end
render json: response
end
def destroy def destroy
issuable.destroy issuable.destroy
destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym
...@@ -68,6 +116,10 @@ module IssuableActions ...@@ -68,6 +116,10 @@ module IssuableActions
end end
end end
def authorize_update_issuable!
render_404 unless can?(current_user, :"update_#{resource_name}", issuable)
end
def bulk_update_params def bulk_update_params
permitted_keys = [ permitted_keys = [
:issuable_ids, :issuable_ids,
...@@ -92,4 +144,24 @@ module IssuableActions ...@@ -92,4 +144,24 @@ module IssuableActions
def resource_name def resource_name
@resource_name ||= controller_name.singularize @resource_name ||= controller_name.singularize
end end
def render_entity_json
if @issuable.valid?
render json: serializer.represent(@issuable)
else
render json: { errors: @issuable.errors.full_messages }, status: :unprocessable_entity
end
end
def show_view
'show'
end
def serializer
raise NotImplementedError
end
def update_service
raise NotImplementedError
end
end end
...@@ -30,11 +30,11 @@ class JwtController < ApplicationController ...@@ -30,11 +30,11 @@ class JwtController < ApplicationController
render_unauthorized render_unauthorized
end end
end end
rescue Gitlab::Auth::MissingPersonalTokenError rescue Gitlab::Auth::MissingPersonalAccessTokenError
render_missing_personal_token render_missing_personal_access_token
end end
def render_missing_personal_token def render_missing_personal_access_token
render json: { render json: {
errors: [ errors: [
{ code: 'UNAUTHORIZED', { code: 'UNAUTHORIZED',
......
...@@ -39,7 +39,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController ...@@ -39,7 +39,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end end
def set_index_vars def set_index_vars
@scopes = Gitlab::Auth.available_scopes @scopes = Gitlab::Auth.available_scopes(current_user)
@inactive_personal_access_tokens = finder(state: 'inactive').execute @inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at) @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
......
...@@ -24,16 +24,6 @@ class ProfilesController < Profiles::ApplicationController ...@@ -24,16 +24,6 @@ class ProfilesController < Profiles::ApplicationController
end end
end end
def reset_private_token
Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_authentication_token!
end
flash[:notice] = "Private token was successfully reset"
redirect_to profile_account_path
end
def reset_incoming_email_token def reset_incoming_email_token
Users::UpdateService.new(current_user, user: @user).execute! do |user| Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_incoming_email_token! user.reset_incoming_email_token!
...@@ -41,7 +31,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -41,7 +31,7 @@ class ProfilesController < Profiles::ApplicationController
flash[:notice] = "Incoming email token was successfully reset" flash[:notice] = "Incoming email token was successfully reset"
redirect_to profile_account_path redirect_to profile_personal_access_tokens_path
end end
def reset_rss_token def reset_rss_token
...@@ -51,7 +41,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -51,7 +41,7 @@ class ProfilesController < Profiles::ApplicationController
flash[:notice] = "RSS token was successfully reset" flash[:notice] = "RSS token was successfully reset"
redirect_to profile_account_path redirect_to profile_personal_access_tokens_path
end end
def audit_log def audit_log
......
...@@ -53,8 +53,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -53,8 +53,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
send_challenges send_challenges
render plain: "HTTP Basic: Access denied\n", status: 401 render plain: "HTTP Basic: Access denied\n", status: 401
rescue Gitlab::Auth::MissingPersonalTokenError rescue Gitlab::Auth::MissingPersonalAccessTokenError
render_missing_personal_token render_missing_personal_access_token
end end
def basic_auth_provided? def basic_auth_provided?
...@@ -78,7 +78,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -78,7 +78,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
@project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}") @project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}")
end end
def render_missing_personal_token def render_missing_personal_access_token
render plain: "HTTP Basic: Access denied\n" \ render plain: "HTTP Basic: Access denied\n" \
"You must use a personal access token with 'api' scope for Git over HTTP.\n" \ "You must use a personal access token with 'api' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}", "You can generate one at #{profile_personal_access_tokens_url}",
......
...@@ -12,12 +12,7 @@ class Projects::GroupLinksController < Projects::ApplicationController ...@@ -12,12 +12,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
if group if group
return render_404 unless can?(current_user, :read_group, group) return render_404 unless can?(current_user, :read_group, group)
Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group)
project.project_group_links.create(
group: group,
group_access: params[:link_group_access],
expires_at: params[:expires_at]
)
else else
flash[:alert] = 'Please select a group.' flash[:alert] = 'Please select a group.'
end end
...@@ -32,7 +27,9 @@ class Projects::GroupLinksController < Projects::ApplicationController ...@@ -32,7 +27,9 @@ class Projects::GroupLinksController < Projects::ApplicationController
end end
def destroy def destroy
project.project_group_links.find(params[:id]).destroy group_link = project.project_group_links.find(params[:id])
::Projects::GroupLinks::DestroyService.new(project, current_user).execute(group_link)
respond_to do |format| respond_to do |format|
format.html do format.html do
...@@ -47,4 +44,8 @@ class Projects::GroupLinksController < Projects::ApplicationController ...@@ -47,4 +44,8 @@ class Projects::GroupLinksController < Projects::ApplicationController
def group_link_params def group_link_params
params.require(:group_link).permit(:group_access, :expires_at) params.require(:group_link).permit(:group_access, :expires_at)
end end
def group_link_create_params
params.permit(:link_group_access, :expires_at)
end
end end
...@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
# Allow modify issue # Allow modify issue
before_action :authorize_update_issue!, only: [:edit, :update, :move] before_action :authorize_update_issuable!, only: [:edit, :update, :move]
# Allow create a new branch and empty WIP merge request from current issue # Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request] before_action :authorize_create_merge_request!, only: [:create_merge_request]
...@@ -67,18 +67,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -67,18 +67,6 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with(@issue) respond_with(@issue)
end end
def show
@noteable = @issue
@note = @project.notes.new(noteable: @issue)
respond_to do |format|
format.html
format.json do
render json: serializer.represent(@issue, serializer: params[:serializer])
end
end
end
def discussions def discussions
notes = @issue.notes notes = @issue.notes
.inc_relations_for_view .inc_relations_for_view
...@@ -120,25 +108,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -120,25 +108,6 @@ class Projects::IssuesController < Projects::ApplicationController
end end
end end
def update
update_params = issue_params.merge(spammable_params)
@issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
respond_to do |format|
format.html do
recaptcha_check_with_fallback { render :edit }
end
format.json do
render_issue_json
end
end
rescue ActiveRecord::StaleObjectError
render_conflict_response
end
def move def move
params.require(:move_to_project_id) params.require(:move_to_project_id)
...@@ -196,26 +165,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -196,26 +165,6 @@ class Projects::IssuesController < Projects::ApplicationController
end end
end end
def realtime_changes
Gitlab::PollingInterval.set_header(response, interval: 3_000)
response = {
title: view_context.markdown_field(@issue, :title),
title_text: @issue.title,
description: view_context.markdown_field(@issue, :description),
description_text: @issue.description,
task_status: @issue.task_status
}
if @issue.edited?
response[:updated_at] = @issue.updated_at
response[:updated_by_name] = @issue.last_edited_by.name
response[:updated_by_path] = user_path(@issue.last_edited_by)
end
render json: response
end
def create_merge_request def create_merge_request
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
...@@ -231,7 +180,8 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -231,7 +180,8 @@ class Projects::IssuesController < Projects::ApplicationController
def issue def issue
return @issue if defined?(@issue) return @issue if defined?(@issue)
# The Sortable default scope causes performance issues when used with find_by # The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take! @issuable = @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
@note = @project.notes.new(noteable: @issuable)
return render_404 unless can?(current_user, :read_issue, @issue) return render_404 unless can?(current_user, :read_issue, @issue)
...@@ -246,14 +196,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -246,14 +196,6 @@ class Projects::IssuesController < Projects::ApplicationController
project_issue_path(@project, @issue) project_issue_path(@project, @issue)
end end
def authorize_update_issue!
render_404 unless can?(current_user, :update_issue, @issue)
end
def authorize_admin_issues!
render_404 unless can?(current_user, :admin_issue, @project)
end
def authorize_create_merge_request! def authorize_create_merge_request!
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
end end
...@@ -305,4 +247,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -305,4 +247,9 @@ class Projects::IssuesController < Projects::ApplicationController
def serializer def serializer
IssueSerializer.new(current_user: current_user, project: issue.project) IssueSerializer.new(current_user: current_user, project: issue.project)
end end
def update_service
update_params = issue_params.merge(spammable_params)
Issues::UpdateService.new(project, current_user, update_params)
end
end end
...@@ -9,7 +9,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -9,7 +9,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
skip_before_action :merge_request, only: [:index, :bulk_update] skip_before_action :merge_request, only: [:index, :bulk_update]
skip_before_action :ensure_ref_fetched, only: [:index, :bulk_update] skip_before_action :ensure_ref_fetched, only: [:index, :bulk_update]
before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort] before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :authenticate_user!, only: [:assign_related_issues] before_action :authenticate_user!, only: [:assign_related_issues]
...@@ -256,14 +256,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -256,14 +256,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
alias_method :issuable, :merge_request alias_method :issuable, :merge_request
alias_method :awardable, :merge_request alias_method :awardable, :merge_request
def authorize_update_merge_request!
return render_404 unless can?(current_user, :update_merge_request, @merge_request)
end
def authorize_admin_merge_request!
return render_404 unless can?(current_user, :admin_merge_request, @merge_request)
end
def validates_merge_request def validates_merge_request
# Show git not found page # Show git not found page
# if there is no saved commits between source & target branch # if there is no saved commits between source & target branch
......
...@@ -63,34 +63,34 @@ module CiStatusHelper ...@@ -63,34 +63,34 @@ module CiStatusHelper
def ci_icon_for_status(status) def ci_icon_for_status(status)
if detailed_status?(status) if detailed_status?(status)
return custom_icon(status.icon) return sprite_icon(status.icon)
end end
icon_name = icon_name =
case status case status
when 'success' when 'success'
'icon_status_success' 'status_success'
when 'success_with_warnings' when 'success_with_warnings'
'icon_status_warning' 'status_warning'
when 'failed' when 'failed'
'icon_status_failed' 'status_failed'
when 'pending' when 'pending'
'icon_status_pending' 'status_pending'
when 'running' when 'running'
'icon_status_running' 'status_running'
when 'play' when 'play'
'icon_play' 'play'
when 'created' when 'created'
'icon_status_created' 'status_created'
when 'skipped' when 'skipped'
'icon_status_skipped' 'status_skipped'
when 'manual' when 'manual'
'icon_status_manual' 'status_manual'
else else
'icon_status_canceled' 'status_canceled'
end end
custom_icon(icon_name) sprite_icon(icon_name, size: 16)
end end
def pipeline_status_cache_key(pipeline_status) def pipeline_status_cache_key(pipeline_status)
......
...@@ -71,11 +71,13 @@ module GitlabRoutingHelper ...@@ -71,11 +71,13 @@ module GitlabRoutingHelper
project_commit_url(entity.project, entity.sha, *args) project_commit_url(entity.project, entity.sha, *args)
end end
def preview_markdown_path(project, *args) def preview_markdown_path(parent, *args)
return group_preview_markdown_path(parent) if parent.is_a?(Group)
if @snippet.is_a?(PersonalSnippet) if @snippet.is_a?(PersonalSnippet)
preview_markdown_snippets_path preview_markdown_snippets_path
else else
preview_markdown_project_path(project, *args) preview_markdown_project_path(parent, *args)
end end
end end
......
...@@ -211,15 +211,13 @@ module IssuablesHelper ...@@ -211,15 +211,13 @@ module IssuablesHelper
def issuable_initial_data(issuable) def issuable_initial_data(issuable)
data = { data = {
endpoint: project_issue_path(@project, issuable), endpoint: issuable_path(issuable),
canUpdate: can?(current_user, :update_issue, issuable), canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable),
canDestroy: can?(current_user, :destroy_issue, issuable), canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable),
issuableRef: issuable.to_reference, issuableRef: issuable.to_reference,
markdownPreviewPath: preview_markdown_path(@project), markdownPreviewPath: preview_markdown_path(parent),
markdownDocsPath: help_page_path('user/markdown'), markdownDocsPath: help_page_path('user/markdown'),
issuableTemplates: issuable_templates(issuable), issuableTemplates: issuable_templates(issuable),
projectPath: ref_project.path,
projectNamespace: ref_project.namespace.full_path,
initialTitleHtml: markdown_field(issuable, :title), initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title, initialTitleText: issuable.title,
initialDescriptionHtml: markdown_field(issuable, :description), initialDescriptionHtml: markdown_field(issuable, :description),
...@@ -227,6 +225,12 @@ module IssuablesHelper ...@@ -227,6 +225,12 @@ module IssuablesHelper
initialTaskStatus: issuable.task_status initialTaskStatus: issuable.task_status
} }
if parent.is_a?(Group)
data[:groupPath] = parent.path
else
data.merge!(projectPath: ref_project.path, projectNamespace: ref_project.namespace.full_path)
end
data.merge!(updated_at_by(issuable)) data.merge!(updated_at_by(issuable))
data.to_json data.to_json
...@@ -263,12 +267,7 @@ module IssuablesHelper ...@@ -263,12 +267,7 @@ module IssuablesHelper
end end
def issuable_path(issuable, *options) def issuable_path(issuable, *options)
case issuable polymorphic_path(issuable, *options)
when Issue
issue_path(issuable, *options)
when MergeRequest
merge_request_path(issuable, *options)
end
end end
def issuable_url(issuable, *options) def issuable_url(issuable, *options)
...@@ -369,4 +368,8 @@ module IssuablesHelper ...@@ -369,4 +368,8 @@ module IssuablesHelper
fullPath: @project.full_path fullPath: @project.full_path
} }
end end
def parent
@project || @group
end
end end
...@@ -49,7 +49,8 @@ module CacheMarkdownField ...@@ -49,7 +49,8 @@ module CacheMarkdownField
# Always include a project key, or Banzai complains # Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project) project = self.project if self.respond_to?(:project)
context = cached_markdown_fields[field].merge(project: project) group = self.group if self.respond_to?(:group)
context = cached_markdown_fields[field].merge(project: project, group: group)
# Banzai is less strict about authors, so don't always have an author key # Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author) context[:author] = self.author if self.respond_to?(:author)
......
...@@ -14,7 +14,6 @@ module Issuable ...@@ -14,7 +14,6 @@ module Issuable
include StripAttribute include StripAttribute
include Awardable include Awardable
include Taskable include Taskable
include TimeTrackable
include Importable include Importable
include Editable include Editable
include AfterCommitQueue include AfterCommitQueue
...@@ -95,8 +94,6 @@ module Issuable ...@@ -95,8 +94,6 @@ module Issuable
strip_attributes :title strip_attributes :title
acts_as_paranoid
after_save :record_metrics, unless: :imported? after_save :record_metrics, unless: :imported?
# We want to use optimistic lock for cases when only title or description are involved # We want to use optimistic lock for cases when only title or description are involved
......
# Placeholder class for model that is implemented in EE
# It will reserve (ee#3853) '&' as a reference prefix, but the table does not exists in CE
class Epic < ActiveRecord::Base
# TODO: this will be implemented as part of #3853
def to_reference
end
end
...@@ -180,6 +180,12 @@ class Group < Namespace ...@@ -180,6 +180,12 @@ class Group < Namespace
add_user(user, :owner, current_user: current_user) add_user(user, :owner, current_user: current_user)
end end
def member?(user, min_access_level = Gitlab::Access::GUEST)
return false unless user
max_member_access_for_user(user) >= min_access_level
end
def has_owner?(user) def has_owner?(user)
return false unless user return false unless user
......
...@@ -10,6 +10,7 @@ class Issue < ActiveRecord::Base ...@@ -10,6 +10,7 @@ class Issue < ActiveRecord::Base
include FasterCacheKeys include FasterCacheKeys
include RelativePositioning include RelativePositioning
include CreatedAtFilterable include CreatedAtFilterable
include TimeTrackable
DueDateStruct = Struct.new(:title, :name).freeze DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
...@@ -74,6 +75,8 @@ class Issue < ActiveRecord::Base ...@@ -74,6 +75,8 @@ class Issue < ActiveRecord::Base
end end
end end
acts_as_paranoid
def self.reference_prefix def self.reference_prefix
'#' '#'
end end
......
...@@ -6,6 +6,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -6,6 +6,7 @@ class MergeRequest < ActiveRecord::Base
include Sortable include Sortable
include IgnorableColumn include IgnorableColumn
include CreatedAtFilterable include CreatedAtFilterable
include TimeTrackable
ignore_column :locked_at ignore_column :locked_at
...@@ -119,6 +120,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -119,6 +120,8 @@ class MergeRequest < ActiveRecord::Base
after_save :keep_around_commit after_save :keep_around_commit
acts_as_paranoid
def self.reference_prefix def self.reference_prefix
'!' '!'
end end
......
...@@ -48,6 +48,10 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -48,6 +48,10 @@ class MergeRequestDiff < ActiveRecord::Base
# Collect information about commits and diff from repository # Collect information about commits and diff from repository
# and save it to the database as serialized data # and save it to the database as serialized data
def save_git_content def save_git_content
MergeRequest
.where('id = ? AND COALESCE(latest_merge_request_diff_id, 0) < ?', self.merge_request_id, self.id)
.update_all(latest_merge_request_diff_id: self.id)
ensure_commit_shas ensure_commit_shas
save_commits save_commits
save_diffs save_diffs
......
...@@ -69,7 +69,7 @@ class Note < ActiveRecord::Base ...@@ -69,7 +69,7 @@ class Note < ActiveRecord::Base
delegate :title, to: :noteable, allow_nil: true delegate :title, to: :noteable, allow_nil: true
validates :note, presence: true validates :note, presence: true
validates :project, presence: true, unless: :for_personal_snippet? validates :project, presence: true, if: :for_project_noteable?
# Attachments are deprecated and are handled by Markdown uploader # Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size } validates :attachment, file_size: { maximum: :max_attachment_size }
...@@ -114,7 +114,7 @@ class Note < ActiveRecord::Base ...@@ -114,7 +114,7 @@ class Note < ActiveRecord::Base
after_initialize :ensure_discussion_id after_initialize :ensure_discussion_id
before_validation :nullify_blank_type, :nullify_blank_line_code before_validation :nullify_blank_type, :nullify_blank_line_code
before_validation :set_discussion_id, on: :create before_validation :set_discussion_id, on: :create
after_save :keep_around_commit, unless: :for_personal_snippet? after_save :keep_around_commit, if: :for_project_noteable?
after_save :expire_etag_cache after_save :expire_etag_cache
after_destroy :expire_etag_cache after_destroy :expire_etag_cache
...@@ -208,6 +208,10 @@ class Note < ActiveRecord::Base ...@@ -208,6 +208,10 @@ class Note < ActiveRecord::Base
noteable.is_a?(PersonalSnippet) noteable.is_a?(PersonalSnippet)
end end
def for_project_noteable?
!for_personal_snippet?
end
def skip_project_check? def skip_project_check?
for_personal_snippet? for_personal_snippet?
end end
......
...@@ -2,5 +2,13 @@ class OauthAccessToken < Doorkeeper::AccessToken ...@@ -2,5 +2,13 @@ class OauthAccessToken < Doorkeeper::AccessToken
belongs_to :resource_owner, class_name: 'User' belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application' belongs_to :application, class_name: 'Doorkeeper::Application'
alias_method :user, :resource_owner alias_attribute :user, :resource_owner
def scopes=(value)
if value.is_a?(Array)
super(Doorkeeper::OAuth::Scopes.from_array(value).to_s)
else
super
end
end
end end
...@@ -21,8 +21,8 @@ class User < ActiveRecord::Base ...@@ -21,8 +21,8 @@ class User < ActiveRecord::Base
ignore_column :external_email ignore_column :external_email
ignore_column :email_provider ignore_column :email_provider
ignore_column :authentication_token
add_authentication_token_field :authentication_token
add_authentication_token_field :incoming_email_token add_authentication_token_field :incoming_email_token
add_authentication_token_field :rss_token add_authentication_token_field :rss_token
...@@ -163,7 +163,7 @@ class User < ActiveRecord::Base ...@@ -163,7 +163,7 @@ class User < ActiveRecord::Base
before_validation :sanitize_attrs before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed? before_validation :set_notification_email, if: :email_changed?
before_validation :set_public_email, if: :public_email_changed? before_validation :set_public_email, if: :public_email_changed?
before_save :ensure_authentication_token, :ensure_incoming_email_token before_save :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: :external_changed? before_save :ensure_user_rights_and_limits, if: :external_changed?
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) } before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? } before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
...@@ -185,8 +185,6 @@ class User < ActiveRecord::Base ...@@ -185,8 +185,6 @@ class User < ActiveRecord::Base
# Note: When adding an option, it MUST go on the end of the array. # Note: When adding an option, it MUST go on the end of the array.
enum project_view: [:readme, :activity, :files] enum project_view: [:readme, :activity, :files]
alias_attribute :private_token, :authentication_token
delegate :path, to: :namespace, allow_nil: true, prefix: true delegate :path, to: :namespace, allow_nil: true, prefix: true
state_machine :state, initial: :active do state_machine :state, initial: :active do
......
class IssuableEntity < Grape::Entity class IssuableEntity < Grape::Entity
include RequestAwareEntity
expose :id expose :id
expose :iid expose :iid
expose :author_id expose :author_id
expose :description expose :description
expose :lock_version expose :lock_version
expose :milestone_id expose :milestone_id
expose :state
expose :title expose :title
expose :updated_by_id expose :updated_by_id
expose :created_at expose :created_at
expose :updated_at expose :updated_at
expose :deleted_at
expose :time_estimate
expose :total_time_spent
expose :human_time_estimate
expose :human_total_time_spent
expose :milestone, using: API::Entities::Milestone expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity expose :labels, using: LabelEntity
end end
class IssueEntity < IssuableEntity class IssueEntity < IssuableEntity
include RequestAwareEntity include TimeTrackableEntity
expose :state
expose :deleted_at
expose :branch_name expose :branch_name
expose :confidential expose :confidential
expose :discussion_locked expose :discussion_locked
......
class MergeRequestEntity < IssuableEntity class MergeRequestEntity < IssuableEntity
include RequestAwareEntity include TimeTrackableEntity
expose :state
expose :deleted_at
expose :in_progress_merge_commit_sha expose :in_progress_merge_commit_sha
expose :merge_commit_sha expose :merge_commit_sha
expose :merge_error expose :merge_error
......
module TimeTrackableEntity
extend ActiveSupport::Concern
extend Grape
included do
expose :time_estimate
expose :total_time_spent
expose :human_time_estimate
expose :human_total_time_spent
end
end
...@@ -39,11 +39,8 @@ class AccessTokenValidationService ...@@ -39,11 +39,8 @@ class AccessTokenValidationService
token_scopes = token.scopes.map(&:to_sym) token_scopes = token.scopes.map(&:to_sym)
required_scopes.any? do |scope| required_scopes.any? do |scope|
if scope.respond_to?(:sufficient?) scope = API::Scope.new(scope) unless scope.is_a?(API::Scope)
scope.sufficient?(token_scopes, request) scope.sufficient?(token_scopes, request)
else
API::Scope.new(scope).sufficient?(token_scopes, request)
end
end end
end end
end end
......
module Issuable
class CommonSystemNotesService < ::BaseService
attr_reader :issuable
def execute(issuable, old_labels)
@issuable = issuable
if issuable.previous_changes.include?('title')
create_title_change_note(issuable.previous_changes['title'].first)
end
handle_description_change_note
handle_time_tracking_note if issuable.is_a?(TimeTrackable)
create_labels_note(old_labels) if issuable.labels != old_labels
create_discussion_lock_note if issuable.previous_changes.include?('discussion_locked')
create_milestone_note if issuable.previous_changes.include?('milestone_id')
end
private
def handle_time_tracking_note
if issuable.previous_changes.include?('time_estimate')
create_time_estimate_note
end
if issuable.time_spent?
create_time_spent_note
end
end
def handle_description_change_note
if issuable.previous_changes.include?('description')
if issuable.tasks? && issuable.updated_tasks.any?
create_task_status_note
else
# TODO: Show this note if non-task content was modified.
# https://gitlab.com/gitlab-org/gitlab-ce/issues/33577
create_description_change_note
end
end
end
def create_labels_note(old_labels)
added_labels = issuable.labels - old_labels
removed_labels = old_labels - issuable.labels
SystemNoteService.change_label(issuable, issuable.project, current_user, added_labels, removed_labels)
end
def create_title_change_note(old_title)
SystemNoteService.change_title(issuable, issuable.project, current_user, old_title)
end
def create_description_change_note
SystemNoteService.change_description(issuable, issuable.project, current_user)
end
def create_task_status_note
issuable.updated_tasks.each do |task|
SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
end
end
def create_time_estimate_note
SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
end
def create_time_spent_note
SystemNoteService.change_time_spent(issuable, issuable.project, issuable.time_spent_user)
end
def create_milestone_note
SystemNoteService.change_milestone(issuable, issuable.project, current_user, issuable.milestone)
end
def create_discussion_lock_note
SystemNoteService.discussion_lock(issuable, current_user)
end
end
end
class IssuableBaseService < BaseService class IssuableBaseService < BaseService
private private
def create_milestone_note(issuable)
SystemNoteService.change_milestone(
issuable, issuable.project, current_user, issuable.milestone)
end
def create_labels_note(issuable, old_labels)
added_labels = issuable.labels - old_labels
removed_labels = old_labels - issuable.labels
SystemNoteService.change_label(
issuable, issuable.project, current_user, added_labels, removed_labels)
end
def create_title_change_note(issuable, old_title)
SystemNoteService.change_title(
issuable, issuable.project, current_user, old_title)
end
def create_description_change_note(issuable)
SystemNoteService.change_description(issuable, issuable.project, current_user)
end
def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
SystemNoteService.change_branch(
issuable, issuable.project, current_user, branch_type,
old_branch, new_branch)
end
def create_task_status_note(issuable)
issuable.updated_tasks.each do |task|
SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
end
end
def create_time_estimate_note(issuable)
SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
end
def create_time_spent_note(issuable)
SystemNoteService.change_time_spent(issuable, issuable.project, current_user)
end
def create_discussion_lock_note(issuable)
SystemNoteService.discussion_lock(issuable, current_user)
end
def filter_params(issuable) def filter_params(issuable)
ability_name = :"admin_#{issuable.to_ability_name}" ability_name = :"admin_#{issuable.to_ability_name}"
unless can?(current_user, ability_name, project) unless can?(current_user, ability_name, issuable)
params.delete(:milestone_id) params.delete(:milestone_id)
params.delete(:labels) params.delete(:labels)
params.delete(:add_label_ids) params.delete(:add_label_ids)
...@@ -233,15 +187,14 @@ class IssuableBaseService < BaseService ...@@ -233,15 +187,14 @@ class IssuableBaseService < BaseService
# We have to perform this check before saving the issuable as Rails resets # We have to perform this check before saving the issuable as Rails resets
# the changed fields upon calling #save. # the changed fields upon calling #save.
update_project_counters = issuable.update_project_counter_caches? update_project_counters = issuable.project && issuable.update_project_counter_caches?
if issuable.with_transaction_returning_status { issuable.save } if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field # We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do ActiveRecord::Base.no_touching do
handle_common_system_notes(issuable, old_labels: old_labels) Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels)
end end
change_discussion_lock(issuable)
handle_changes( handle_changes(
issuable, issuable,
old_labels: old_labels, old_labels: old_labels,
...@@ -300,12 +253,6 @@ class IssuableBaseService < BaseService ...@@ -300,12 +253,6 @@ class IssuableBaseService < BaseService
end end
end end
def change_discussion_lock(issuable)
if issuable.previous_changes.include?('discussion_locked')
create_discussion_lock_note(issuable)
end
end
def toggle_award(issuable) def toggle_award(issuable)
award = params.delete(:emoji_award) award = params.delete(:emoji_award)
if award if award
...@@ -328,35 +275,17 @@ class IssuableBaseService < BaseService ...@@ -328,35 +275,17 @@ class IssuableBaseService < BaseService
attrs_changed || labels_changed || assignees_changed attrs_changed || labels_changed || assignees_changed
end end
def handle_common_system_notes(issuable, old_labels: []) def invalidate_cache_counts(issuable, users: [])
if issuable.previous_changes.include?('title') users.each do |user|
create_title_change_note(issuable, issuable.previous_changes['title'].first) user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend
end
if issuable.previous_changes.include?('description')
if issuable.tasks? && issuable.updated_tasks.any?
create_task_status_note(issuable)
else
# TODO: Show this note if non-task content was modified.
# https://gitlab.com/gitlab-org/gitlab-ce/issues/33577
create_description_change_note(issuable)
end
end
if issuable.previous_changes.include?('time_estimate')
create_time_estimate_note(issuable)
end end
if issuable.time_spent?
create_time_spent_note(issuable)
end end
create_labels_note(issuable, old_labels) if issuable.labels != old_labels # override if needed
def handle_changes(issuable, options)
end end
def invalidate_cache_counts(issuable, users: []) # override if needed
users.each do |user| def execute_hooks(issuable, action = 'open', params = {})
user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend
end
end end
end end
...@@ -27,10 +27,6 @@ module Issues ...@@ -27,10 +27,6 @@ module Issues
todo_service.update_issue(issue, current_user, old_mentioned_users) todo_service.update_issue(issue, current_user, old_mentioned_users)
end end
if issue.previous_changes.include?('milestone_id')
create_milestone_note(issue)
end
if issue.assignees != old_assignees if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees) create_assignee_note(issue, old_assignees)
notification_service.reassigned_issue(issue, current_user, old_assignees) notification_service.reassigned_issue(issue, current_user, old_assignees)
......
...@@ -40,10 +40,6 @@ module MergeRequests ...@@ -40,10 +40,6 @@ module MergeRequests
merge_request.target_branch) merge_request.target_branch)
end end
if merge_request.previous_changes.include?('milestone_id')
create_milestone_note(merge_request)
end
if merge_request.previous_changes.include?('assignee_id') if merge_request.previous_changes.include?('assignee_id')
create_assignee_note(merge_request) create_assignee_note(merge_request)
notification_service.reassigned_merge_request(merge_request, current_user) notification_service.reassigned_merge_request(merge_request, current_user)
...@@ -111,5 +107,11 @@ module MergeRequests ...@@ -111,5 +107,11 @@ module MergeRequests
end end
end end
end end
def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
SystemNoteService.change_branch(
issuable, issuable.project, current_user, branch_type,
old_branch, new_branch)
end
end end
end end
...@@ -6,8 +6,7 @@ class MetricsService ...@@ -6,8 +6,7 @@ class MetricsService
Gitlab::HealthChecks::Redis::RedisCheck, Gitlab::HealthChecks::Redis::RedisCheck,
Gitlab::HealthChecks::Redis::CacheCheck, Gitlab::HealthChecks::Redis::CacheCheck,
Gitlab::HealthChecks::Redis::QueuesCheck, Gitlab::HealthChecks::Redis::QueuesCheck,
Gitlab::HealthChecks::Redis::SharedStateCheck, Gitlab::HealthChecks::Redis::SharedStateCheck
Gitlab::HealthChecks::FsShardsCheck
].freeze ].freeze
def prometheus_metrics_text def prometheus_metrics_text
......
module Projects
module GroupLinks
class CreateService < BaseService
def execute(group)
return false unless group
project.project_group_links.create(
group: group,
group_access: params[:link_group_access],
expires_at: params[:expires_at]
)
end
end
end
end
module Projects
module GroupLinks
class DestroyService < BaseService
def execute(group_link)
return false unless group_link
group_link.destroy
end
end
end
end
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
%td %td
= truncate(hook_log.url, length: 50) = truncate(hook_log.url, length: 50)
%td.light %td.light
#{number_with_precision(hook_log.execution_duration, precision: 2)} ms #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
%td.light %td.light
= time_ago_with_tooltip(hook_log.created_at) = time_ago_with_tooltip(hook_log.created_at)
%td %td
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
= hidden_field_tag :namespace_id, params[:namespace_id] = hidden_field_tag :namespace_id, params[:namespace_id]
- namespace = Namespace.find(params[:namespace_id]) - namespace = Namespace.find(params[:namespace_id])
- toggle_text = "#{namespace.kind}: #{namespace.full_path}" - toggle_text = "#{namespace.kind}: #{namespace.full_path}"
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' }) = dropdown_toggle(toggle_text, { toggle: 'dropdown', is_filter: 'true' }, { toggle_class: 'js-namespace-select large' })
.dropdown-menu.dropdown-select.dropdown-menu-align-right .dropdown-menu.dropdown-select.dropdown-menu-align-right
= dropdown_title('Namespaces') = dropdown_title('Namespaces')
= dropdown_filter("Search for Namespace") = dropdown_filter("Search for Namespace")
......
...@@ -115,7 +115,7 @@ ...@@ -115,7 +115,7 @@
= f.label :new_namespace_id, "Namespace", class: 'control-label' = f.label :new_namespace_id, "Namespace", class: 'control-label'
.col-sm-10 .col-sm-10
.dropdown .dropdown
= dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id', show_any: 'false' }, { toggle_class: 'js-namespace-select large' }) = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' })
.dropdown-menu.dropdown-select .dropdown-menu.dropdown-select
= dropdown_title('Namespaces') = dropdown_title('Namespaces')
= dropdown_filter("Search for Namespace") = dropdown_filter("Search for Namespace")
......
...@@ -52,6 +52,7 @@ ...@@ -52,6 +52,7 @@
%br %br
- if @runners.any? - if @runners.any?
.runners-content
.table-holder .table-holder
%table.table %table.table
%thead %thead
...@@ -63,7 +64,7 @@ ...@@ -63,7 +64,7 @@
%th Projects %th Projects
%th Jobs %th Jobs
%th Tags %th Tags
%th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc')) %th Last contact
%th %th
- @runners.each do |runner| - @runners.each do |runner|
......
...@@ -5,9 +5,9 @@ ...@@ -5,9 +5,9 @@
- if link && status.has_details? - if link && status.has_details?
= link_to status.details_path, class: css_classes, title: title do = link_to status.details_path, class: css_classes, title: title do
= custom_icon(status.icon) = sprite_icon(status.icon)
= status.text = status.text
- else - else
%span{ class: css_classes, title: title } %span{ class: css_classes, title: title }
= custom_icon(status.icon) = sprite_icon(status.icon)
= status.text = status.text
...@@ -7,13 +7,13 @@ ...@@ -7,13 +7,13 @@
- if status.has_details? - if status.has_details?
= link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do
%span{ class: klass }= custom_icon(status.icon) %span{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text= subject.name %span.ci-build-text= subject.name
- else - else
.menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } } .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } }
%span{ class: klass }= custom_icon(status.icon) %span{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text= subject.name %span.ci-build-text= subject.name
- if status.has_action? - if status.has_action?
= link_to status.action_path, class: 'ci-action-icon-wrapper js-ci-action-icon', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do = link_to status.action_path, class: "ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
= custom_icon(status.action_icon) = sprite_icon(status.action_icon, css_class: "icon-action-#{status.action_icon}")
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
%li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }> %li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
= link_to todos_filter_path(state: 'pending') do = link_to todos_filter_path(state: 'pending') do
%span %span
To do Todos
%span.badge %span.badge
= number_with_delimiter(todos_pending_count) = number_with_delimiter(todos_pending_count)
%li.todos-done{ class: active_when(params[:state] == 'done') }> %li.todos-done{ class: active_when(params[:state] == 'done') }>
......
- name = label.parameterize
- attribute = name.underscore
.reset-action
%p.cgray
= label_tag name, label, class: "label-light"
= text_field_tag name, current_user.send(attribute), class: 'form-control', readonly: true, onclick: 'this.select()'
%p.help-block
= help_text
.prepend-top-default
= link_to button_label, [:reset, attribute, :profile], method: :put, data: { confirm: 'Are you sure?' }, class: 'btn btn-default private-token'
...@@ -6,22 +6,6 @@ ...@@ -6,22 +6,6 @@
.alert.alert-info .alert.alert-info
Some options are unavailable for LDAP accounts Some options are unavailable for LDAP accounts
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Private Tokens
%p
Keep these tokens secret, anyone with access to them can interact with
GitLab as if they were you.
.col-lg-8.private-tokens-reset
= render partial: 'reset_token', locals: { label: 'Private token', button_label: 'Reset private token', help_text: 'Your private token is used to access the API and Atom feeds without username/password authentication.' }
= render partial: 'reset_token', locals: { label: 'RSS token', button_label: 'Reset RSS token', help_text: 'Your RSS token is used to create urls for personalized RSS feeds.' }
- if incoming_email_token_enabled?
= render partial: 'reset_token', locals: { label: 'Incoming email token', button_label: 'Reset incoming email token', help_text: 'Your incoming email token is used to create new issues by email, and is included in your project-specific email addresses.' }
%hr
.row.prepend-top-default .row.prepend-top-default
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
......
...@@ -30,3 +30,40 @@ ...@@ -30,3 +30,40 @@
= render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes = render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes
= render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens = render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens
%hr
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
RSS token
%p
Your RSS token is used to authenticate you when your RSS reader loads a personalized RSS feed, and is included in your personal RSS feed URLs.
%p
It cannot be used to access any other data.
.col-lg-8.rss-token-reset
= label_tag :rss_token, 'RSS token', class: "label-light"
= text_field_tag :rss_token, current_user.rss_token, class: 'form-control', readonly: true, onclick: 'this.select()'
%p.help-block
Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds as if they were you.
You should
= link_to 'reset it', [:reset, :rss_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS URLs currently in use will stop working.' }
if that ever happens.
- if incoming_email_token_enabled?
%hr
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Incoming email token
%p
Your incoming email token is used to authenticate you when you create a new issue by email, and is included in your personal project-specific email addresses.
%p
It cannot be used to access any other data.
.col-lg-8.incoming-email-token-reset
= label_tag :incoming_email_token, 'Incoming email token', class: "label-light"
= text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control', readonly: true, onclick: 'this.select()'
%p.help-block
Keep this token secret. Anyone who gets ahold of it can create issues as if they were you.
You should
= link_to 'reset it', [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: 'Are you sure? Any issue email addresses currently in use will stop working.' }
if that ever happens.
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- project = local_assigns.fetch(:project) - project = local_assigns.fetch(:project)
- expanded = Rails.env.test? - expanded = Rails.env.test?
%section.settings %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
Export project Export project
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
.bs-callout.bs-callout-info .bs-callout.bs-callout-info
%p.append-bottom-0 %p.append-bottom-0
%p %p
......
- expanded = Rails.env.test? - expanded = Rails.env.test?
%section.settings %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
Deploy Keys Deploy Keys
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
%h5.prepend-top-0 %h5.prepend-top-0
Create a new deploy key for this project Create a new deploy key for this project
= render @deploy_keys.form_partial_path = render @deploy_keys.form_partial_path
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
- expanded = Rails.env.test? - expanded = Rails.env.test?
.project-edit-container .project-edit-container
%section.settings.general-settings %section.settings.general-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
General project settings General project settings
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Update your project name, description, avatar, and other general settings. Update your project name, description, avatar, and other general settings.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
.project-edit-errors .project-edit-errors
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f| = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f|
%fieldset %fieldset
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
= link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
= f.submit 'Save changes', class: "btn btn-save" = f.submit 'Save changes', class: "btn btn-save"
%section.settings.sharing-permissions %section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
Permissions Permissions
...@@ -69,13 +69,13 @@ ...@@ -69,13 +69,13 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Enable or disable certain project features and choose access levels. Enable or disable certain project features and choose access levels.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
%script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project) %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project)
.js-project-permissions-form .js-project-permissions-form
= f.submit 'Save changes', class: "btn btn-save" = f.submit 'Save changes', class: "btn btn-save"
%section.settings.merge-requests-feature{ class: ("hidden" if @project.project_feature.send(:merge_requests_access_level) == 0) } %section.settings.merge-requests-feature.no-animate{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }
.settings-header .settings-header
%h4 %h4
Merge request settings Merge request settings
...@@ -83,14 +83,14 @@ ...@@ -83,14 +83,14 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Customize your merge request restrictions. Customize your merge request restrictions.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f| = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f|
= render 'merge_request_settings', form: f = render 'merge_request_settings', form: f
= f.submit 'Save changes', class: "btn btn-save" = f.submit 'Save changes', class: "btn btn-save"
= render 'export', project: @project = render 'export', project: @project
%section.settings.advanced-settings %section.settings.advanced-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
Advanced settings Advanced settings
...@@ -98,7 +98,7 @@ ...@@ -98,7 +98,7 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project. Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
.sub-section .sub-section
%h4 Housekeeping %h4 Housekeeping
%p %p
......
- @no_container = true - @no_container = true
- page_title "Contributors" - page_title _('Contributors')
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag('common_d3') = webpack_bundle_tag('common_d3')
= webpack_bundle_tag('graphs') = webpack_bundle_tag('graphs')
...@@ -7,23 +7,23 @@ ...@@ -7,23 +7,23 @@
.js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) } .js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) }
.sub-header-block .sub-header-block
.tree-ref-holder .tree-ref-holder.inline.vertical-align-middle
= render 'shared/ref_switcher', destination: 'graphs' = render 'shared/ref_switcher', destination: 'graphs'
%ul.breadcrumb.repo-breadcrumb = link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn'
= commits_breadcrumbs
.loading-graph .loading-graph
.center .center
%h3.page-title %h3.page-title
%i.fa.fa-spinner.fa-spin %i.fa.fa-spinner.fa-spin
Building repository graph. = s_('ContributorsPage|Building repository graph.')
%p.slead Please wait a moment, this page will automatically refresh when ready. %p.slead
= s_('ContributorsPage|Please wait a moment, this page will automatically refresh when ready.')
.stat-graph.hide .stat-graph.hide
.header.clearfix .header.clearfix
%h3#date_header.page-title %h3#date_header.page-title
%p.light %p.light
Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits. = s_('ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits.') % { branch_name: @ref }
%input#brush_change{ :type => "hidden" } %input#brush_change{ :type => "hidden" }
.graphs.row .graphs.row
#contributors-master #contributors-master
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
%td %td
= truncate(hook_log.url, length: 50) = truncate(hook_log.url, length: 50)
%td.light %td.light
#{number_with_precision(hook_log.execution_duration, precision: 2)} ms #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
%td.light %td.light
= time_ago_with_tooltip(hook_log.created_at) = time_ago_with_tooltip(hook_log.created_at)
%td %td
......
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
- builds.select{|build| build.status == build_status}.each do |build| - builds.select{|build| build.status == build_status}.each do |build|
.build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } } .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
= link_to project_job_path(@project, build) do = link_to project_job_path(@project, build) do
= icon('arrow-right') = sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right')
%span{ class: "ci-status-icon-#{build.status}" } %span{ class: "ci-status-icon-#{build.status}" }
= ci_icon_for_status(build.status) = ci_icon_for_status(build.status)
%span %span
...@@ -100,4 +100,5 @@ ...@@ -100,4 +100,5 @@
- else - else
= build.id = build.id
- if build.retried? - if build.retried?
%i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' } %span.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
= sprite_icon('retry', size:16, css_class: 'icon-retry')
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
%li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] } %li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request' = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
%li{ class: merge_request_button_visibility(@merge_request, false) } %li{ class: merge_request_button_visibility(@merge_request, false) }
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
- if can_update_merge_request - if can_update_merge_request
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit" = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit"
......
- expanded = Rails.env.test? - expanded = Rails.env.test?
%section.settings %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
Protected Branches Protected Branches
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Keep stable branches secure and force developers to use merge requests. Keep stable branches secure and force developers to use merge requests.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
%p %p
By default, protected branches are designed to: By default, protected branches are designed to:
%ul %ul
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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