Commit 43a570dc authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-to-ee' into 'master'

CE Upstream - Monday

Closes omnibus-gitlab#1714, gitlab-ce#32587, and gitlab-ce#32218

See merge request !2107
parents 6656d92a 35997518
......@@ -8,13 +8,10 @@ entry.
## 9.2.4 (2017-06-02)
- No changes.
- Fix visibility when referencing snippets.
## 9.2.3 (2017-05-31)
- No changes.
- No changes.
- Move uploads from 'public/uploads' to 'public/uploads/system'.
- Escapes html content before appending it to the DOM.
- Restrict API X-Frame-Options to same origin.
......
......@@ -2,6 +2,7 @@ source 'https://rubygems.org'
gem 'rails', '4.2.8'
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
gem 'bootsnap', '~> 1.0.0'
# Responders respond_to and respond_with
gem 'responders', '~> 2.0'
......@@ -17,7 +18,7 @@ gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.25.1.1'
gem 'faraday', '~> 0.11.0'
gem 'faraday', '~> 0.12'
# Authentication libraries
gem 'devise', '~> 4.2'
......@@ -268,7 +269,7 @@ gem 'sentry-raven', '~> 2.4.0'
gem 'premailer-rails', '~> 1.9.0'
# I18n
gem 'ruby_parser', '~> 3.8.4', require: false
gem 'ruby_parser', '~> 3.8', require: false
gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development
......@@ -379,7 +380,7 @@ gem 'html2text'
gem 'ruby-prof', '~> 0.16.2'
# OAuth
gem 'oauth2', '~> 1.3.0'
gem 'oauth2', '~> 1.4'
# Soft deletion
gem 'paranoia', '~> 2.2'
......
......@@ -91,6 +91,8 @@ GEM
bindata (2.3.5)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (1.0.0)
msgpack (~> 1.0)
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
......@@ -215,7 +217,7 @@ GEM
factory_girl_rails (4.7.0)
factory_girl (~> 4.7.0)
railties (>= 3.0.0)
faraday (0.11.0)
faraday (0.12.1)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.11.0.1)
faraday (>= 0.7.4, < 1.0)
......@@ -490,6 +492,7 @@ GEM
minitest (5.7.0)
mmap2 (2.2.6)
mousetrap-rails (1.4.6)
msgpack (1.1.0)
multi_json (1.12.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
......@@ -505,8 +508,8 @@ GEM
mini_portile2 (~> 2.1.0)
numerizer (0.1.1)
oauth (0.5.1)
oauth2 (1.3.1)
faraday (>= 0.8, < 0.12)
oauth2 (1.4.0)
faraday (>= 0.8, < 0.13)
jwt (~> 1.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
......@@ -716,7 +719,7 @@ GEM
retriable (1.4.1)
rinku (2.0.0)
rotp (2.1.2)
rouge (2.0.7)
rouge (2.1.0)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
......@@ -764,7 +767,7 @@ GEM
ruby-progressbar (1.8.1)
ruby-saml (1.4.1)
nokogiri (>= 1.5.10)
ruby_parser (3.8.4)
ruby_parser (3.9.0)
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
......@@ -797,7 +800,7 @@ GEM
sentry-raven (2.4.0)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
sexp_processor (4.8.0)
sexp_processor (4.9.0)
sham_rack (1.3.6)
rack
shoulda-matchers (2.8.0)
......@@ -950,6 +953,7 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
bootsnap (~> 1.0.0)
bootstrap-sass (~> 3.3.0)
brakeman (~> 3.6.0)
browser (~> 2.2)
......@@ -981,7 +985,7 @@ DEPENDENCIES
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0)
faraday (~> 0.11.0)
faraday (~> 0.12)
faraday_middleware-aws-signers-v4
ffaker (~> 2.4)
flay (~> 2.8.0)
......@@ -1044,7 +1048,7 @@ DEPENDENCIES
net-ldap
net-ssh (~> 3.0.1)
nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.3.0)
oauth2 (~> 1.4)
octokit (~> 4.6.2)
oj (~> 2.17.4)
omniauth (~> 1.4.2)
......@@ -1105,7 +1109,7 @@ DEPENDENCIES
rubocop-rspec (~> 1.15.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
ruby_parser (~> 3.8.4)
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.25.1.1)
sanitize (~> 2.0)
......
......@@ -88,6 +88,7 @@ function installGlEmojiElement() {
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
if (
emojiUnicode &&
isEmojiUnicode &&
!isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion)
) {
......
......@@ -28,7 +28,8 @@ function isSkinToneComboEmoji(emojiUnicode) {
// doesn't support the skin tone versions of horse racing
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
return Array.from(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
const firstCharacter = Array.from(emojiUnicode)[0];
return firstCharacter && firstCharacter.codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode);
}
......
......@@ -35,7 +35,7 @@ export default class BlobFileDropzone {
this.removeFile(file);
});
this.on('sending', function (file, xhr, formData) {
formData.append('branch_name', form.find('input[name="branch_name"]').val());
formData.append('branch_name', form.find('.js-branch-name').val());
formData.append('create_merge_request', form.find('.js-create-merge-request').val());
formData.append('commit_message', form.find('.js-commit-message').val());
});
......
class CreateBranchDropdown {
constructor(el, targetBranchDropdown) {
this.targetBranchDropdown = targetBranchDropdown;
this.el = el;
this.dropdownBack = this.el.closest('.dropdown').querySelector('.dropdown-menu-back');
this.cancelButton = this.el.querySelector('.js-cancel-branch-btn');
this.newBranchField = this.el.querySelector('#new_branch_name');
this.newBranchCreateButton = this.el.querySelector('.js-new-branch-btn');
this.newBranchCreateButton.setAttribute('disabled', '');
this.addBindings();
this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('beforeunload', this.cleanupWrapper);
}
cleanup() {
this.cleanBindings();
document.removeEventListener('beforeunload', this.cleanupWrapper);
}
cleanBindings() {
this.newBranchField.removeEventListener('keyup', this.enableBranchCreateButtonWrapper);
this.newBranchField.removeEventListener('change', this.enableBranchCreateButtonWrapper);
this.newBranchField.removeEventListener('keydown', this.handleNewBranchKeydownWrapper);
this.dropdownBack.removeEventListener('click', this.resetFormWrapper);
this.cancelButton.removeEventListener('click', this.handleCancelClickWrapper);
this.newBranchCreateButton.removeEventListener('click', this.createBranchWrapper);
}
addBindings() {
this.enableBranchCreateButtonWrapper = this.enableBranchCreateButton.bind(this);
this.handleNewBranchKeydownWrapper = this.handleNewBranchKeydown.bind(this);
this.resetFormWrapper = this.resetForm.bind(this);
this.handleCancelClickWrapper = this.handleCancelClick.bind(this);
this.createBranchWrapper = this.createBranch.bind(this);
this.newBranchField.addEventListener('keyup', this.enableBranchCreateButtonWrapper);
this.newBranchField.addEventListener('change', this.enableBranchCreateButtonWrapper);
this.newBranchField.addEventListener('keydown', this.handleNewBranchKeydownWrapper);
this.dropdownBack.addEventListener('click', this.resetFormWrapper);
this.cancelButton.addEventListener('click', this.handleCancelClickWrapper);
this.newBranchCreateButton.addEventListener('click', this.createBranchWrapper);
}
handleCancelClick(e) {
e.preventDefault();
e.stopPropagation();
this.resetForm();
this.dropdownBack.click();
}
handleNewBranchKeydown(e) {
const keyCode = e.which;
const ENTER_KEYCODE = 13;
if (keyCode === ENTER_KEYCODE) {
this.createBranch(e);
}
}
enableBranchCreateButton() {
if (this.newBranchField.value !== '') {
this.newBranchCreateButton.removeAttribute('disabled');
} else {
this.newBranchCreateButton.setAttribute('disabled', '');
}
}
resetForm() {
this.newBranchField.value = '';
this.enableBranchCreateButtonWrapper();
}
createBranch(e) {
e.preventDefault();
if (this.newBranchCreateButton.getAttribute('disabled') === '') {
return;
}
const newBranchName = this.newBranchField.value;
this.targetBranchDropdown.setNewBranch(newBranchName);
this.resetForm();
}
}
window.gl = window.gl || {};
gl.CreateBranchDropdown = CreateBranchDropdown;
/* eslint-disable class-methods-use-this */
const SELECT_ITEM_MSG = 'Select';
class TargetBranchDropDown {
constructor(dropdown) {
this.dropdown = dropdown;
this.$dropdown = $(dropdown);
this.fieldName = this.dropdown.getAttribute('data-field-name');
this.form = this.dropdown.closest('form');
this.createDropdown();
}
static bootstrap() {
const dropdowns = document.querySelectorAll('.js-project-branches-dropdown');
[].forEach.call(dropdowns, dropdown => new TargetBranchDropDown(dropdown));
}
createDropdown() {
const self = this;
this.$dropdown.glDropdown({
selectable: true,
filterable: true,
search: {
fields: ['title'],
},
data: (term, callback) => $.ajax({
url: self.dropdown.getAttribute('data-refs-url'),
data: {
ref: self.dropdown.getAttribute('data-ref'),
show_all: true,
},
dataType: 'json',
}).done(refs => callback(self.dropdownData(refs))),
toggleLabel(item, el) {
if (el.is('.is-active')) {
return item.text;
}
return SELECT_ITEM_MSG;
},
clicked(options) {
options.e.preventDefault();
self.onClick.call(self);
},
fieldName: self.fieldName,
});
return new gl.CreateBranchDropdown(this.form.querySelector('.dropdown-new-branch'), this);
}
onClick() {
this.enableSubmit();
this.$dropdown.trigger('change.branch');
}
enableSubmit() {
const submitBtn = this.form.querySelector('[type="submit"]');
if (this.branchInput && this.branchInput.value) {
submitBtn.removeAttribute('disabled');
} else {
submitBtn.setAttribute('disabled', '');
}
}
dropdownData(refs) {
const branchList = this.dropdownItems(refs);
this.cachedRefs = refs;
this.addDefaultBranch(branchList);
this.addNewBranch(branchList);
return { Branches: branchList };
}
dropdownItems(refs) {
return refs.map(this.dropdownItem);
}
dropdownItem(ref) {
return { id: ref, text: ref, title: ref };
}
addDefaultBranch(branchList) {
// when no branch is selected do nothing
if (!this.branchInput) {
return;
}
const branchInputVal = this.branchInput.value;
const currentBranchIndex = this.searchBranch(branchList, branchInputVal);
if (currentBranchIndex === -1) {
this.unshiftBranch(branchList, this.dropdownItem(branchInputVal));
}
}
addNewBranch(branchList) {
if (this.newBranch) {
this.unshiftBranch(branchList, this.newBranch);
}
}
searchBranch(branchList, branchName) {
return _.findIndex(branchList, el => branchName === el.id);
}
unshiftBranch(branchList, branch) {
const branchIndex = this.searchBranch(branchList, branch.id);
if (branchIndex === -1) {
branchList.unshift(branch);
}
}
setNewBranch(newBranchName) {
this.newBranch = this.dropdownItem(newBranchName);
this.refreshData();
this.selectBranch(this.searchBranch(this.glDropdown.fullData.Branches, newBranchName));
}
refreshData() {
this.glDropdown.fullData = this.dropdownData(this.cachedRefs);
this.clearFilter();
}
clearFilter() {
// apply an empty filter in order to refresh the data
this.glDropdown.filter.filter('');
this.dropdown.closest('.dropdown').querySelector('.dropdown-page-one .dropdown-input-field').value = '';
}
selectBranch(index) {
const branch = this.dropdown.closest('.dropdown').querySelectorAll('li a')[index];
if (!branch.classList.contains('is-active')) {
branch.click();
} else {
this.closeDropdown();
}
}
closeDropdown() {
this.dropdown.closest('.dropdown').querySelector('.dropdown-menu-close').click();
}
get branchInput() {
return this.form.querySelector(`input[name="${this.fieldName}"]`);
}
get glDropdown() {
return this.$dropdown.data('glDropdown');
}
}
window.gl = window.gl || {};
gl.TargetBranchDropDown = TargetBranchDropDown;
......@@ -105,6 +105,8 @@ $(() => {
if (list.type === 'closed') {
list.position = Infinity;
list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' };
} else if (list.type === 'backlog') {
list.position = -1;
}
});
......@@ -147,7 +149,7 @@ $(() => {
},
computed: {
disabled() {
return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length;
return !this.store.lists.filter(list => !list.preset).length;
},
tooltipTitle() {
if (this.disabled) {
......
/* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* global Sortable */
import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list';
import boardBlankState from './board_blank_state';
import './board_delete';
......@@ -22,6 +23,10 @@ gl.issueBoards.Board = Vue.extend({
disabled: Boolean,
issueLinkBase: String,
rootPath: String,
boardId: {
type: String,
required: true,
},
},
data () {
return {
......@@ -78,7 +83,16 @@ gl.issueBoards.Board = Vue.extend({
methods: {
showNewIssueForm() {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
}
},
toggleExpanded(e) {
if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) {
this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) {
localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded);
}
}
},
},
mounted () {
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
......@@ -102,4 +116,11 @@ gl.issueBoards.Board = Vue.extend({
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
},
created() {
if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) {
const isCollapsed = localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
this.list.isExpanded = !isCollapsed;
}
},
});
......@@ -32,9 +32,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({
showSidebar () {
return Object.keys(this.issue).length;
},
assigneeId() {
return this.issue.assignee ? this.issue.assignee.id : 0;
},
milestoneTitle() {
return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
}
......
......@@ -152,6 +152,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
<div class="card-assignee">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
:key="assignee.id"
v-if="shouldRenderAssignee(index)"
class="js-no-trigger"
:link-href="assigneeUrl(assignee)"
......
......@@ -26,7 +26,8 @@ gl.issueBoards.ModalFooter = Vue.extend({
},
methods: {
addIssues() {
const list = this.modal.selectedList || this.state.lists[0];
const firstListIndex = 1;
const list = this.modal.selectedList || this.state.lists[firstListIndex];
const selectedIssues = ModalStore.getSelectedIssues();
const issueIds = selectedIssues.map(issue => issue.globalId);
......
......@@ -11,7 +11,7 @@ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
},
computed: {
selected() {
return this.modal.selectedList || this.state.lists[0];
return this.modal.selectedList || this.state.lists[1];
},
},
destroyed() {
......
......@@ -12,7 +12,9 @@ class List {
this.position = obj.position;
this.title = obj.title;
this.type = obj.list_type;
this.preset = ['closed', 'blank'].indexOf(this.type) > -1;
this.preset = ['backlog', 'closed', 'blank'].indexOf(this.type) > -1;
this.isExpandable = ['backlog', 'closed'].indexOf(this.type) > -1;
this.isExpanded = true;
this.page = 1;
this.loading = true;
this.loadingMore = false;
......
......@@ -37,10 +37,14 @@ gl.issueBoards.BoardsStore = {
},
new (listObj) {
const list = this.addList(listObj);
const backlogList = this.findList('type', 'backlog', 'backlog');
list
.save()
.then(() => {
// Remove any new issues from the backlog
// as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList));
this.state.lists = _.sortBy(this.state.lists, 'position');
})
.catch(() => {
......@@ -53,7 +57,7 @@ gl.issueBoards.BoardsStore = {
},
shouldAddBlankState () {
// Decide whether to add the blank state
return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0]);
},
addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
......@@ -106,7 +110,7 @@ gl.issueBoards.BoardsStore = {
issueTo.removeLabel(listFrom.label);
}
if (listTo.type === 'closed') {
if (listTo.type === 'closed' && listFrom.type !== 'backlog') {
issueLists.forEach((list) => {
list.removeIssue(issue);
});
......
......@@ -20,6 +20,7 @@ window.Build = (function () {
this.$document = $(document);
this.logBytes = 0;
this.scrollOffsetPadding = 30;
this.hasBeenScrolled = false;
this.updateDropdown = this.updateDropdown.bind(this);
this.getBuildTrace = this.getBuildTrace.bind(this);
......@@ -62,6 +63,15 @@ window.Build = (function () {
.off('click')
.on('click', this.scrollToBottom.bind(this));
const scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$scrollContainer
.off('scroll')
.on('scroll', () => {
this.hasBeenScrolled = true;
scrollThrottled();
});
$(window)
.off('resize.build')
.on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
......@@ -70,25 +80,16 @@ window.Build = (function () {
// eslint-disable-next-line
this.getBuildTrace()
.then(() => this.makeTraceScrollable())
.then(() => this.scrollToBottom());
.then(() => this.toggleScroll())
.then(() => {
if (!this.hasBeenScrolled) {
this.scrollToBottom();
}
});
this.verifyTopPosition();
}
Build.prototype.makeTraceScrollable = function () {
this.$scrollContainer.niceScroll({
cursorcolor: '#fff',
cursoropacitymin: 1,
cursorwidth: '3px',
railpadding: { top: 5, bottom: 5, right: 5 },
});
this.$scrollContainer.on('scroll', _.throttle(this.toggleScroll.bind(this), 100));
this.toggleScroll();
};
Build.prototype.canScroll = function () {
return (this.$scrollContainer.prop('scrollHeight') - this.scrollOffsetPadding) > this.$scrollContainer.height();
};
......@@ -104,12 +105,11 @@ window.Build = (function () {
*
*/
Build.prototype.toggleScroll = function () {
const bottomScroll = this.$scrollContainer.scrollTop() +
this.scrollOffsetPadding +
this.$scrollContainer.height();
const currentPosition = this.$scrollContainer.scrollTop();
const bottomScroll = currentPosition + this.$scrollContainer.innerHeight();
if (this.canScroll()) {
if (this.$scrollContainer.scrollTop() === 0) {
if (currentPosition === 0) {
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (bottomScroll === this.$scrollContainer.prop('scrollHeight')) {
......@@ -123,12 +123,14 @@ window.Build = (function () {
};
Build.prototype.scrollToTop = function () {
this.$scrollContainer.getNiceScroll(0).doScrollTop(0);
this.hasBeenScrolled = true;
this.$scrollContainer.scrollTop(0);
this.toggleScroll();
};
Build.prototype.scrollToBottom = function () {
this.$scrollContainer.getNiceScroll(0).doScrollTo(this.$scrollContainer.prop('scrollHeight'));
this.hasBeenScrolled = true;
this.$scrollContainer.scrollTop(this.$scrollContainer.prop('scrollHeight'));
this.toggleScroll();
};
......@@ -216,7 +218,11 @@ window.Build = (function () {
Build.timeout = setTimeout(() => {
//eslint-disable-next-line
this.getBuildTrace()
.then(() => this.scrollToBottom());
.then(() => {
if (!this.hasBeenScrolled) {
this.scrollToBottom();
}
});
}, 4000);
} else {
this.$buildRefreshAnimation.remove();
......@@ -238,7 +244,7 @@ window.Build = (function () {
};
Build.prototype.toggleSidebar = function (shouldHide) {
const shouldShow = !shouldHide;
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
this.$buildTrace
.toggleClass('sidebar-expanded', shouldShow)
......@@ -253,7 +259,7 @@ window.Build = (function () {
this.verifyTopPosition();
if (this.$scrollContainer.getNiceScroll(0)) {
if (this.canScroll()) {
this.toggleScroll();
}
};
......
// ECMAScript polyfills
import 'core-js/fn/array/find';
import 'core-js/fn/array/find-index';
import 'core-js/fn/array/from';
import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign';
......
......@@ -80,21 +80,27 @@
v-if="isLoading && !hasKeys"
size="2"
label="Loading deploy keys"
/>
/>
<div v-else-if="hasKeys">
<keys-panel
title="Enabled deploy keys for this project"
:keys="keys.enabled_keys"
:store="store" />
:store="store"
:endpoint="endpoint"
/>
<keys-panel
title="Deploy keys from projects you have access to"
:keys="keys.available_project_keys"
:store="store" />
:store="store"
:endpoint="endpoint"
/>
<keys-panel
v-if="keys.public_keys.length"
title="Public deploy keys available to any project"
:keys="keys.public_keys"
:store="store" />
:store="store"
:endpoint="endpoint"
/>
</div>
</div>
</template>
......@@ -11,6 +11,10 @@
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
},
components: {
actionBtn,
......@@ -19,6 +23,9 @@
timeagoDate() {
return gl.utils.getTimeago().format(this.deployKey.created_at);
},
editDeployKeyPath() {
return `${this.endpoint}/${this.deployKey.id}/edit`;
},
},
methods: {
isEnabled(id) {
......@@ -33,7 +40,8 @@
<div class="pull-left append-right-10 hidden-xs">
<i
aria-hidden="true"
class="fa fa-key key-icon">
class="fa fa-key key-icon"
>
</i>
</div>
<div class="deploy-key-content key-list-item-info">
......@@ -45,7 +53,8 @@
</div>
<div
v-if="deployKey.can_push"
class="write-access-allowed">
class="write-access-allowed"
>
Write access allowed
</div>
</div>
......@@ -53,7 +62,8 @@
<a
v-for="project in deployKey.projects"
class="label deploy-project-label"
:href="project.full_path">
:href="project.full_path"
>
{{ project.full_name }}
</a>
</div>
......@@ -61,20 +71,30 @@
<span class="key-created-at">
created {{ timeagoDate }}
</span>
<a
v-if="deployKey.can_edit"
class="btn btn-small"
:href="editDeployKeyPath"
>
Edit
</a>
<action-btn
v-if="!isEnabled(deployKey.id)"
:deploy-key="deployKey"
type="enable"/>
type="enable"
/>
<action-btn
v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned"
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="remove" />
type="remove"
/>
<action-btn
v-else
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="disable" />
type="disable"
/>
</div>
</div>
</template>
......@@ -20,6 +20,10 @@
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
},
components: {
key,
......@@ -34,18 +38,22 @@
({{ keys.length }})
</h5>
<ul class="well-list"
v-if="keys.length">
v-if="keys.length"
>
<li
v-for="deployKey in keys"
:key="deployKey.id">
<key
:deploy-key="deployKey"
:store="store" />
:store="store"
:endpoint="endpoint"
/>
</li>
</ul>
<div
class="settings-message text-center"
v-else-if="showHelpBox">
v-else-if="showHelpBox"
>
No deploy keys found. Create one with the form above.
</div>
</div>
......
......@@ -167,9 +167,6 @@ import AuditLogs from './audit_logs';
case 'admin:projects:index':
new ProjectsList();
break;
case 'dashboard:groups:index':
new GroupsList();
break;
case 'explore:groups:index':
new GroupsList();
......@@ -343,25 +340,14 @@ import AuditLogs from './audit_logs';
shortcut_handler = new ShortcutsNavigation();
new TreeView();
new BlobViewer();
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:find_file:show':
shortcut_handler = true;
break;
case 'projects:blob:new':
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blob:create':
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blob:show':
new BlobViewer();
gl.TargetBranchDropDown.bootstrap();
initBlob();
break;
case 'projects:blob:edit':
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blame:show':
initBlob();
break;
......@@ -385,9 +371,11 @@ import AuditLogs from './audit_logs';
new ProjectFork();
break;
case 'projects:artifacts:browse':
new ShortcutsNavigation();
new BuildArtifacts();
break;
case 'projects:artifacts:file':
new ShortcutsNavigation();
new BlobViewer();
break;
case 'help:index':
......
......@@ -252,7 +252,7 @@ export default {
</div>
</div>
<div class="content-list environments-container">
<div class="environments-container">
<loading-icon
label="Loading environments"
size="3"
......
......@@ -69,7 +69,7 @@ export default {
</span>
</button>
<ul class="dropdown-menu">
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<button
type="button"
......
......@@ -423,11 +423,13 @@ export default {
</script>
<template>
<div
:class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }">
:class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }"
role="row">
<div class="table-section section-10" role="gridcell">
<div
v-if="!model.isFolder"
class="table-mobile-header">
class="table-mobile-header"
role="rowheader">
Environment
</div>
<span
......@@ -513,6 +515,7 @@ export default {
<div class="table-section section-25" role="gridcell">
<div
v-if="!model.isFolder"
role="rowheader"
class="table-mobile-header">
Commit
</div>
......@@ -529,7 +532,7 @@ export default {
</div>
<div
v-if="!model.isFolder && !hasLastDeploymentKey"
class="commit-title">
class="commit-title table-mobile-content">
No deployments yet
</div>
</div>
......@@ -537,6 +540,7 @@ export default {
<div class="table-section section-10" role="gridcell">
<div
v-if="!model.isFolder"
role="rowheader"
class="table-mobile-header">
Updated
</div>
......
......@@ -66,19 +66,19 @@ export default {
<template>
<div class="ci-table" role="grid">
<div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section section-10 environments-name" role="rowheader">
<div class="table-section section-10 environments-name" role="columnheader">
Environment
</div>
<div class="table-section section-10 environments-deploy" role="rowheader">
<div class="table-section section-10 environments-deploy" role="columnheader">
Deployment
</div>
<div class="table-section section-15 environments-build" role="rowheader">
<div class="table-section section-15 environments-build" role="columnheader">
Job
</div>
<div class="table-section section-25 environments-commit" role="rowheader">
<div class="table-section section-25 environments-commit" role="columnheader">
Commit
</div>
<div class="table-section section-10 environments-date" role="rowheader">
<div class="table-section section-10 environments-date" role="columnheader">
Updated
</div>
</div>
......
......@@ -8,39 +8,87 @@ export default class FilterableList {
this.filterForm = form;
this.listFilterElement = filter;
this.listHolderElement = holder;
this.isBusy = false;
}
getFilterEndpoint() {
return `${this.filterForm.getAttribute('action')}?${$(this.filterForm).serialize()}`;
}
getPagePath() {
return this.getFilterEndpoint();
}
initSearch() {
this.debounceFilter = _.debounce(this.filterResults.bind(this), 500);
// Wrap to prevent passing event arguments to .filterResults;
this.debounceFilter = _.debounce(this.onFilterInput.bind(this), 500);
this.listFilterElement.removeEventListener('input', this.debounceFilter);
this.unbindEvents();
this.bindEvents();
}
onFilterInput() {
const $form = $(this.filterForm);
const queryData = {};
const filterGroupsParam = $form.find('[name="filter_groups"]').val();
if (filterGroupsParam) {
queryData.filter_groups = filterGroupsParam;
}
this.filterResults(queryData);
if (this.setDefaultFilterOption) {
this.setDefaultFilterOption();
}
}
bindEvents() {
this.listFilterElement.addEventListener('input', this.debounceFilter);
}
filterResults() {
const form = this.filterForm;
const filterUrl = `${form.getAttribute('action')}?${$(form).serialize()}`;
unbindEvents() {
this.listFilterElement.removeEventListener('input', this.debounceFilter);
}
filterResults(queryData) {
if (this.isBusy) {
return false;
}
$(this.listHolderElement).fadeTo(250, 0.5);
return $.ajax({
url: form.getAttribute('action'),
data: $(form).serialize(),
url: this.getFilterEndpoint(),
data: queryData,
type: 'GET',
dataType: 'json',
context: this,
complete() {
$(this.listHolderElement).fadeTo(250, 1);
complete: this.onFilterComplete,
beforeSend: () => {
this.isBusy = true;
},
success(data) {
this.listHolderElement.innerHTML = data.html;
// Change url so if user reload a page - search results are saved
return window.history.replaceState({
page: filterUrl,
}, document.title, filterUrl);
success: (response, textStatus, xhr) => {
this.onFilterSuccess(response, xhr, queryData);
},
});
}
onFilterSuccess(response, xhr, queryData) {
if (response.html) {
this.listHolderElement.innerHTML = response.html;
}
// Change url so if user reload a page - search results are saved
const currentPath = this.getPagePath(queryData);
return window.history.replaceState({
page: currentPath,
}, document.title, currentPath);
}
onFilterComplete() {
this.isBusy = false;
$(this.listHolderElement).fadeTo(250, 1);
}
}
......@@ -81,6 +81,41 @@ class FilteredSearchManager {
}
}
bindStateEvents() {
this.stateFilters = document.querySelector('.container-fluid .issues-state-filters');
if (this.stateFilters) {
this.searchStateWrapper = this.searchState.bind(this);
this.stateFilters.querySelector('[data-state="opened"]')
.addEventListener('click', this.searchStateWrapper);
this.stateFilters.querySelector('[data-state="closed"]')
.addEventListener('click', this.searchStateWrapper);
this.stateFilters.querySelector('[data-state="all"]')
.addEventListener('click', this.searchStateWrapper);
this.mergedState = this.stateFilters.querySelector('[data-state="merged"]');
if (this.mergedState) {
this.mergedState.addEventListener('click', this.searchStateWrapper);
}
}
}
unbindStateEvents() {
if (this.stateFilters) {
this.stateFilters.querySelector('[data-state="opened"]')
.removeEventListener('click', this.searchStateWrapper);
this.stateFilters.querySelector('[data-state="closed"]')
.removeEventListener('click', this.searchStateWrapper);
this.stateFilters.querySelector('[data-state="all"]')
.removeEventListener('click', this.searchStateWrapper);
if (this.mergedState) {
this.mergedState.removeEventListener('click', this.searchStateWrapper);
}
}
}
bindEvents() {
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
......@@ -116,6 +151,8 @@ class FilteredSearchManager {
document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
this.bindStateEvents();
}
unbindEvents() {
......@@ -136,6 +173,8 @@ class FilteredSearchManager {
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
this.unbindStateEvents();
}
checkForBackspace(e) {
......@@ -451,7 +490,19 @@ class FilteredSearchManager {
}
}
search() {
searchState(e) {
const target = e.currentTarget;
// remove focus outline after click
target.blur();
const state = target.dataset && target.dataset.state;
if (state) {
this.search(state);
}
}
search(state = null) {
const paths = [];
const searchQuery = gl.DropdownUtils.getSearchQuery();
......@@ -459,7 +510,7 @@ class FilteredSearchManager {
const { tokens, searchToken }
= this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys());
const currentState = gl.utils.getParameterByName('state') || 'opened';
const currentState = state || gl.utils.getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
tokens.forEach((token) => {
......
......@@ -248,7 +248,7 @@ GitLabDropdown = (function() {
return function(data) {
_this.fullData = data;
_this.parseData(_this.fullData);
_this.focusTextInput();
_this.focusTextInput(true);
if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
return _this.filter.input.trigger('input');
}
......@@ -743,8 +743,20 @@ GitLabDropdown = (function() {
return [selectedObject, isMarking];
};
GitLabDropdown.prototype.focusTextInput = function() {
if (this.options.filterable) { this.filterInput.focus(); }
GitLabDropdown.prototype.focusTextInput = function(triggerFocus = false) {
if (this.options.filterable) {
$(':focus').blur();
this.dropdown.one('transitionend', () => {
this.filterInput.focus();
});
if (triggerFocus) {
// This triggers after a ajax request
// in case of slow requests, the dropdown transition could already be finished
this.dropdown.trigger('transitionend');
}
}
};
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
......
<script>
export default {
props: {
groups: {
type: Object,
required: true,
},
baseGroup: {
type: Object,
required: false,
default: () => ({}),
},
},
};
</script>
<template>
<ul class="content-list group-list-tree">
<group-item
v-for="(group, index) in groups"
:key="index"
:group="group"
:base-group="baseGroup"
:collection="groups"
/>
</ul>
</template>
<script>
import eventHub from '../event_hub';
export default {
props: {
group: {
type: Object,
required: true,
},
baseGroup: {
type: Object,
required: false,
default: () => ({}),
},
collection: {
type: Object,
required: false,
default: () => ({}),
},
},
methods: {
onClickRowGroup(e) {
e.stopPropagation();
// Skip for buttons
if (!(e.target.tagName === 'A') && !(e.target.tagName === 'I' && e.target.parentElement.tagName === 'A')) {
if (this.group.hasSubgroups) {
eventHub.$emit('toggleSubGroups', this.group);
} else {
window.location.href = this.group.webUrl;
}
}
},
onLeaveGroup(e) {
e.preventDefault();
// eslint-disable-next-line no-alert
if (confirm(`Are you sure you want to leave the "${this.group.fullName}" group?`)) {
this.leaveGroup();
}
},
leaveGroup() {
eventHub.$emit('leaveGroup', this.group, this.collection);
},
},
computed: {
groupDomId() {
return `group-${this.group.id}`;
},
rowClass() {
return {
'group-row': true,
'is-open': this.group.isOpen,
'has-subgroups': this.group.hasSubgroups,
'no-description': !this.group.description,
};
},
visibilityIcon() {
return {
fa: true,
'fa-globe': this.group.visibility === 'public',
'fa-shield': this.group.visibility === 'internal',
'fa-lock': this.group.visibility === 'private',
};
},
fullPath() {
let fullPath = '';
if (this.group.isOrphan) {
// check if current group is baseGroup
if (Object.keys(this.baseGroup).length > 0 && this.baseGroup !== this.group) {
// Remove baseGroup prefix from our current group.fullName. e.g:
// baseGroup.fullName: `level1`
// group.fullName: `level1 / level2 / level3`
// Result: `level2 / level3`
const gfn = this.group.fullName;
const bfn = this.baseGroup.fullName;
const length = bfn.length;
const start = gfn.indexOf(bfn);
const extraPrefixChars = 3;
fullPath = gfn.substr(start + length + extraPrefixChars);
} else {
fullPath = this.group.fullName;
}
} else {
fullPath = this.group.name;
}
return fullPath;
},
hasGroups() {
return Object.keys(this.group.subGroups).length > 0;
},
},
};
</script>
<template>
<li
@click.stop="onClickRowGroup"
:id="groupDomId"
:class="rowClass"
>
<div
class="group-row-contents">
<div
class="controls">
<a
v-if="group.canEdit"
class="edit-group btn"
:href="group.editPath">
<i
class="fa fa-cogs"
aria-hidden="true"
>
</i>
</a>
<a
@click="onLeaveGroup"
:href="group.leavePath"
class="leave-group btn"
title="Leave this group">
<i
class="fa fa-sign-out"
aria-hidden="true"
>
</i>
</a>
</div>
<div
class="stats">
<span
class="number-projects">
<i
class="fa fa-bookmark"
aria-hidden="true"
>
</i>
{{group.numberProjects}}
</span>
<span
class="number-users">
<i
class="fa fa-users"
aria-hidden="true"
>
</i>
{{group.numberUsers}}
</span>
<span
class="group-visibility">
<i
:class="visibilityIcon"
aria-hidden="true"
>
</i>
</span>
</div>
<div
class="folder-toggle-wrap">
<span
class="folder-caret"
v-if="group.hasSubgroups">
<i
v-if="group.isOpen"
class="fa fa-caret-down"
aria-hidden="true"
>
</i>
<i
v-if="!group.isOpen"
class="fa fa-caret-right"
aria-hidden="true"
>
</i>
</span>
<span class="folder-icon">
<i
v-if="group.isOpen"
class="fa fa-folder-open"
aria-hidden="true"
>
</i>
<i
v-if="!group.isOpen"
class="fa fa-folder"
aria-hidden="true">
</i>
</span>
</div>
<div
class="avatar-container s40 hidden-xs">
<a
:href="group.webUrl">
<img
class="avatar s40"
:src="group.avatarUrl"
/>
</a>
</div>
<div
class="title">
<a
:href="group.webUrl">{{fullPath}}</a>
<template v-if="group.permissions.humanGroupAccess">
as
<span class="access-type">{{group.permissions.humanGroupAccess}}</span>
</template>
</div>
<div
class="description">{{group.description}}</div>
</div>
<group-folder
v-if="group.isOpen && hasGroups"
:groups="group.subGroups"
:baseGroup="group"
/>
</li>
</template>
<script>
import tablePagination from '~/vue_shared/components/table_pagination.vue';
import eventHub from '../event_hub';
export default {
props: {
groups: {
type: Object,
required: true,
},
pageInfo: {
type: Object,
required: true,
},
},
components: {
tablePagination,
},
methods: {
change(page) {
const filterGroupsParam = gl.utils.getParameterByName('filter_groups');
const sortParam = gl.utils.getParameterByName('sort');
eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam);
},
},
};
</script>
<template>
<div class="groups-list-tree-container">
<group-folder
:groups="groups"
/>
<table-pagination
:change="change"
:pageInfo="pageInfo"
/>
</div>
</template>
import Vue from 'vue';
export default new Vue();
import FilterableList from '~/filterable_list';
import eventHub from './event_hub';
export default class GroupFilterableList extends FilterableList {
constructor({ form, filter, holder, filterEndpoint, pagePath }) {
super(form, filter, holder);
this.form = form;
this.filterEndpoint = filterEndpoint;
this.pagePath = pagePath;
this.$dropdown = $('.js-group-filter-dropdown-wrap');
}
getFilterEndpoint() {
return this.filterEndpoint;
}
getPagePath(queryData) {
const params = queryData ? $.param(queryData) : '';
const queryString = params ? `?${params}` : '';
return `${this.pagePath}${queryString}`;
}
bindEvents() {
super.bindEvents();
this.onFormSubmitWrapper = this.onFormSubmit.bind(this);
this.onFilterOptionClikWrapper = this.onOptionClick.bind(this);
this.filterForm.addEventListener('submit', this.onFormSubmitWrapper);
this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper);
}
onFormSubmit(e) {
e.preventDefault();
const $form = $(this.form);
const filterGroupsParam = $form.find('[name="filter_groups"]').val();
const queryData = {};
if (filterGroupsParam) {
queryData.filter_groups = filterGroupsParam;
}
this.filterResults(queryData);
this.setDefaultFilterOption();
}
setDefaultFilterOption() {
const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu a:first-child').text());
this.$dropdown.find('.dropdown-label').text(defaultOption);
}
onOptionClick(e) {
e.preventDefault();
const queryData = {};
const sortParam = gl.utils.getParameterByName('sort', e.currentTarget.href);
if (sortParam) {
queryData.sort = sortParam;
}
this.filterResults(queryData);
// Active selected option
this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text));
// Clear current value on search form
this.form.querySelector('[name="filter_groups"]').value = '';
}
onFilterSuccess(data, xhr, queryData) {
super.onFilterSuccess(data, xhr, queryData);
const paginationData = {
'X-Per-Page': xhr.getResponseHeader('X-Per-Page'),
'X-Page': xhr.getResponseHeader('X-Page'),
'X-Total': xhr.getResponseHeader('X-Total'),
'X-Total-Pages': xhr.getResponseHeader('X-Total-Pages'),
'X-Next-Page': xhr.getResponseHeader('X-Next-Page'),
'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'),
};
eventHub.$emit('updateGroups', data);
eventHub.$emit('updatePagination', paginationData);
}
}
/* global Flash */
import Vue from 'vue';
import GroupFilterableList from './groups_filterable_list';
import GroupsComponent from './components/groups.vue';
import GroupFolder from './components/group_folder.vue';
import GroupItem from './components/group_item.vue';
import GroupsStore from './stores/groups_store';
import GroupsService from './services/groups_service';
import eventHub from './event_hub';
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('dashboard-group-app');
// Don't do anything if element doesn't exist (No groups)
// This is for when the user enters directly to the page via URL
if (!el) {
return;
}
Vue.component('groups-component', GroupsComponent);
Vue.component('group-folder', GroupFolder);
Vue.component('group-item', GroupItem);
// eslint-disable-next-line no-new
new Vue({
el,
data() {
this.store = new GroupsStore();
this.service = new GroupsService(el.dataset.endpoint);
return {
store: this.store,
isLoading: true,
state: this.store.state,
loading: true,
};
},
computed: {
isEmpty() {
return Object.keys(this.state.groups).length === 0;
},
},
methods: {
fetchGroups(parentGroup) {
let parentId = null;
let getGroups = null;
let page = null;
let sort = null;
let pageParam = null;
let sortParam = null;
let filterGroups = null;
let filterGroupsParam = null;
if (parentGroup) {
parentId = parentGroup.id;
} else {
this.isLoading = true;
}
pageParam = gl.utils.getParameterByName('page');
if (pageParam) {
page = pageParam;
}
filterGroupsParam = gl.utils.getParameterByName('filter_groups');
if (filterGroupsParam) {
filterGroups = filterGroupsParam;
}
sortParam = gl.utils.getParameterByName('sort');
if (sortParam) {
sort = sortParam;
}
getGroups = this.service.getGroups(parentId, page, filterGroups, sort);
getGroups
.then(response => response.json())
.then((response) => {
this.isLoading = false;
this.updateGroups(response, parentGroup);
})
.catch(this.handleErrorResponse);
return getGroups;
},
fetchPage(page, filterGroups, sort) {
this.isLoading = true;
return this.service
.getGroups(null, page, filterGroups, sort)
.then((response) => {
this.isLoading = false;
$.scrollTo(0);
const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href);
window.history.replaceState({
page: currentPath,
}, document.title, currentPath);
this.updateGroups(response.json());
this.updatePagination(response.headers);
})
.catch(this.handleErrorResponse);
},
toggleSubGroups(parentGroup = null) {
if (!parentGroup.isOpen) {
this.store.resetGroups(parentGroup);
this.fetchGroups(parentGroup);
}
this.store.toggleSubGroups(parentGroup);
},
leaveGroup(group, collection) {
this.service.leaveGroup(group.leavePath)
.then((response) => {
$.scrollTo(0);
this.store.removeGroup(group, collection);
// eslint-disable-next-line no-new
new Flash(response.json().notice, 'notice');
})
.catch((response) => {
let message = 'An error occurred. Please try again.';
if (response.status === 403) {
message = 'Failed to leave the group. Please make sure you are not the only owner';
}
// eslint-disable-next-line no-new
new Flash(message);
});
},
updateGroups(groups, parentGroup) {
this.store.setGroups(groups, parentGroup);
},
updatePagination(headers) {
this.store.storePagination(headers);
},
handleErrorResponse() {
this.isLoading = false;
$.scrollTo(0);
// eslint-disable-next-line no-new
new Flash('An error occurred. Please try again.');
},
},
created() {
eventHub.$on('fetchPage', this.fetchPage);
eventHub.$on('toggleSubGroups', this.toggleSubGroups);
eventHub.$on('leaveGroup', this.leaveGroup);
eventHub.$on('updateGroups', this.updateGroups);
eventHub.$on('updatePagination', this.updatePagination);
},
beforeMount() {
let groupFilterList = null;
const form = document.querySelector('form#group-filter-form');
const filter = document.querySelector('.js-groups-list-filter');
const holder = document.querySelector('.js-groups-list-holder');
const opts = {
form,
filter,
holder,
filterEndpoint: el.dataset.endpoint,
pagePath: el.dataset.path,
};
groupFilterList = new GroupFilterableList(opts);
groupFilterList.initSearch();
},
mounted() {
this.fetchGroups()
.then((response) => {
this.updatePagination(response.headers);
this.isLoading = false;
})
.catch(this.handleErrorResponse);
},
beforeDestroy() {
eventHub.$off('fetchPage', this.fetchPage);
eventHub.$off('toggleSubGroups', this.toggleSubGroups);
eventHub.$off('leaveGroup', this.leaveGroup);
eventHub.$off('updateGroups', this.updateGroups);
eventHub.$off('updatePagination', this.updatePagination);
},
});
});
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class GroupsService {
constructor(endpoint) {
this.groups = Vue.resource(endpoint);
}
getGroups(parentId, page, filterGroups, sort) {
const data = {};
if (parentId) {
data.parent_id = parentId;
} else {
// Do not send the following param for sub groups
if (page) {
data.page = page;
}
if (filterGroups) {
data.filter_groups = filterGroups;
}
if (sort) {
data.sort = sort;
}
}
return this.groups.get(data);
}
// eslint-disable-next-line class-methods-use-this
leaveGroup(endpoint) {
return Vue.http.delete(endpoint);
}
}
import Vue from 'vue';
export default class GroupsStore {
constructor() {
this.state = {};
this.state.groups = {};
this.state.pageInfo = {};
}
setGroups(rawGroups, parent) {
const parentGroup = parent;
const tree = this.buildTree(rawGroups, parentGroup);
if (parentGroup) {
parentGroup.subGroups = tree;
} else {
this.state.groups = tree;
}
return tree;
}
// eslint-disable-next-line class-methods-use-this
resetGroups(parent) {
const parentGroup = parent;
parentGroup.subGroups = {};
}
storePagination(pagination = {}) {
let paginationInfo;
if (Object.keys(pagination).length) {
const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
} else {
paginationInfo = pagination;
}
this.state.pageInfo = paginationInfo;
}
buildTree(rawGroups, parentGroup) {
const groups = this.decorateGroups(rawGroups);
const tree = {};
const mappedGroups = {};
const orphans = [];
// Map groups to an object
groups.map((group) => {
mappedGroups[group.id] = group;
mappedGroups[group.id].subGroups = {};
return group;
});
Object.keys(mappedGroups).map((key) => {
const currentGroup = mappedGroups[key];
if (currentGroup.parentId) {
// If the group is not at the root level, add it to its parent array of subGroups.
const findParentGroup = mappedGroups[currentGroup.parentId];
if (findParentGroup) {
mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup;
mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups
} else if (parentGroup && parentGroup.id === currentGroup.parentId) {
tree[currentGroup.id] = currentGroup;
} else {
// Means the groups hast no direct parent.
// Save for later processing, we will add them to its corresponding base group
orphans.push(currentGroup);
}
} else {
// If the group is at the root level, add it to first level elements array.
tree[currentGroup.id] = currentGroup;
}
return key;
});
// Hopefully this array will be empty for most cases
if (orphans.length) {
orphans.map((orphan) => {
let found = false;
const currentOrphan = orphan;
Object.keys(tree).map((key) => {
const group = tree[key];
if (currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0) {
group.subGroups[currentOrphan.id] = currentOrphan;
group.isOpen = true;
currentOrphan.isOrphan = true;
found = true;
}
return key;
});
if (!found) {
currentOrphan.isOrphan = true;
tree[currentOrphan.id] = currentOrphan;
}
return orphan;
});
}
return tree;
}
decorateGroups(rawGroups) {
this.groups = rawGroups.map(this.decorateGroup);
return this.groups;
}
// eslint-disable-next-line class-methods-use-this
decorateGroup(rawGroup) {
return {
id: rawGroup.id,
fullName: rawGroup.full_name,
fullPath: rawGroup.full_path,
avatarUrl: rawGroup.avatar_url,
name: rawGroup.name,
hasSubgroups: rawGroup.has_subgroups,
canEdit: rawGroup.can_edit,
description: rawGroup.description,
webUrl: rawGroup.web_url,
parentId: rawGroup.parent_id,
visibility: rawGroup.visibility,
leavePath: rawGroup.leave_path,
editPath: rawGroup.edit_path,
isOpen: false,
isOrphan: false,
numberProjects: rawGroup.number_projects_with_delimiter,
numberUsers: rawGroup.number_users_with_delimiter,
permissions: {
humanGroupAccess: rawGroup.permissions.human_group_access,
},
subGroups: {},
};
}
// eslint-disable-next-line class-methods-use-this
removeGroup(group, collection) {
Vue.delete(collection, group.id);
}
// eslint-disable-next-line class-methods-use-this
toggleSubGroups(toggleGroup) {
const group = toggleGroup;
group.isOpen = !group.isOpen;
return group;
}
}
......@@ -167,8 +167,8 @@
if the name does not exist this function will return `null`
otherwise it will return the value of the param key provided
*/
w.gl.utils.getParameterByName = (name) => {
const url = window.location.href;
w.gl.utils.getParameterByName = (name, parseUrl) => {
const url = parseUrl || window.location.href;
name = name.replace(/[[\]]/g, '\\$&');
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
const results = regex.exec(url);
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -39,10 +39,6 @@ import './shortcuts_network';
// behaviors
import './behaviors/';
// blob
import './blob/create_branch_dropdown';
import './blob/target_branch_dropdown';
// templates
import './templates/issuable_template_selector';
import './templates/issuable_template_selectors';
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */
(function() {
this.NewCommitForm = (function() {
function NewCommitForm(form, targetBranchName = 'target_branch') {
function NewCommitForm(form) {
this.form = form;
this.targetBranchName = targetBranchName;
this.renderDestination = this.renderDestination.bind(this);
this.targetBranchDropdown = form.find('button.js-target-branch');
this.branchName = form.find('.js-branch-name');
this.originalBranch = form.find('.js-original-branch');
this.createMergeRequest = form.find('.js-create-merge-request');
this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
this.targetBranchDropdown.on('change.branch', this.renderDestination);
this.branchName.keyup(this.renderDestination);
this.renderDestination();
}
NewCommitForm.prototype.renderDestination = function() {
var different;
var targetBranch = this.form.find(`input[name="${this.targetBranchName}"]`);
different = targetBranch.val() !== this.originalBranch.val();
different = this.branchName.val() !== this.originalBranch.val();
if (different) {
this.createMergeRequestContainer.show();
if (!this.wasDifferent) {
......
......@@ -16,6 +16,7 @@
/* global Flash */
import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
export default {
props: {
......@@ -31,6 +32,10 @@ export default {
},
},
mixins: [
tooltipMixin,
],
data() {
return {
isLoading: false,
......@@ -127,9 +132,10 @@ export default {
<template>
<div class="dropdown">
<button
ref="tooltip"
:class="triggerButtonClass"
@click="onClickStage"
class="mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button"
class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button"
:title="stage.title"
data-placement="top"
data-toggle="dropdown"
......
......@@ -284,9 +284,7 @@ export default {
<table-pagination
v-if="shouldRenderPagination"
:pagenum="pagenum"
:change="change"
:count="state.count.all"
:pageInfo="state.pageInfo"
/>
</div>
......
.awards {
display: flex;
flex-wrap: wrap;
.emoji-icon {
width: 20px;
height: 20px;
......@@ -100,7 +103,6 @@
.award-menu-holder {
display: inline-block;
position: absolute;
.tooltip {
white-space: nowrap;
......@@ -108,9 +110,11 @@
}
.award-control {
margin: 0 5px 6px 0;
margin: 4px 8px 4px 0;
outline: 0;
position: relative;
display: block;
float: left;
&.disabled {
cursor: default;
......
......@@ -48,6 +48,10 @@
@include chevron-active;
border-color: $gray-darkest;
}
[data-toggle="dropdown"] {
outline: 0;
}
}
.dropdown-toggle {
......@@ -109,6 +113,7 @@
&:focus:active {
@include chevron-active;
border-color: $dropdown-toggle-active-border-color;
outline: 0;
}
}
......@@ -201,6 +206,11 @@
width: 100%;
}
&.dropdown-open-left {
right: 0;
left: auto;
}
&.is-loading {
.dropdown-content {
display: none;
......@@ -261,7 +271,14 @@
text-transform: capitalize;
}
.separator + .dropdown-header {
.dropdown-bold-header {
font-weight: 600;
line-height: 22px;
padding: 0 16px;
}
.separator + .dropdown-header,
.separator + .dropdown-bold-header {
padding-top: 2px;
}
......
......@@ -270,3 +270,103 @@ ul.controls {
ul.indent-list {
padding: 10px 0 0 30px;
}
// Specific styles for tree list
.group-list-tree {
.folder-toggle-wrap {
float: left;
line-height: $list-text-height;
font-size: 0;
span {
font-size: $gl-font-size;
}
}
.folder-caret,
.folder-icon {
display: inline-block;
}
.folder-caret {
width: 15px;
}
.folder-icon {
width: 20px;
}
> .group-row:not(.has-subgroups) {
.folder-caret .fa {
opacity: 0;
}
}
.content-list li:last-child {
padding-bottom: 0;
}
.group-list-tree {
margin-bottom: 0;
margin-left: 30px;
position: relative;
&::before {
content: '';
display: block;
width: 0;
position: absolute;
top: 5px;
bottom: 0;
left: -16px;
border-left: 2px solid $border-white-normal;
}
.group-row {
position: relative;
&::before {
content: "";
display: block;
width: 10px;
height: 0;
border-top: 2px solid $border-white-normal;
position: absolute;
top: 30px;
left: -16px;
}
&:last-child::before {
background: $white-light;
height: auto;
top: 30px;
bottom: 0;
}
}
}
.group-row {
padding: 0;
border: none;
}
.group-row-contents {
padding: 10px 10px 8px;
border-top: solid 1px transparent;
border-bottom: solid 1px $white-normal;
&:hover {
border-color: $row-hover-border;
background-color: $row-hover;
cursor: pointer;
}
}
}
.js-groups-list-holder {
.groups-list-loading {
font-size: 34px;
text-align: center;
}
}
......@@ -45,7 +45,8 @@
li {
display: flex;
a {
a,
.btn-link {
padding: $gl-btn-padding;
padding-bottom: 11px;
font-size: 14px;
......@@ -67,7 +68,29 @@
}
}
&.active a {
.btn-link {
padding-top: 16px;
padding-left: 15px;
padding-right: 15px;
border-left: none;
border-right: none;
border-top: none;
border-radius: 0;
&:hover,
&:active,
&:focus {
background-color: transparent;
}
&:active {
outline: 0;
box-shadow: none;
}
}
&.active a,
&.active .btn-link {
border-bottom: 2px solid $link-underline-blue;
color: $black;
font-weight: 600;
......
......@@ -19,10 +19,6 @@
.table-section {
white-space: nowrap;
.branch-commit {
max-width: 100%;
}
$section-widths: 10 15 20 25 30 40;
@each $width in $section-widths {
&.section-#{$width} {
......@@ -87,4 +83,9 @@
@media (min-width: $screen-md-min) {
flex: 0 0 90%;
}
.avatar {
float: none;
margin-right: 4px;
}
}
......@@ -127,9 +127,51 @@
@media (min-width: $screen-sm-min) {
width: 400px;
}
&.is-expandable {
.board-header {
cursor: pointer;
}
}
&.is-collapsed {
width: 50px;
.board-header {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.board-title {
position: initial;
padding: 0;
border-bottom: 0;
> span {
display: block;
transform: rotate(90deg) translate(25px, 0);
}
}
.board-title-expandable-toggle {
position: absolute;
top: 50%;
left: 50%;
margin-left: -10px;
}
.board-list-component,
.board-issue-count-holder {
display: none;
}
}
}
.board-inner {
position: relative;
height: 100%;
font-size: $issue-boards-font-size;
background: $gray-light;
......
......@@ -71,7 +71,9 @@
height: 35px;
display: flex;
justify-content: flex-end;
border-bottom: 1px outset $white-light;
background: $gray-light;
border: 1px solid $gray-normal;
color: $gl-text-color;
.truncated-info {
margin: 0 auto;
......@@ -82,7 +84,7 @@
}
.raw-link {
color: inherit;
color: $gl-text-color;
margin-left: 5px;
text-decoration: underline;
}
......@@ -93,17 +95,25 @@
display: flex;
align-self: center;
font-size: 15px;
margin-bottom: 4px;
svg {
height: 15px;
display: block;
fill: $white-light;
fill: $gl-text-color;
}
a,
.controllers-buttons,
.btn-scroll {
margin: 0 8px;
color: $white-light;
color: $gl-text-color;
height: 15px;
vertical-align: middle;
padding: 0;
width: 12px;
}
.controllers-buttons {
margin: 1px 10px;
}
.btn-scroll.animate {
......@@ -137,9 +147,9 @@
top: 35px;
left: 10px;
bottom: 0;
overflow-y: hidden;
padding-bottom: 20px;
padding-right: 20px;
overflow-y: scroll;
overflow-x: hidden;
padding: 10px 20px 20px 5px;
}
.environment-information {
......
......@@ -228,7 +228,7 @@
margin: 10px 0;
background: $gray-light;
display: none;
white-space: pre-line;
white-space: pre-wrap;
word-break: normal;
pre {
......
......@@ -285,7 +285,7 @@
border-top: 1px solid $border-color;
.environment-action-buttons {
padding: 10px;
padding: 10px 5px;
display: flex;
.btn {
......@@ -293,15 +293,20 @@
}
> .btn-group,
.external-url,
.btn {
> .external-url,
> .btn {
flex: 1;
flex-basis: 28px;
margin: 0 5px;
}
.dropdown-new {
width: 100%;
}
.dropdown-menu {
min-width: initial;
}
}
}
}
......
......@@ -58,7 +58,7 @@
}
.emoji-block {
padding: 10px 0 4px;
padding: 10px 0;
}
}
......@@ -728,6 +728,36 @@
}
}
.confidential-issue-warning {
background-color: $gl-gray;
border-radius: 3px;
padding: $gl-btn-padding $gl-padding;
margin-top: $gl-padding-top;
font-size: 14px;
color: $white-light;
.fa {
margin-right: 8px;
}
a {
color: $white-light;
text-decoration: underline;
}
&.affix {
position: static;
width: initial;
@media (min-width: $screen-sm-min) {
position: sticky;
position: -webkit-sticky;
top: 60px;
z-index: 200;
}
}
}
.add-issuable-form-input-wrapper {
height: auto;
padding: $gl-vert-padding $gl-vert-padding 0 $gl-input-padding;
......
......@@ -267,14 +267,19 @@ ul.related-merge-requests > li {
}
@media (min-width: $screen-sm-min) {
.new-branch-col {
padding-top: 0;
text-align: right;
}
.emoji-block .row {
display: flex;
.create-mr-dropdown-wrap {
.btn-group:not(.hide) {
display: inline-block;
.new-branch-col {
padding-top: 0;
text-align: right;
align-self: center;
}
.create-mr-dropdown-wrap {
.btn-group:not(.hide) {
display: inline-block;
}
}
}
}
......
......@@ -128,6 +128,7 @@
a {
width: 100%;
font-size: 18px;
margin-right: 0;
&:hover {
border: 1px solid transparent;
......@@ -140,6 +141,7 @@
a {
border: none;
border-bottom: 2px solid $link-underline-blue;
margin-right: 0;
color: $black;
&:hover {
......
......@@ -3,6 +3,41 @@
border-bottom: 1px solid $border-color;
}
.project-member-tabs {
background: $gray-light;
border: 1px solid $border-color;
li {
width: 50%;
&.active {
background: $white-light;
}
&:first-child {
border-right: 1px solid $border-color;
}
a {
width: 100%;
text-align: center;
}
}
}
.users-project-form {
.btn-create {
margin-right: 10px;
}
}
.project-member-tab-content {
padding: $gl-padding;
border: 1px solid $border-color;
border-top: 0;
margin-bottom: $gl-padding;
}
.member {
&.is-overriden {
.btn-ldap-override {
......
......@@ -103,41 +103,6 @@
}
}
.confidential-issue-warning {
background-color: $gray-normal;
border-radius: 3px;
padding: 3px 12px;
margin: auto;
margin-top: 0;
text-align: center;
font-size: 12px;
align-items: center;
@media (max-width: $screen-md-max) {
// On smaller devices the warning becomes the fourth item in the list,
// rather than centering, and grows to span the full width of the
// comment area.
order: 4;
margin: 6px auto;
width: 100%;
}
.fa {
margin-right: 8px;
}
}
.right-sidebar-expanded {
.confidential-issue-warning {
// When the sidebar is open the warning becomes the fourth item in the list,
// rather than centering, and grows to span the full width of the
// comment area.
order: 4;
margin: 6px auto;
width: 100%;
}
}
.discussion-form {
padding: $gl-padding-top $gl-padding $gl-padding;
background-color: $white-light;
......
......@@ -38,9 +38,12 @@ ul.notes {
}
.discussion {
overflow: hidden;
display: block;
position: relative;
.diff-content {
overflow: visible;
}
}
> li {
......@@ -443,6 +446,52 @@ ul.notes {
.note-action-button {
margin-left: 8px;
}
.more-actions-toggle {
margin-left: 2px;
}
}
.more-actions {
display: inline;
.tooltip {
white-space: nowrap;
}
}
.more-actions-toggle {
padding: 0;
&:hover .icon,
&:focus .icon {
color: $blue-600;
}
.icon {
padding: 0 6px;
}
}
.more-actions-dropdown {
width: 180px;
min-width: 180px;
margin-top: $gl-btn-padding;
li > a,
li > .btn {
color: $gl-text-color;
padding: $gl-btn-padding;
width: 100%;
text-align: left;
&:hover,
&:focus {
color: $gl-text-color;
background-color: $blue-25;
border-radius: $border-radius-default;
}
}
}
.discussion-actions {
......
......@@ -793,8 +793,7 @@ a.allowed-to-push {
}
.project-refs-form .dropdown-menu,
.dropdown-menu-projects,
.dropdown-menu-branches {
.dropdown-menu-projects {
width: 300px;
@media (min-width: $screen-sm-min) {
......
class Admin::DeployKeysController < Admin::ApplicationController
before_action :deploy_keys, only: [:index]
before_action :deploy_key, only: [:destroy]
before_action :deploy_key, only: [:destroy, :edit, :update]
def index
end
......@@ -10,12 +10,24 @@ class Admin::DeployKeysController < Admin::ApplicationController
end
def create
@deploy_key = deploy_keys.new(deploy_key_params.merge(user: current_user))
@deploy_key = deploy_keys.new(create_params.merge(user: current_user))
if @deploy_key.save
redirect_to admin_deploy_keys_path
else
render "new"
render 'new'
end
end
def edit
end
def update
if deploy_key.update_attributes(update_params)
flash[:notice] = 'Deploy key was successfully updated.'
redirect_to admin_deploy_keys_path
else
render 'edit'
end
end
......@@ -38,7 +50,11 @@ class Admin::DeployKeysController < Admin::ApplicationController
@deploy_keys ||= DeployKey.are_public
end
def deploy_key_params
def create_params
params.require(:deploy_key).permit(:key, :title, :can_push)
end
def update_params
params.require(:deploy_key).permit(:title, :can_push)
end
end
module CreatesCommit
extend ActiveSupport::Concern
def set_start_branch_to_branch_name
branch_exists = @repository.find_branch(@branch_name)
@start_branch = @branch_name if branch_exists
end
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
if can?(current_user, :push_code, @project)
@project_to_commit_into = @project
......
......@@ -56,9 +56,14 @@ module MembershipActions
log_audit_event(member, action: :destroy) unless member.request?
redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
respond_to do |format|
format.html do
redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
redirect_to redirect_path, notice: notice
end
redirect_to redirect_path, notice: notice
format.json { render json: { notice: notice } }
end
end
protected
......
......@@ -46,8 +46,10 @@ module MilestoneActions
def milestone_redirect_path
if @project
namespace_project_milestone_path(@project.namespace, @project, @milestone)
else
elsif @group
group_milestone_path(@group, @milestone.safe_title, title: @milestone.title)
else
dashboard_milestone_path(@milestone.safe_title, title: @milestone.title)
end
end
end
class Dashboard::GroupsController < Dashboard::ApplicationController
def index
@group_members = current_user.group_members.includes(source: :route).joins(:group)
@group_members = @group_members.merge(Group.search(params[:filter_groups])) if params[:filter_groups].present?
@group_members = @group_members.merge(Group.sort(@sort = params[:sort]))
@group_members = @group_members.page(params[:page])
@groups =
if params[:parent_id] && Group.supports_nested_groups?
parent = Group.find_by(id: params[:parent_id])
if can?(current_user, :read_group, parent)
GroupsFinder.new(current_user, parent: parent).execute
else
Group.none
end
else
current_user.groups
end
@groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present?
@groups = @groups.includes(:route)
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.page(params[:page])
respond_to do |format|
format.html
format.json do
render json: {
html: view_to_html_string("dashboard/groups/_groups", locals: { group_members: @group_members })
}
render json: GroupSerializer
.new(current_user: @current_user)
.with_pagination(request, response)
.represent(@groups)
end
end
end
......
class Dashboard::MilestonesController < Dashboard::ApplicationController
include MilestoneActions
before_action :projects
before_action :milestone, only: [:show]
before_action :milestone, only: [:show, :merge_requests, :participants, :labels]
def index
respond_to do |format|
......
......@@ -39,7 +39,7 @@ class JwtController < ApplicationController
errors: [
{ code: 'UNAUTHORIZED',
message: "HTTP Basic: Access denied\n" \
"You have 2FA enabled, please use a personal access token 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}" }
]
}, status: 401
......
......@@ -26,8 +26,6 @@ class Projects::BlobController < Projects::ApplicationController
end
def create
set_start_branch_to_branch_name
create_commit(Files::CreateService, success_notice: "The file has been successfully created.",
success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@branch_name, @file_path)) },
failure_view: :new,
......@@ -55,7 +53,7 @@ class Projects::BlobController < Projects::ApplicationController
def edit
if can_collaborate_with_project?
blob.load_all_data!(@repository)
blob.load_all_data!
else
redirect_to action: 'show'
end
......@@ -74,7 +72,7 @@ class Projects::BlobController < Projects::ApplicationController
def preview
@content = params[:content]
@blob.load_all_data!(@repository)
@blob.load_all_data!
diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true)
diff_lines = diffy.diff.scan(/.*\n/)[2..-1]
diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines)
......@@ -93,9 +91,11 @@ class Projects::BlobController < Projects::ApplicationController
def diff
apply_diff_view_cookie!
@form = UnfoldForm.new(params)
@lines = Gitlab::Highlight.highlight_lines(repository, @ref, @path)
@lines = @lines[@form.since - 1..@form.to - 1]
@blob.load_all_data!
@lines = Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: @repository).lines
@form = UnfoldForm.new(params)
@lines = @lines[@form.since - 1..@form.to - 1].map(&:html_safe)
if @form.bottom?
@match_line = ''
......@@ -111,7 +111,7 @@ class Projects::BlobController < Projects::ApplicationController
private
def blob
@blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path), @project)
@blob ||= @repository.blob_at(@commit.id, @path)
if @blob
@blob
......
......@@ -5,7 +5,9 @@ module Projects
before_action :authorize_read_list!, only: [:index]
def index
render json: serialize_as_json(board.lists)
lists = ::Boards::Lists::ListService.new(project, current_user).execute(board)
render json: serialize_as_json(lists)
end
def create
......
......@@ -10,10 +10,10 @@ class Projects::BranchesController < Projects::ApplicationController
def index
@sort = params[:sort].presence || sort_value_name
@branches = BranchesFinder.new(@repository, params).execute
@branches = Kaminari.paginate_array(@branches).page(params[:page])
respond_to do |format|
format.html do
paginate_branches
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
@max_commits = @branches.reduce(0) do |memo, branch|
......@@ -22,7 +22,6 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
format.json do
paginate_branches unless params[:show_all]
render json: @branches.map(&:name)
end
end
......@@ -106,10 +105,6 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
def paginate_branches
@branches = Kaminari.paginate_array(@branches).page(params[:page])
end
def url_to_autodeploy_setup(project, branch_name)
namespace_project_new_blob_path(
project.namespace,
......
......@@ -4,6 +4,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project!
before_action :authorize_update_deploy_key!, only: [:edit, :update]
layout "project_settings"
......@@ -21,7 +22,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def create
@key = DeployKey.new(deploy_key_params.merge(user: current_user))
@key = DeployKey.new(create_params.merge(user: current_user))
unless @key.valid? && @project.deploy_keys << @key
flash[:alert] = @key.errors.full_messages.join(', ').html_safe
......@@ -32,6 +33,18 @@ class Projects::DeployKeysController < Projects::ApplicationController
redirect_to_repository_settings(@project)
end
def edit
end
def update
if deploy_key.update_attributes(update_params)
flash[:notice] = 'Deploy key was successfully updated.'
redirect_to_repository_settings(@project)
else
render 'edit'
end
end
def enable
load_key
Projects::EnableDeployKeyService.new(@project, current_user, params).execute
......@@ -59,10 +72,22 @@ class Projects::DeployKeysController < Projects::ApplicationController
protected
def deploy_key_params
def deploy_key
@deploy_key ||= @project.deploy_keys.find(params[:id])
end
def create_params
params.require(:deploy_key).permit(:key, :title, :can_push)
end
def update_params
params.require(:deploy_key).permit(:title, :can_push)
end
def authorize_update_deploy_key!
access_denied! unless can?(current_user, :update_deploy_key, deploy_key)
end
def log_audit_event(key_title, options = {})
AuditEventService.new(current_user, @project, options)
.for_deploy_key(key_title).security_event
......
......@@ -104,7 +104,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
def render_missing_personal_token
render plain: "HTTP Basic: Access denied\n" \
"You have 2FA enabled, please use a personal access token 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}",
status: 401
end
......
......@@ -8,7 +8,7 @@ class Projects::LabelsController < Projects::ApplicationController
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update,
:generate, :destroy, :remove_priority,
:set_priorities]
before_action :authorize_admin_group!, only: [:promote]
before_action :authorize_admin_group_labels!, only: [:promote]
respond_to :js, :html
......@@ -161,7 +161,7 @@ class Projects::LabelsController < Projects::ApplicationController
return render_404 unless can?(current_user, :admin_label, @project)
end
def authorize_admin_group!
return render_404 unless can?(current_user, :admin_group, @project.group)
def authorize_admin_group_labels!
return render_404 unless can?(current_user, :admin_label, @project.group)
end
end
......@@ -43,7 +43,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
if schedule.update(owner: current_user)
redirect_to pipeline_schedules_path(@project)
else
redirect_to pipeline_schedules_path(@project), alert: "Failed to change the owner"
redirect_to pipeline_schedules_path(@project), alert: _("Failed to change the owner")
end
end
......@@ -53,7 +53,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
else
redirect_to pipeline_schedules_path(@project),
status: 302,
alert: "Failed to remove the pipeline schedule"
alert: _("Failed to remove the pipeline schedule")
end
end
......
......@@ -36,7 +36,6 @@ class Projects::TreeController < Projects::ApplicationController
def create_dir
return render_404 unless @commit_params.values.all?
set_start_branch_to_branch_name
create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.",
success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@branch_name, @dir_name)),
failure_path: namespace_project_tree_path(@project.namespace, @project, @ref))
......
......@@ -8,7 +8,7 @@ module GroupsHelper
group = Group.find_by_full_path(group)
end
group.try(:avatar_url) || image_path('no_group_avatar.png')
group.try(:avatar_url) || ActionController::Base.helpers.image_path('no_group_avatar.png')
end
def group_title(group, name = nil, url = nil)
......
......@@ -138,6 +138,8 @@ module MilestonesHelper
merge_requests_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
merge_requests_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
else
merge_requests_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
end
end
......@@ -146,6 +148,8 @@ module MilestonesHelper
participants_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
participants_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
else
participants_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
end
end
......@@ -154,6 +158,8 @@ module MilestonesHelper
labels_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
labels_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
else
labels_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
end
end
end
......@@ -13,7 +13,7 @@ module NavHelper
else
"page-gutter right-sidebar-expanded"
end
elsif current_path?('builds#show')
elsif current_path?('jobs#show')
"page-gutter build-sidebar right-sidebar-expanded"
elsif current_path?('wikis#show') ||
current_path?('wikis#edit') ||
......
......@@ -90,14 +90,18 @@ module NotesHelper
end
end
def note_url(note)
def note_url(note, project = @project)
if note.noteable.is_a?(PersonalSnippet)
snippet_note_path(note.noteable, note)
else
namespace_project_note_path(@project.namespace, @project, note)
namespace_project_note_path(project.namespace, project, note)
end
end
def noteable_note_url(note)
Gitlab::UrlBuilder.build(note)
end
def form_resources
if @snippet.is_a?(PersonalSnippet)
[@note]
......
......@@ -146,7 +146,7 @@ module ProjectsHelper
end
options = options_for_select(
options,
options.invert,
selected: highest_available_option || @project.project_feature.public_send(field),
disabled: disabled_option
)
......@@ -485,9 +485,9 @@ module ProjectsHelper
def project_feature_options
{
s_('ProjectFeature|Disabled') => ProjectFeature::DISABLED,
s_('ProjectFeature|Only team members') => ProjectFeature::PRIVATE,
s_('ProjectFeature|Everyone with access') => ProjectFeature::ENABLED
ProjectFeature::DISABLED => s_('ProjectFeature|Disabled'),
ProjectFeature::PRIVATE => s_('ProjectFeature|Only team members'),
ProjectFeature::ENABLED => s_('ProjectFeature|Everyone with access')
}
end
......
class Approver < ActiveRecord::Base
belongs_to :target, polymorphic: true
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user
validates :user, presence: true
......
class ApproverGroup < ActiveRecord::Base
belongs_to :target, polymorphic: true
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :group
validates :group, presence: true
......
......@@ -5,7 +5,7 @@ class AwardEmoji < ActiveRecord::Base
include Participable
include GhostUser
belongs_to :awardable, polymorphic: true
belongs_to :awardable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user
validates :awardable, :user, presence: true
......
......@@ -94,6 +94,10 @@ class Blob < SimpleDelegator
end
end
def load_all_data!
super(project.repository) if project
end
def no_highlighting?
raw_size && raw_size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
end
......@@ -151,6 +155,10 @@ class Blob < SimpleDelegator
@extension ||= extname.downcase.delete('.')
end
def file_type
Gitlab::FileDetector.type_of(path)
end
def video?
UploaderHelper::VIDEO_EXT.include?(extension)
end
......@@ -176,16 +184,19 @@ class Blob < SimpleDelegator
end
def rendered_as_text?(ignore_errors: true)
simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?)
simple_viewer.is_a?(BlobViewer::Text) && (ignore_errors || simple_viewer.render_error.nil?)
end
def show_viewer_switcher?
rendered_as_text? && rich_viewer
end
def expanded?
!!@expanded
end
def expand!
simple_viewer&.expanded = true
rich_viewer&.expanded = true
@expanded = true
end
private
......
......@@ -6,15 +6,15 @@ module BlobViewer
self.loading_partial_name = 'loading'
delegate :partial_path, :loading_partial_path, :rich?, :simple?, :text?, :binary?, to: :class
delegate :partial_path, :loading_partial_path, :rich?, :simple?, :load_async?, :text?, :binary?, to: :class
attr_reader :blob
attr_accessor :expanded
delegate :project, to: :blob
def initialize(blob)
@blob = blob
@initially_binary = blob.binary?
end
def self.partial_path
......@@ -52,19 +52,15 @@ module BlobViewer
def self.can_render?(blob, verify_binary: true)
return false if verify_binary && binary? != blob.binary?
return true if extensions&.include?(blob.extension)
return true if file_types&.include?(Gitlab::FileDetector.type_of(blob.path))
return true if file_types&.include?(blob.file_type)
false
end
def load_async?
self.class.load_async? && render_error.nil?
end
def collapsed?
return @collapsed if defined?(@collapsed)
@collapsed = !expanded && collapse_limit && blob.raw_size > collapse_limit
@collapsed = !blob.expanded? && collapse_limit && blob.raw_size > collapse_limit
end
def too_large?
......@@ -73,6 +69,10 @@ module BlobViewer
@too_large = size_limit && blob.raw_size > size_limit
end
def binary_detected_after_load?
!@initially_binary && blob.binary?
end
# This method is used on the server side to check whether we can attempt to
# render the blob at all. Human-readable error messages are found in the
# `BlobHelper#blob_render_error_reason` helper.
......
......@@ -4,6 +4,5 @@ module BlobViewer
include ServerSide
self.partial_name = 'empty'
self.binary = true
end
end
......@@ -9,9 +9,7 @@ module BlobViewer
end
def prepare!
if blob.project
blob.load_all_data!(blob.project.repository)
end
blob.load_all_data!
end
def render_error
......
......@@ -6,6 +6,10 @@ class Board < ActiveRecord::Base
validates :name, :project, presence: true
def backlog_list
lists.merge(List.backlog).take
end
def closed_list
lists.merge(List.closed).take
end
......
......@@ -114,16 +114,16 @@ class Commit
#
# Usually, the commit title is the first line of the commit message.
# In case this first line is longer than 100 characters, it is cut off
# after 80 characters and ellipses (`&hellp;`) are appended.
# after 80 characters + `...`
def title
full_title.length > 100 ? full_title[0..79] << "…" : full_title
return full_title if full_title.length < 100
full_title.truncate(81, separator: ' ', omission: '…')
end
# Returns the full commits title
def full_title
return @full_title if @full_title
@full_title =
@full_title ||=
if safe_message.blank?
no_commit_message
else
......@@ -131,19 +131,14 @@ class Commit
end
end
# Returns the commits description
#
# cut off, ellipses (`&hellp;`) are prepended to the commit message.
# Returns full commit message if title is truncated (greater than 99 characters)
# otherwise returns commit message without first line
def description
title_end = safe_message.index("\n")
@description ||=
if (!title_end && safe_message.length > 100) || (title_end && title_end > 100)
"…" << safe_message[80..-1]
else
safe_message.split("\n", 2)[1].try(:chomp)
end
end
return safe_message if full_title.length >= 100
safe_message.split("\n", 2)[1].try(:chomp)
end
def description?
description.present?
end
......@@ -326,12 +321,11 @@ class Commit
end
def raw_diffs(*args)
# Uncomment when https://gitlab.com/gitlab-org/gitaly/merge_requests/170 is merged
# if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
# Gitlab::GitalyClient::Commit.new(project.repository).diff_from_parent(self, *args)
# else
raw.diffs(*args)
# end
if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
Gitlab::GitalyClient::Commit.new(project.repository).diff_from_parent(self, *args)
else
raw.diffs(*args)
end
end
def raw_deltas
......
......@@ -4,7 +4,7 @@ class Deployment < ActiveRecord::Base
belongs_to :project, required: true, validate: true
belongs_to :environment, required: true, validate: true
belongs_to :user
belongs_to :deployable, polymorphic: true
belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
validates :sha, presence: true
validates :ref, presence: true
......
......@@ -47,7 +47,7 @@ class Event < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :project
belongs_to :target, polymorphic: true
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
# For Hash only
serialize :data # rubocop:disable Cop/ActiverecordSerialize
......
class LabelLink < ActiveRecord::Base
include Importable
belongs_to :target, polymorphic: true
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :label
validates :target, presence: true, unless: :importing?
......
......@@ -2,7 +2,7 @@ class List < ActiveRecord::Base
belongs_to :board
belongs_to :label
enum list_type: { label: 1, closed: 2 }
enum list_type: { backlog: 0, label: 1, closed: 2 }
validates :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label?
......
......@@ -9,7 +9,7 @@ class Member < ActiveRecord::Base
belongs_to :created_by, class_name: "User"
belongs_to :user
belongs_to :source, polymorphic: true
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
delegate :name, :username, :email, to: :user, prefix: true
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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