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. ...@@ -8,13 +8,10 @@ entry.
## 9.2.4 (2017-06-02) ## 9.2.4 (2017-06-02)
- No changes.
- Fix visibility when referencing snippets. - Fix visibility when referencing snippets.
## 9.2.3 (2017-05-31) ## 9.2.3 (2017-05-31)
- No changes.
- No changes.
- Move uploads from 'public/uploads' to 'public/uploads/system'. - Move uploads from 'public/uploads' to 'public/uploads/system'.
- Escapes html content before appending it to the DOM. - Escapes html content before appending it to the DOM.
- Restrict API X-Frame-Options to same origin. - Restrict API X-Frame-Options to same origin.
......
...@@ -2,6 +2,7 @@ source 'https://rubygems.org' ...@@ -2,6 +2,7 @@ source 'https://rubygems.org'
gem 'rails', '4.2.8' gem 'rails', '4.2.8'
gem 'rails-deprecated_sanitizer', '~> 1.0.3' gem 'rails-deprecated_sanitizer', '~> 1.0.3'
gem 'bootsnap', '~> 1.0.0'
# Responders respond_to and respond_with # Responders respond_to and respond_with
gem 'responders', '~> 2.0' gem 'responders', '~> 2.0'
...@@ -17,7 +18,7 @@ gem 'pg', '~> 0.18.2', group: :postgres ...@@ -17,7 +18,7 @@ gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.25.1.1' gem 'rugged', '~> 0.25.1.1'
gem 'faraday', '~> 0.11.0' gem 'faraday', '~> 0.12'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
...@@ -268,7 +269,7 @@ gem 'sentry-raven', '~> 2.4.0' ...@@ -268,7 +269,7 @@ gem 'sentry-raven', '~> 2.4.0'
gem 'premailer-rails', '~> 1.9.0' gem 'premailer-rails', '~> 1.9.0'
# I18n # 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', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0' gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development gem 'gettext', '~> 3.2.2', require: false, group: :development
...@@ -379,7 +380,7 @@ gem 'html2text' ...@@ -379,7 +380,7 @@ gem 'html2text'
gem 'ruby-prof', '~> 0.16.2' gem 'ruby-prof', '~> 0.16.2'
# OAuth # OAuth
gem 'oauth2', '~> 1.3.0' gem 'oauth2', '~> 1.4'
# Soft deletion # Soft deletion
gem 'paranoia', '~> 2.2' gem 'paranoia', '~> 2.2'
......
...@@ -91,6 +91,8 @@ GEM ...@@ -91,6 +91,8 @@ GEM
bindata (2.3.5) bindata (2.3.5)
binding_of_caller (0.7.2) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.0.0)
msgpack (~> 1.0)
bootstrap-sass (3.3.6) bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1) autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4) sass (>= 3.3.4)
...@@ -215,7 +217,7 @@ GEM ...@@ -215,7 +217,7 @@ GEM
factory_girl_rails (4.7.0) factory_girl_rails (4.7.0)
factory_girl (~> 4.7.0) factory_girl (~> 4.7.0)
railties (>= 3.0.0) railties (>= 3.0.0)
faraday (0.11.0) faraday (0.12.1)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
faraday_middleware (0.11.0.1) faraday_middleware (0.11.0.1)
faraday (>= 0.7.4, < 1.0) faraday (>= 0.7.4, < 1.0)
...@@ -490,6 +492,7 @@ GEM ...@@ -490,6 +492,7 @@ GEM
minitest (5.7.0) minitest (5.7.0)
mmap2 (2.2.6) mmap2 (2.2.6)
mousetrap-rails (1.4.6) mousetrap-rails (1.4.6)
msgpack (1.1.0)
multi_json (1.12.1) multi_json (1.12.1)
multi_xml (0.6.0) multi_xml (0.6.0)
multipart-post (2.0.0) multipart-post (2.0.0)
...@@ -505,8 +508,8 @@ GEM ...@@ -505,8 +508,8 @@ GEM
mini_portile2 (~> 2.1.0) mini_portile2 (~> 2.1.0)
numerizer (0.1.1) numerizer (0.1.1)
oauth (0.5.1) oauth (0.5.1)
oauth2 (1.3.1) oauth2 (1.4.0)
faraday (>= 0.8, < 0.12) faraday (>= 0.8, < 0.13)
jwt (~> 1.0) jwt (~> 1.0)
multi_json (~> 1.3) multi_json (~> 1.3)
multi_xml (~> 0.5) multi_xml (~> 0.5)
...@@ -716,7 +719,7 @@ GEM ...@@ -716,7 +719,7 @@ GEM
retriable (1.4.1) retriable (1.4.1)
rinku (2.0.0) rinku (2.0.0)
rotp (2.1.2) rotp (2.1.2)
rouge (2.0.7) rouge (2.1.0)
rqrcode (0.7.0) rqrcode (0.7.0)
chunky_png chunky_png
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
...@@ -764,7 +767,7 @@ GEM ...@@ -764,7 +767,7 @@ GEM
ruby-progressbar (1.8.1) ruby-progressbar (1.8.1)
ruby-saml (1.4.1) ruby-saml (1.4.1)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
ruby_parser (3.8.4) ruby_parser (3.9.0)
sexp_processor (~> 4.1) sexp_processor (~> 4.1)
rubyntlm (0.5.2) rubyntlm (0.5.2)
rubypants (0.2.0) rubypants (0.2.0)
...@@ -797,7 +800,7 @@ GEM ...@@ -797,7 +800,7 @@ GEM
sentry-raven (2.4.0) sentry-raven (2.4.0)
faraday (>= 0.7.6, < 1.0) faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9) settingslogic (2.0.9)
sexp_processor (4.8.0) sexp_processor (4.9.0)
sham_rack (1.3.6) sham_rack (1.3.6)
rack rack
shoulda-matchers (2.8.0) shoulda-matchers (2.8.0)
...@@ -950,6 +953,7 @@ DEPENDENCIES ...@@ -950,6 +953,7 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0) benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0) better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
bootsnap (~> 1.0.0)
bootstrap-sass (~> 3.3.0) bootstrap-sass (~> 3.3.0)
brakeman (~> 3.6.0) brakeman (~> 3.6.0)
browser (~> 2.2) browser (~> 2.2)
...@@ -981,7 +985,7 @@ DEPENDENCIES ...@@ -981,7 +985,7 @@ DEPENDENCIES
email_reply_trimmer (~> 0.1) email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0) email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0) factory_girl_rails (~> 4.7.0)
faraday (~> 0.11.0) faraday (~> 0.12)
faraday_middleware-aws-signers-v4 faraday_middleware-aws-signers-v4
ffaker (~> 2.4) ffaker (~> 2.4)
flay (~> 2.8.0) flay (~> 2.8.0)
...@@ -1044,7 +1048,7 @@ DEPENDENCIES ...@@ -1044,7 +1048,7 @@ DEPENDENCIES
net-ldap net-ldap
net-ssh (~> 3.0.1) net-ssh (~> 3.0.1)
nokogiri (~> 1.6.7, >= 1.6.7.2) nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.3.0) oauth2 (~> 1.4)
octokit (~> 4.6.2) octokit (~> 4.6.2)
oj (~> 2.17.4) oj (~> 2.17.4)
omniauth (~> 1.4.2) omniauth (~> 1.4.2)
...@@ -1105,7 +1109,7 @@ DEPENDENCIES ...@@ -1105,7 +1109,7 @@ DEPENDENCIES
rubocop-rspec (~> 1.15.0) rubocop-rspec (~> 1.15.0)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2) ruby-prof (~> 0.16.2)
ruby_parser (~> 3.8.4) ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4) rufus-scheduler (~> 3.4)
rugged (~> 0.25.1.1) rugged (~> 0.25.1.1)
sanitize (~> 2.0) sanitize (~> 2.0)
......
...@@ -88,6 +88,7 @@ function installGlEmojiElement() { ...@@ -88,6 +88,7 @@ function installGlEmojiElement() {
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0; const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
if ( if (
emojiUnicode &&
isEmojiUnicode && isEmojiUnicode &&
!isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion) !isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion)
) { ) {
......
...@@ -28,7 +28,8 @@ function isSkinToneComboEmoji(emojiUnicode) { ...@@ -28,7 +28,8 @@ function isSkinToneComboEmoji(emojiUnicode) {
// doesn't support the skin tone versions of horse racing // doesn't support the skin tone versions of horse racing
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) { 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); isSkinToneComboEmoji(emojiUnicode);
} }
......
...@@ -35,7 +35,7 @@ export default class BlobFileDropzone { ...@@ -35,7 +35,7 @@ export default class BlobFileDropzone {
this.removeFile(file); this.removeFile(file);
}); });
this.on('sending', function (file, xhr, formData) { 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('create_merge_request', form.find('.js-create-merge-request').val());
formData.append('commit_message', form.find('.js-commit-message').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 @@ $(() => { ...@@ -105,6 +105,8 @@ $(() => {
if (list.type === 'closed') { if (list.type === 'closed') {
list.position = Infinity; list.position = Infinity;
list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' }; 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 @@ $(() => { ...@@ -147,7 +149,7 @@ $(() => {
}, },
computed: { computed: {
disabled() { disabled() {
return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length; return !this.store.lists.filter(list => !list.preset).length;
}, },
tooltipTitle() { tooltipTitle() {
if (this.disabled) { if (this.disabled) {
......
/* eslint-disable comma-dangle, space-before-function-paren, one-var */ /* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* global Sortable */ /* global Sortable */
import Vue from 'vue'; import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list'; import boardList from './board_list';
import boardBlankState from './board_blank_state'; import boardBlankState from './board_blank_state';
import './board_delete'; import './board_delete';
...@@ -22,6 +23,10 @@ gl.issueBoards.Board = Vue.extend({ ...@@ -22,6 +23,10 @@ gl.issueBoards.Board = Vue.extend({
disabled: Boolean, disabled: Boolean,
issueLinkBase: String, issueLinkBase: String,
rootPath: String, rootPath: String,
boardId: {
type: String,
required: true,
},
}, },
data () { data () {
return { return {
...@@ -78,7 +83,16 @@ gl.issueBoards.Board = Vue.extend({ ...@@ -78,7 +83,16 @@ gl.issueBoards.Board = Vue.extend({
methods: { methods: {
showNewIssueForm() { showNewIssueForm() {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; 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 () { mounted () {
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
...@@ -102,4 +116,11 @@ gl.issueBoards.Board = Vue.extend({ ...@@ -102,4 +116,11 @@ gl.issueBoards.Board = Vue.extend({
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); 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({ ...@@ -32,9 +32,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({
showSidebar () { showSidebar () {
return Object.keys(this.issue).length; return Object.keys(this.issue).length;
}, },
assigneeId() {
return this.issue.assignee ? this.issue.assignee.id : 0;
},
milestoneTitle() { milestoneTitle() {
return this.issue.milestone ? this.issue.milestone.title : 'No Milestone'; return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
} }
......
...@@ -152,6 +152,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({ ...@@ -152,6 +152,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
<div class="card-assignee"> <div class="card-assignee">
<user-avatar-link <user-avatar-link
v-for="(assignee, index) in issue.assignees" v-for="(assignee, index) in issue.assignees"
:key="assignee.id"
v-if="shouldRenderAssignee(index)" v-if="shouldRenderAssignee(index)"
class="js-no-trigger" class="js-no-trigger"
:link-href="assigneeUrl(assignee)" :link-href="assigneeUrl(assignee)"
......
...@@ -26,7 +26,8 @@ gl.issueBoards.ModalFooter = Vue.extend({ ...@@ -26,7 +26,8 @@ gl.issueBoards.ModalFooter = Vue.extend({
}, },
methods: { methods: {
addIssues() { 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 selectedIssues = ModalStore.getSelectedIssues();
const issueIds = selectedIssues.map(issue => issue.globalId); const issueIds = selectedIssues.map(issue => issue.globalId);
......
...@@ -11,7 +11,7 @@ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ ...@@ -11,7 +11,7 @@ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
}, },
computed: { computed: {
selected() { selected() {
return this.modal.selectedList || this.state.lists[0]; return this.modal.selectedList || this.state.lists[1];
}, },
}, },
destroyed() { destroyed() {
......
...@@ -12,7 +12,9 @@ class List { ...@@ -12,7 +12,9 @@ class List {
this.position = obj.position; this.position = obj.position;
this.title = obj.title; this.title = obj.title;
this.type = obj.list_type; 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.page = 1;
this.loading = true; this.loading = true;
this.loadingMore = false; this.loadingMore = false;
......
...@@ -37,10 +37,14 @@ gl.issueBoards.BoardsStore = { ...@@ -37,10 +37,14 @@ gl.issueBoards.BoardsStore = {
}, },
new (listObj) { new (listObj) {
const list = this.addList(listObj); const list = this.addList(listObj);
const backlogList = this.findList('type', 'backlog', 'backlog');
list list
.save() .save()
.then(() => { .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'); this.state.lists = _.sortBy(this.state.lists, 'position');
}) })
.catch(() => { .catch(() => {
...@@ -53,7 +57,7 @@ gl.issueBoards.BoardsStore = { ...@@ -53,7 +57,7 @@ gl.issueBoards.BoardsStore = {
}, },
shouldAddBlankState () { shouldAddBlankState () {
// Decide whether to add the blank state // 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 () { addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
...@@ -106,7 +110,7 @@ gl.issueBoards.BoardsStore = { ...@@ -106,7 +110,7 @@ gl.issueBoards.BoardsStore = {
issueTo.removeLabel(listFrom.label); issueTo.removeLabel(listFrom.label);
} }
if (listTo.type === 'closed') { if (listTo.type === 'closed' && listFrom.type !== 'backlog') {
issueLists.forEach((list) => { issueLists.forEach((list) => {
list.removeIssue(issue); list.removeIssue(issue);
}); });
......
...@@ -20,6 +20,7 @@ window.Build = (function () { ...@@ -20,6 +20,7 @@ window.Build = (function () {
this.$document = $(document); this.$document = $(document);
this.logBytes = 0; this.logBytes = 0;
this.scrollOffsetPadding = 30; this.scrollOffsetPadding = 30;
this.hasBeenScrolled = false;
this.updateDropdown = this.updateDropdown.bind(this); this.updateDropdown = this.updateDropdown.bind(this);
this.getBuildTrace = this.getBuildTrace.bind(this); this.getBuildTrace = this.getBuildTrace.bind(this);
...@@ -62,6 +63,15 @@ window.Build = (function () { ...@@ -62,6 +63,15 @@ window.Build = (function () {
.off('click') .off('click')
.on('click', this.scrollToBottom.bind(this)); .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) $(window)
.off('resize.build') .off('resize.build')
.on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100)); .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
...@@ -70,25 +80,16 @@ window.Build = (function () { ...@@ -70,25 +80,16 @@ window.Build = (function () {
// eslint-disable-next-line // eslint-disable-next-line
this.getBuildTrace() this.getBuildTrace()
.then(() => this.makeTraceScrollable()) .then(() => this.toggleScroll())
.then(() => this.scrollToBottom()); .then(() => {
if (!this.hasBeenScrolled) {
this.scrollToBottom();
}
});
this.verifyTopPosition(); 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 () { Build.prototype.canScroll = function () {
return (this.$scrollContainer.prop('scrollHeight') - this.scrollOffsetPadding) > this.$scrollContainer.height(); return (this.$scrollContainer.prop('scrollHeight') - this.scrollOffsetPadding) > this.$scrollContainer.height();
}; };
...@@ -104,12 +105,11 @@ window.Build = (function () { ...@@ -104,12 +105,11 @@ window.Build = (function () {
* *
*/ */
Build.prototype.toggleScroll = function () { Build.prototype.toggleScroll = function () {
const bottomScroll = this.$scrollContainer.scrollTop() + const currentPosition = this.$scrollContainer.scrollTop();
this.scrollOffsetPadding + const bottomScroll = currentPosition + this.$scrollContainer.innerHeight();
this.$scrollContainer.height();
if (this.canScroll()) { if (this.canScroll()) {
if (this.$scrollContainer.scrollTop() === 0) { if (currentPosition === 0) {
this.toggleDisableButton(this.$scrollTopBtn, true); this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false); this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (bottomScroll === this.$scrollContainer.prop('scrollHeight')) { } else if (bottomScroll === this.$scrollContainer.prop('scrollHeight')) {
...@@ -123,12 +123,14 @@ window.Build = (function () { ...@@ -123,12 +123,14 @@ window.Build = (function () {
}; };
Build.prototype.scrollToTop = function () { Build.prototype.scrollToTop = function () {
this.$scrollContainer.getNiceScroll(0).doScrollTop(0); this.hasBeenScrolled = true;
this.$scrollContainer.scrollTop(0);
this.toggleScroll(); this.toggleScroll();
}; };
Build.prototype.scrollToBottom = function () { 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(); this.toggleScroll();
}; };
...@@ -216,7 +218,11 @@ window.Build = (function () { ...@@ -216,7 +218,11 @@ window.Build = (function () {
Build.timeout = setTimeout(() => { Build.timeout = setTimeout(() => {
//eslint-disable-next-line //eslint-disable-next-line
this.getBuildTrace() this.getBuildTrace()
.then(() => this.scrollToBottom()); .then(() => {
if (!this.hasBeenScrolled) {
this.scrollToBottom();
}
});
}, 4000); }, 4000);
} else { } else {
this.$buildRefreshAnimation.remove(); this.$buildRefreshAnimation.remove();
...@@ -238,7 +244,7 @@ window.Build = (function () { ...@@ -238,7 +244,7 @@ window.Build = (function () {
}; };
Build.prototype.toggleSidebar = function (shouldHide) { Build.prototype.toggleSidebar = function (shouldHide) {
const shouldShow = !shouldHide; const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
this.$buildTrace this.$buildTrace
.toggleClass('sidebar-expanded', shouldShow) .toggleClass('sidebar-expanded', shouldShow)
...@@ -253,7 +259,7 @@ window.Build = (function () { ...@@ -253,7 +259,7 @@ window.Build = (function () {
this.verifyTopPosition(); this.verifyTopPosition();
if (this.$scrollContainer.getNiceScroll(0)) { if (this.canScroll()) {
this.toggleScroll(); this.toggleScroll();
} }
}; };
......
// ECMAScript polyfills // ECMAScript polyfills
import 'core-js/fn/array/find'; import 'core-js/fn/array/find';
import 'core-js/fn/array/find-index';
import 'core-js/fn/array/from'; import 'core-js/fn/array/from';
import 'core-js/fn/array/includes'; import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign'; import 'core-js/fn/object/assign';
......
...@@ -80,21 +80,27 @@ ...@@ -80,21 +80,27 @@
v-if="isLoading && !hasKeys" v-if="isLoading && !hasKeys"
size="2" size="2"
label="Loading deploy keys" label="Loading deploy keys"
/> />
<div v-else-if="hasKeys"> <div v-else-if="hasKeys">
<keys-panel <keys-panel
title="Enabled deploy keys for this project" title="Enabled deploy keys for this project"
:keys="keys.enabled_keys" :keys="keys.enabled_keys"
:store="store" /> :store="store"
:endpoint="endpoint"
/>
<keys-panel <keys-panel
title="Deploy keys from projects you have access to" title="Deploy keys from projects you have access to"
:keys="keys.available_project_keys" :keys="keys.available_project_keys"
:store="store" /> :store="store"
:endpoint="endpoint"
/>
<keys-panel <keys-panel
v-if="keys.public_keys.length" v-if="keys.public_keys.length"
title="Public deploy keys available to any project" title="Public deploy keys available to any project"
:keys="keys.public_keys" :keys="keys.public_keys"
:store="store" /> :store="store"
:endpoint="endpoint"
/>
</div> </div>
</div> </div>
</template> </template>
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
type: Object, type: Object,
required: true, required: true,
}, },
endpoint: {
type: String,
required: true,
},
}, },
components: { components: {
actionBtn, actionBtn,
...@@ -19,6 +23,9 @@ ...@@ -19,6 +23,9 @@
timeagoDate() { timeagoDate() {
return gl.utils.getTimeago().format(this.deployKey.created_at); return gl.utils.getTimeago().format(this.deployKey.created_at);
}, },
editDeployKeyPath() {
return `${this.endpoint}/${this.deployKey.id}/edit`;
},
}, },
methods: { methods: {
isEnabled(id) { isEnabled(id) {
...@@ -33,7 +40,8 @@ ...@@ -33,7 +40,8 @@
<div class="pull-left append-right-10 hidden-xs"> <div class="pull-left append-right-10 hidden-xs">
<i <i
aria-hidden="true" aria-hidden="true"
class="fa fa-key key-icon"> class="fa fa-key key-icon"
>
</i> </i>
</div> </div>
<div class="deploy-key-content key-list-item-info"> <div class="deploy-key-content key-list-item-info">
...@@ -45,7 +53,8 @@ ...@@ -45,7 +53,8 @@
</div> </div>
<div <div
v-if="deployKey.can_push" v-if="deployKey.can_push"
class="write-access-allowed"> class="write-access-allowed"
>
Write access allowed Write access allowed
</div> </div>
</div> </div>
...@@ -53,7 +62,8 @@ ...@@ -53,7 +62,8 @@
<a <a
v-for="project in deployKey.projects" v-for="project in deployKey.projects"
class="label deploy-project-label" class="label deploy-project-label"
:href="project.full_path"> :href="project.full_path"
>
{{ project.full_name }} {{ project.full_name }}
</a> </a>
</div> </div>
...@@ -61,20 +71,30 @@ ...@@ -61,20 +71,30 @@
<span class="key-created-at"> <span class="key-created-at">
created {{ timeagoDate }} created {{ timeagoDate }}
</span> </span>
<a
v-if="deployKey.can_edit"
class="btn btn-small"
:href="editDeployKeyPath"
>
Edit
</a>
<action-btn <action-btn
v-if="!isEnabled(deployKey.id)" v-if="!isEnabled(deployKey.id)"
:deploy-key="deployKey" :deploy-key="deployKey"
type="enable"/> type="enable"
/>
<action-btn <action-btn
v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned" v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned"
:deploy-key="deployKey" :deploy-key="deployKey"
btn-css-class="btn-warning" btn-css-class="btn-warning"
type="remove" /> type="remove"
/>
<action-btn <action-btn
v-else v-else
:deploy-key="deployKey" :deploy-key="deployKey"
btn-css-class="btn-warning" btn-css-class="btn-warning"
type="disable" /> type="disable"
/>
</div> </div>
</div> </div>
</template> </template>
...@@ -20,6 +20,10 @@ ...@@ -20,6 +20,10 @@
type: Object, type: Object,
required: true, required: true,
}, },
endpoint: {
type: String,
required: true,
},
}, },
components: { components: {
key, key,
...@@ -34,18 +38,22 @@ ...@@ -34,18 +38,22 @@
({{ keys.length }}) ({{ keys.length }})
</h5> </h5>
<ul class="well-list" <ul class="well-list"
v-if="keys.length"> v-if="keys.length"
>
<li <li
v-for="deployKey in keys" v-for="deployKey in keys"
:key="deployKey.id"> :key="deployKey.id">
<key <key
:deploy-key="deployKey" :deploy-key="deployKey"
:store="store" /> :store="store"
:endpoint="endpoint"
/>
</li> </li>
</ul> </ul>
<div <div
class="settings-message text-center" class="settings-message text-center"
v-else-if="showHelpBox"> v-else-if="showHelpBox"
>
No deploy keys found. Create one with the form above. No deploy keys found. Create one with the form above.
</div> </div>
</div> </div>
......
...@@ -167,9 +167,6 @@ import AuditLogs from './audit_logs'; ...@@ -167,9 +167,6 @@ import AuditLogs from './audit_logs';
case 'admin:projects:index': case 'admin:projects:index':
new ProjectsList(); new ProjectsList();
break; break;
case 'dashboard:groups:index':
new GroupsList();
break;
case 'explore:groups:index': case 'explore:groups:index':
new GroupsList(); new GroupsList();
...@@ -343,25 +340,14 @@ import AuditLogs from './audit_logs'; ...@@ -343,25 +340,14 @@ import AuditLogs from './audit_logs';
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new TreeView(); new TreeView();
new BlobViewer(); new BlobViewer();
gl.TargetBranchDropDown.bootstrap();
break; break;
case 'projects:find_file:show': case 'projects:find_file:show':
shortcut_handler = true; shortcut_handler = true;
break; break;
case 'projects:blob:new':
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blob:create':
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blob:show': case 'projects:blob:show':
new BlobViewer(); new BlobViewer();
gl.TargetBranchDropDown.bootstrap();
initBlob(); initBlob();
break; break;
case 'projects:blob:edit':
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blame:show': case 'projects:blame:show':
initBlob(); initBlob();
break; break;
...@@ -385,9 +371,11 @@ import AuditLogs from './audit_logs'; ...@@ -385,9 +371,11 @@ import AuditLogs from './audit_logs';
new ProjectFork(); new ProjectFork();
break; break;
case 'projects:artifacts:browse': case 'projects:artifacts:browse':
new ShortcutsNavigation();
new BuildArtifacts(); new BuildArtifacts();
break; break;
case 'projects:artifacts:file': case 'projects:artifacts:file':
new ShortcutsNavigation();
new BlobViewer(); new BlobViewer();
break; break;
case 'help:index': case 'help:index':
......
...@@ -252,7 +252,7 @@ export default { ...@@ -252,7 +252,7 @@ export default {
</div> </div>
</div> </div>
<div class="content-list environments-container"> <div class="environments-container">
<loading-icon <loading-icon
label="Loading environments" label="Loading environments"
size="3" size="3"
......
...@@ -69,7 +69,7 @@ export default { ...@@ -69,7 +69,7 @@ export default {
</span> </span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions"> <li v-for="action in actions">
<button <button
type="button" type="button"
......
...@@ -423,11 +423,13 @@ export default { ...@@ -423,11 +423,13 @@ 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="{ '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 class="table-section section-10" role="gridcell">
<div <div
v-if="!model.isFolder" v-if="!model.isFolder"
class="table-mobile-header"> class="table-mobile-header"
role="rowheader">
Environment Environment
</div> </div>
<span <span
...@@ -513,6 +515,7 @@ export default { ...@@ -513,6 +515,7 @@ export default {
<div class="table-section section-25" role="gridcell"> <div class="table-section section-25" role="gridcell">
<div <div
v-if="!model.isFolder" v-if="!model.isFolder"
role="rowheader"
class="table-mobile-header"> class="table-mobile-header">
Commit Commit
</div> </div>
...@@ -529,7 +532,7 @@ export default { ...@@ -529,7 +532,7 @@ export default {
</div> </div>
<div <div
v-if="!model.isFolder && !hasLastDeploymentKey" v-if="!model.isFolder && !hasLastDeploymentKey"
class="commit-title"> class="commit-title table-mobile-content">
No deployments yet No deployments yet
</div> </div>
</div> </div>
...@@ -537,6 +540,7 @@ export default { ...@@ -537,6 +540,7 @@ export default {
<div class="table-section section-10" role="gridcell"> <div class="table-section section-10" role="gridcell">
<div <div
v-if="!model.isFolder" v-if="!model.isFolder"
role="rowheader"
class="table-mobile-header"> class="table-mobile-header">
Updated Updated
</div> </div>
......
...@@ -66,19 +66,19 @@ export default { ...@@ -66,19 +66,19 @@ export default {
<template> <template>
<div class="ci-table" role="grid"> <div class="ci-table" role="grid">
<div class="gl-responsive-table-row table-row-header" role="row"> <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 Environment
</div> </div>
<div class="table-section section-10 environments-deploy" role="rowheader"> <div class="table-section section-10 environments-deploy" role="columnheader">
Deployment Deployment
</div> </div>
<div class="table-section section-15 environments-build" role="rowheader"> <div class="table-section section-15 environments-build" role="columnheader">
Job Job
</div> </div>
<div class="table-section section-25 environments-commit" role="rowheader"> <div class="table-section section-25 environments-commit" role="columnheader">
Commit Commit
</div> </div>
<div class="table-section section-10 environments-date" role="rowheader"> <div class="table-section section-10 environments-date" role="columnheader">
Updated Updated
</div> </div>
</div> </div>
......
...@@ -8,39 +8,87 @@ export default class FilterableList { ...@@ -8,39 +8,87 @@ export default class FilterableList {
this.filterForm = form; this.filterForm = form;
this.listFilterElement = filter; this.listFilterElement = filter;
this.listHolderElement = holder; this.listHolderElement = holder;
this.isBusy = false;
}
getFilterEndpoint() {
return `${this.filterForm.getAttribute('action')}?${$(this.filterForm).serialize()}`;
}
getPagePath() {
return this.getFilterEndpoint();
} }
initSearch() { 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); this.listFilterElement.addEventListener('input', this.debounceFilter);
} }
filterResults() { unbindEvents() {
const form = this.filterForm; this.listFilterElement.removeEventListener('input', this.debounceFilter);
const filterUrl = `${form.getAttribute('action')}?${$(form).serialize()}`; }
filterResults(queryData) {
if (this.isBusy) {
return false;
}
$(this.listHolderElement).fadeTo(250, 0.5); $(this.listHolderElement).fadeTo(250, 0.5);
return $.ajax({ return $.ajax({
url: form.getAttribute('action'), url: this.getFilterEndpoint(),
data: $(form).serialize(), data: queryData,
type: 'GET', type: 'GET',
dataType: 'json', dataType: 'json',
context: this, context: this,
complete() { complete: this.onFilterComplete,
$(this.listHolderElement).fadeTo(250, 1); beforeSend: () => {
this.isBusy = true;
}, },
success(data) { success: (response, textStatus, xhr) => {
this.listHolderElement.innerHTML = data.html; this.onFilterSuccess(response, xhr, queryData);
// Change url so if user reload a page - search results are saved
return window.history.replaceState({
page: filterUrl,
}, document.title, filterUrl);
}, },
}); });
} }
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 { ...@@ -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() { bindEvents() {
this.handleFormSubmit = this.handleFormSubmit.bind(this); this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
...@@ -116,6 +151,8 @@ class FilteredSearchManager { ...@@ -116,6 +151,8 @@ class FilteredSearchManager {
document.addEventListener('click', this.removeInputContainerFocusWrapper); document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper); document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
this.bindStateEvents();
} }
unbindEvents() { unbindEvents() {
...@@ -136,6 +173,8 @@ class FilteredSearchManager { ...@@ -136,6 +173,8 @@ class FilteredSearchManager {
document.removeEventListener('click', this.removeInputContainerFocusWrapper); document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper); document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
this.unbindStateEvents();
} }
checkForBackspace(e) { checkForBackspace(e) {
...@@ -451,7 +490,19 @@ class FilteredSearchManager { ...@@ -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 paths = [];
const searchQuery = gl.DropdownUtils.getSearchQuery(); const searchQuery = gl.DropdownUtils.getSearchQuery();
...@@ -459,7 +510,7 @@ class FilteredSearchManager { ...@@ -459,7 +510,7 @@ class FilteredSearchManager {
const { tokens, searchToken } const { tokens, searchToken }
= this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys()); = 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}`); paths.push(`state=${currentState}`);
tokens.forEach((token) => { tokens.forEach((token) => {
......
...@@ -248,7 +248,7 @@ GitLabDropdown = (function() { ...@@ -248,7 +248,7 @@ GitLabDropdown = (function() {
return function(data) { return function(data) {
_this.fullData = data; _this.fullData = data;
_this.parseData(_this.fullData); _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() !== '') { if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
return _this.filter.input.trigger('input'); return _this.filter.input.trigger('input');
} }
...@@ -743,8 +743,20 @@ GitLabDropdown = (function() { ...@@ -743,8 +743,20 @@ GitLabDropdown = (function() {
return [selectedObject, isMarking]; return [selectedObject, isMarking];
}; };
GitLabDropdown.prototype.focusTextInput = function() { GitLabDropdown.prototype.focusTextInput = function(triggerFocus = false) {
if (this.options.filterable) { this.filterInput.focus(); } 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) { 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 @@ ...@@ -167,8 +167,8 @@
if the name does not exist this function will return `null` if the name does not exist this function will return `null`
otherwise it will return the value of the param key provided otherwise it will return the value of the param key provided
*/ */
w.gl.utils.getParameterByName = (name) => { w.gl.utils.getParameterByName = (name, parseUrl) => {
const url = window.location.href; const url = parseUrl || window.location.href;
name = name.replace(/[[\]]/g, '\\$&'); name = name.replace(/[[\]]/g, '\\$&');
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
const results = regex.exec(url); 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'; ...@@ -39,10 +39,6 @@ import './shortcuts_network';
// behaviors // behaviors
import './behaviors/'; import './behaviors/';
// blob
import './blob/create_branch_dropdown';
import './blob/target_branch_dropdown';
// templates // templates
import './templates/issuable_template_selector'; import './templates/issuable_template_selector';
import './templates/issuable_template_selectors'; 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 */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */
(function() { (function() {
this.NewCommitForm = (function() { this.NewCommitForm = (function() {
function NewCommitForm(form, targetBranchName = 'target_branch') { function NewCommitForm(form) {
this.form = form; this.form = form;
this.targetBranchName = targetBranchName;
this.renderDestination = this.renderDestination.bind(this); 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.originalBranch = form.find('.js-original-branch');
this.createMergeRequest = form.find('.js-create-merge-request'); this.createMergeRequest = form.find('.js-create-merge-request');
this.createMergeRequestContainer = form.find('.js-create-merge-request-container'); this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
this.targetBranchDropdown.on('change.branch', this.renderDestination); this.branchName.keyup(this.renderDestination);
this.renderDestination(); this.renderDestination();
} }
NewCommitForm.prototype.renderDestination = function() { NewCommitForm.prototype.renderDestination = function() {
var different; var different;
var targetBranch = this.form.find(`input[name="${this.targetBranchName}"]`); different = this.branchName.val() !== this.originalBranch.val();
different = targetBranch.val() !== this.originalBranch.val();
if (different) { if (different) {
this.createMergeRequestContainer.show(); this.createMergeRequestContainer.show();
if (!this.wasDifferent) { if (!this.wasDifferent) {
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
/* global Flash */ /* global Flash */
import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons'; import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
export default { export default {
props: { props: {
...@@ -31,6 +32,10 @@ export default { ...@@ -31,6 +32,10 @@ export default {
}, },
}, },
mixins: [
tooltipMixin,
],
data() { data() {
return { return {
isLoading: false, isLoading: false,
...@@ -127,9 +132,10 @@ export default { ...@@ -127,9 +132,10 @@ export default {
<template> <template>
<div class="dropdown"> <div class="dropdown">
<button <button
ref="tooltip"
:class="triggerButtonClass" :class="triggerButtonClass"
@click="onClickStage" @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" :title="stage.title"
data-placement="top" data-placement="top"
data-toggle="dropdown" data-toggle="dropdown"
......
...@@ -284,9 +284,7 @@ export default { ...@@ -284,9 +284,7 @@ export default {
<table-pagination <table-pagination
v-if="shouldRenderPagination" v-if="shouldRenderPagination"
:pagenum="pagenum"
:change="change" :change="change"
:count="state.count.all"
:pageInfo="state.pageInfo" :pageInfo="state.pageInfo"
/> />
</div> </div>
......
.awards { .awards {
display: flex;
flex-wrap: wrap;
.emoji-icon { .emoji-icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
...@@ -100,7 +103,6 @@ ...@@ -100,7 +103,6 @@
.award-menu-holder { .award-menu-holder {
display: inline-block; display: inline-block;
position: absolute;
.tooltip { .tooltip {
white-space: nowrap; white-space: nowrap;
...@@ -108,9 +110,11 @@ ...@@ -108,9 +110,11 @@
} }
.award-control { .award-control {
margin: 0 5px 6px 0; margin: 4px 8px 4px 0;
outline: 0; outline: 0;
position: relative; position: relative;
display: block;
float: left;
&.disabled { &.disabled {
cursor: default; cursor: default;
......
...@@ -48,6 +48,10 @@ ...@@ -48,6 +48,10 @@
@include chevron-active; @include chevron-active;
border-color: $gray-darkest; border-color: $gray-darkest;
} }
[data-toggle="dropdown"] {
outline: 0;
}
} }
.dropdown-toggle { .dropdown-toggle {
...@@ -109,6 +113,7 @@ ...@@ -109,6 +113,7 @@
&:focus:active { &:focus:active {
@include chevron-active; @include chevron-active;
border-color: $dropdown-toggle-active-border-color; border-color: $dropdown-toggle-active-border-color;
outline: 0;
} }
} }
...@@ -201,6 +206,11 @@ ...@@ -201,6 +206,11 @@
width: 100%; width: 100%;
} }
&.dropdown-open-left {
right: 0;
left: auto;
}
&.is-loading { &.is-loading {
.dropdown-content { .dropdown-content {
display: none; display: none;
...@@ -261,7 +271,14 @@ ...@@ -261,7 +271,14 @@
text-transform: capitalize; 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; padding-top: 2px;
} }
......
...@@ -270,3 +270,103 @@ ul.controls { ...@@ -270,3 +270,103 @@ ul.controls {
ul.indent-list { ul.indent-list {
padding: 10px 0 0 30px; 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 @@ ...@@ -45,7 +45,8 @@
li { li {
display: flex; display: flex;
a { a,
.btn-link {
padding: $gl-btn-padding; padding: $gl-btn-padding;
padding-bottom: 11px; padding-bottom: 11px;
font-size: 14px; font-size: 14px;
...@@ -67,7 +68,29 @@ ...@@ -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; border-bottom: 2px solid $link-underline-blue;
color: $black; color: $black;
font-weight: 600; font-weight: 600;
......
...@@ -19,10 +19,6 @@ ...@@ -19,10 +19,6 @@
.table-section { .table-section {
white-space: nowrap; white-space: nowrap;
.branch-commit {
max-width: 100%;
}
$section-widths: 10 15 20 25 30 40; $section-widths: 10 15 20 25 30 40;
@each $width in $section-widths { @each $width in $section-widths {
&.section-#{$width} { &.section-#{$width} {
...@@ -87,4 +83,9 @@ ...@@ -87,4 +83,9 @@
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
flex: 0 0 90%; flex: 0 0 90%;
} }
.avatar {
float: none;
margin-right: 4px;
}
} }
...@@ -127,9 +127,51 @@ ...@@ -127,9 +127,51 @@
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
width: 400px; 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 { .board-inner {
position: relative;
height: 100%; height: 100%;
font-size: $issue-boards-font-size; font-size: $issue-boards-font-size;
background: $gray-light; background: $gray-light;
......
...@@ -71,7 +71,9 @@ ...@@ -71,7 +71,9 @@
height: 35px; height: 35px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
border-bottom: 1px outset $white-light; background: $gray-light;
border: 1px solid $gray-normal;
color: $gl-text-color;
.truncated-info { .truncated-info {
margin: 0 auto; margin: 0 auto;
...@@ -82,7 +84,7 @@ ...@@ -82,7 +84,7 @@
} }
.raw-link { .raw-link {
color: inherit; color: $gl-text-color;
margin-left: 5px; margin-left: 5px;
text-decoration: underline; text-decoration: underline;
} }
...@@ -93,17 +95,25 @@ ...@@ -93,17 +95,25 @@
display: flex; display: flex;
align-self: center; align-self: center;
font-size: 15px; font-size: 15px;
margin-bottom: 4px;
svg { svg {
height: 15px; height: 15px;
display: block; display: block;
fill: $white-light; fill: $gl-text-color;
} }
a, .controllers-buttons,
.btn-scroll { .btn-scroll {
margin: 0 8px; color: $gl-text-color;
color: $white-light; height: 15px;
vertical-align: middle;
padding: 0;
width: 12px;
}
.controllers-buttons {
margin: 1px 10px;
} }
.btn-scroll.animate { .btn-scroll.animate {
...@@ -137,9 +147,9 @@ ...@@ -137,9 +147,9 @@
top: 35px; top: 35px;
left: 10px; left: 10px;
bottom: 0; bottom: 0;
overflow-y: hidden; overflow-y: scroll;
padding-bottom: 20px; overflow-x: hidden;
padding-right: 20px; padding: 10px 20px 20px 5px;
} }
.environment-information { .environment-information {
......
...@@ -228,7 +228,7 @@ ...@@ -228,7 +228,7 @@
margin: 10px 0; margin: 10px 0;
background: $gray-light; background: $gray-light;
display: none; display: none;
white-space: pre-line; white-space: pre-wrap;
word-break: normal; word-break: normal;
pre { pre {
......
...@@ -285,7 +285,7 @@ ...@@ -285,7 +285,7 @@
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
.environment-action-buttons { .environment-action-buttons {
padding: 10px; padding: 10px 5px;
display: flex; display: flex;
.btn { .btn {
...@@ -293,15 +293,20 @@ ...@@ -293,15 +293,20 @@
} }
> .btn-group, > .btn-group,
.external-url, > .external-url,
.btn { > .btn {
flex: 1; flex: 1;
flex-basis: 28px; flex-basis: 28px;
margin: 0 5px;
} }
.dropdown-new { .dropdown-new {
width: 100%; width: 100%;
} }
.dropdown-menu {
min-width: initial;
}
} }
} }
} }
......
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
} }
.emoji-block { .emoji-block {
padding: 10px 0 4px; padding: 10px 0;
} }
} }
...@@ -728,6 +728,36 @@ ...@@ -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 { .add-issuable-form-input-wrapper {
height: auto; height: auto;
padding: $gl-vert-padding $gl-vert-padding 0 $gl-input-padding; padding: $gl-vert-padding $gl-vert-padding 0 $gl-input-padding;
......
...@@ -267,14 +267,19 @@ ul.related-merge-requests > li { ...@@ -267,14 +267,19 @@ ul.related-merge-requests > li {
} }
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
.new-branch-col { .emoji-block .row {
padding-top: 0; display: flex;
text-align: right;
}
.create-mr-dropdown-wrap { .new-branch-col {
.btn-group:not(.hide) { padding-top: 0;
display: inline-block; text-align: right;
align-self: center;
}
.create-mr-dropdown-wrap {
.btn-group:not(.hide) {
display: inline-block;
}
} }
} }
} }
......
...@@ -128,6 +128,7 @@ ...@@ -128,6 +128,7 @@
a { a {
width: 100%; width: 100%;
font-size: 18px; font-size: 18px;
margin-right: 0;
&:hover { &:hover {
border: 1px solid transparent; border: 1px solid transparent;
...@@ -140,6 +141,7 @@ ...@@ -140,6 +141,7 @@
a { a {
border: none; border: none;
border-bottom: 2px solid $link-underline-blue; border-bottom: 2px solid $link-underline-blue;
margin-right: 0;
color: $black; color: $black;
&:hover { &:hover {
......
...@@ -3,6 +3,41 @@ ...@@ -3,6 +3,41 @@
border-bottom: 1px solid $border-color; 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 { .member {
&.is-overriden { &.is-overriden {
.btn-ldap-override { .btn-ldap-override {
......
...@@ -103,41 +103,6 @@ ...@@ -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 { .discussion-form {
padding: $gl-padding-top $gl-padding $gl-padding; padding: $gl-padding-top $gl-padding $gl-padding;
background-color: $white-light; background-color: $white-light;
......
...@@ -38,9 +38,12 @@ ul.notes { ...@@ -38,9 +38,12 @@ ul.notes {
} }
.discussion { .discussion {
overflow: hidden;
display: block; display: block;
position: relative; position: relative;
.diff-content {
overflow: visible;
}
} }
> li { > li {
...@@ -443,6 +446,52 @@ ul.notes { ...@@ -443,6 +446,52 @@ ul.notes {
.note-action-button { .note-action-button {
margin-left: 8px; 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 { .discussion-actions {
......
...@@ -793,8 +793,7 @@ a.allowed-to-push { ...@@ -793,8 +793,7 @@ a.allowed-to-push {
} }
.project-refs-form .dropdown-menu, .project-refs-form .dropdown-menu,
.dropdown-menu-projects, .dropdown-menu-projects {
.dropdown-menu-branches {
width: 300px; width: 300px;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
......
class Admin::DeployKeysController < Admin::ApplicationController class Admin::DeployKeysController < Admin::ApplicationController
before_action :deploy_keys, only: [:index] before_action :deploy_keys, only: [:index]
before_action :deploy_key, only: [:destroy] before_action :deploy_key, only: [:destroy, :edit, :update]
def index def index
end end
...@@ -10,12 +10,24 @@ class Admin::DeployKeysController < Admin::ApplicationController ...@@ -10,12 +10,24 @@ class Admin::DeployKeysController < Admin::ApplicationController
end end
def create 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 if @deploy_key.save
redirect_to admin_deploy_keys_path redirect_to admin_deploy_keys_path
else 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
end end
...@@ -38,7 +50,11 @@ class Admin::DeployKeysController < Admin::ApplicationController ...@@ -38,7 +50,11 @@ class Admin::DeployKeysController < Admin::ApplicationController
@deploy_keys ||= DeployKey.are_public @deploy_keys ||= DeployKey.are_public
end end
def deploy_key_params def create_params
params.require(:deploy_key).permit(:key, :title, :can_push) params.require(:deploy_key).permit(:key, :title, :can_push)
end end
def update_params
params.require(:deploy_key).permit(:title, :can_push)
end
end end
module CreatesCommit module CreatesCommit
extend ActiveSupport::Concern 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) def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
if can?(current_user, :push_code, @project) if can?(current_user, :push_code, @project)
@project_to_commit_into = @project @project_to_commit_into = @project
......
...@@ -56,9 +56,14 @@ module MembershipActions ...@@ -56,9 +56,14 @@ module MembershipActions
log_audit_event(member, action: :destroy) unless member.request? 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 end
protected protected
......
...@@ -46,8 +46,10 @@ module MilestoneActions ...@@ -46,8 +46,10 @@ module MilestoneActions
def milestone_redirect_path def milestone_redirect_path
if @project if @project
namespace_project_milestone_path(@project.namespace, @project, @milestone) namespace_project_milestone_path(@project.namespace, @project, @milestone)
else elsif @group
group_milestone_path(@group, @milestone.safe_title, title: @milestone.title) group_milestone_path(@group, @milestone.safe_title, title: @milestone.title)
else
dashboard_milestone_path(@milestone.safe_title, title: @milestone.title)
end end
end end
end end
class Dashboard::GroupsController < Dashboard::ApplicationController class Dashboard::GroupsController < Dashboard::ApplicationController
def index def index
@group_members = current_user.group_members.includes(source: :route).joins(:group) @groups =
@group_members = @group_members.merge(Group.search(params[:filter_groups])) if params[:filter_groups].present? if params[:parent_id] && Group.supports_nested_groups?
@group_members = @group_members.merge(Group.sort(@sort = params[:sort])) parent = Group.find_by(id: params[:parent_id])
@group_members = @group_members.page(params[:page])
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| respond_to do |format|
format.html format.html
format.json do format.json do
render json: { render json: GroupSerializer
html: view_to_html_string("dashboard/groups/_groups", locals: { group_members: @group_members }) .new(current_user: @current_user)
} .with_pagination(request, response)
.represent(@groups)
end end
end end
end end
......
class Dashboard::MilestonesController < Dashboard::ApplicationController class Dashboard::MilestonesController < Dashboard::ApplicationController
include MilestoneActions
before_action :projects before_action :projects
before_action :milestone, only: [:show] before_action :milestone, only: [:show, :merge_requests, :participants, :labels]
def index def index
respond_to do |format| respond_to do |format|
......
...@@ -39,7 +39,7 @@ class JwtController < ApplicationController ...@@ -39,7 +39,7 @@ class JwtController < ApplicationController
errors: [ errors: [
{ code: 'UNAUTHORIZED', { code: 'UNAUTHORIZED',
message: "HTTP Basic: Access denied\n" \ 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}" } "You can generate one at #{profile_personal_access_tokens_url}" }
] ]
}, status: 401 }, status: 401
......
...@@ -26,8 +26,6 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -26,8 +26,6 @@ class Projects::BlobController < Projects::ApplicationController
end end
def create def create
set_start_branch_to_branch_name
create_commit(Files::CreateService, success_notice: "The file has been successfully created.", 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)) }, success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@branch_name, @file_path)) },
failure_view: :new, failure_view: :new,
...@@ -55,7 +53,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -55,7 +53,7 @@ class Projects::BlobController < Projects::ApplicationController
def edit def edit
if can_collaborate_with_project? if can_collaborate_with_project?
blob.load_all_data!(@repository) blob.load_all_data!
else else
redirect_to action: 'show' redirect_to action: 'show'
end end
...@@ -74,7 +72,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -74,7 +72,7 @@ class Projects::BlobController < Projects::ApplicationController
def preview def preview
@content = params[:content] @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) diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true)
diff_lines = diffy.diff.scan(/.*\n/)[2..-1] diff_lines = diffy.diff.scan(/.*\n/)[2..-1]
diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines) diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines)
...@@ -93,9 +91,11 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -93,9 +91,11 @@ class Projects::BlobController < Projects::ApplicationController
def diff def diff
apply_diff_view_cookie! apply_diff_view_cookie!
@form = UnfoldForm.new(params) @blob.load_all_data!
@lines = Gitlab::Highlight.highlight_lines(repository, @ref, @path) @lines = Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: @repository).lines
@lines = @lines[@form.since - 1..@form.to - 1]
@form = UnfoldForm.new(params)
@lines = @lines[@form.since - 1..@form.to - 1].map(&:html_safe)
if @form.bottom? if @form.bottom?
@match_line = '' @match_line = ''
...@@ -111,7 +111,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -111,7 +111,7 @@ class Projects::BlobController < Projects::ApplicationController
private private
def blob def blob
@blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path), @project) @blob ||= @repository.blob_at(@commit.id, @path)
if @blob if @blob
@blob @blob
......
...@@ -5,7 +5,9 @@ module Projects ...@@ -5,7 +5,9 @@ module Projects
before_action :authorize_read_list!, only: [:index] before_action :authorize_read_list!, only: [:index]
def 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 end
def create def create
......
...@@ -10,10 +10,10 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -10,10 +10,10 @@ class Projects::BranchesController < Projects::ApplicationController
def index def index
@sort = params[:sort].presence || sort_value_name @sort = params[:sort].presence || sort_value_name
@branches = BranchesFinder.new(@repository, params).execute @branches = BranchesFinder.new(@repository, params).execute
@branches = Kaminari.paginate_array(@branches).page(params[:page])
respond_to do |format| respond_to do |format|
format.html do format.html do
paginate_branches
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name)) @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
@max_commits = @branches.reduce(0) do |memo, branch| @max_commits = @branches.reduce(0) do |memo, branch|
...@@ -22,7 +22,6 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -22,7 +22,6 @@ class Projects::BranchesController < Projects::ApplicationController
end end
end end
format.json do format.json do
paginate_branches unless params[:show_all]
render json: @branches.map(&:name) render json: @branches.map(&:name)
end end
end end
...@@ -106,10 +105,6 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -106,10 +105,6 @@ class Projects::BranchesController < Projects::ApplicationController
end end
end end
def paginate_branches
@branches = Kaminari.paginate_array(@branches).page(params[:page])
end
def url_to_autodeploy_setup(project, branch_name) def url_to_autodeploy_setup(project, branch_name)
namespace_project_new_blob_path( namespace_project_new_blob_path(
project.namespace, project.namespace,
......
...@@ -4,6 +4,7 @@ class Projects::DeployKeysController < Projects::ApplicationController ...@@ -4,6 +4,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
# Authorize # Authorize
before_action :authorize_admin_project! before_action :authorize_admin_project!
before_action :authorize_update_deploy_key!, only: [:edit, :update]
layout "project_settings" layout "project_settings"
...@@ -21,7 +22,7 @@ class Projects::DeployKeysController < Projects::ApplicationController ...@@ -21,7 +22,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end end
def create 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 unless @key.valid? && @project.deploy_keys << @key
flash[:alert] = @key.errors.full_messages.join(', ').html_safe flash[:alert] = @key.errors.full_messages.join(', ').html_safe
...@@ -32,6 +33,18 @@ class Projects::DeployKeysController < Projects::ApplicationController ...@@ -32,6 +33,18 @@ class Projects::DeployKeysController < Projects::ApplicationController
redirect_to_repository_settings(@project) redirect_to_repository_settings(@project)
end 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 def enable
load_key load_key
Projects::EnableDeployKeyService.new(@project, current_user, params).execute Projects::EnableDeployKeyService.new(@project, current_user, params).execute
...@@ -59,10 +72,22 @@ class Projects::DeployKeysController < Projects::ApplicationController ...@@ -59,10 +72,22 @@ class Projects::DeployKeysController < Projects::ApplicationController
protected 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) params.require(:deploy_key).permit(:key, :title, :can_push)
end 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 = {}) def log_audit_event(key_title, options = {})
AuditEventService.new(current_user, @project, options) AuditEventService.new(current_user, @project, options)
.for_deploy_key(key_title).security_event .for_deploy_key(key_title).security_event
......
...@@ -104,7 +104,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -104,7 +104,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
def render_missing_personal_token def render_missing_personal_token
render plain: "HTTP Basic: Access denied\n" \ 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}", "You can generate one at #{profile_personal_access_tokens_url}",
status: 401 status: 401
end end
......
...@@ -8,7 +8,7 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -8,7 +8,7 @@ class Projects::LabelsController < Projects::ApplicationController
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update,
:generate, :destroy, :remove_priority, :generate, :destroy, :remove_priority,
:set_priorities] :set_priorities]
before_action :authorize_admin_group!, only: [:promote] before_action :authorize_admin_group_labels!, only: [:promote]
respond_to :js, :html respond_to :js, :html
...@@ -161,7 +161,7 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -161,7 +161,7 @@ class Projects::LabelsController < Projects::ApplicationController
return render_404 unless can?(current_user, :admin_label, @project) return render_404 unless can?(current_user, :admin_label, @project)
end end
def authorize_admin_group! def authorize_admin_group_labels!
return render_404 unless can?(current_user, :admin_group, @project.group) return render_404 unless can?(current_user, :admin_label, @project.group)
end end
end end
...@@ -43,7 +43,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController ...@@ -43,7 +43,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
if schedule.update(owner: current_user) if schedule.update(owner: current_user)
redirect_to pipeline_schedules_path(@project) redirect_to pipeline_schedules_path(@project)
else 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
end end
...@@ -53,7 +53,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController ...@@ -53,7 +53,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
else else
redirect_to pipeline_schedules_path(@project), redirect_to pipeline_schedules_path(@project),
status: 302, status: 302,
alert: "Failed to remove the pipeline schedule" alert: _("Failed to remove the pipeline schedule")
end end
end end
......
...@@ -36,7 +36,6 @@ class Projects::TreeController < Projects::ApplicationController ...@@ -36,7 +36,6 @@ class Projects::TreeController < Projects::ApplicationController
def create_dir def create_dir
return render_404 unless @commit_params.values.all? 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.", 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)), 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)) failure_path: namespace_project_tree_path(@project.namespace, @project, @ref))
......
...@@ -8,7 +8,7 @@ module GroupsHelper ...@@ -8,7 +8,7 @@ module GroupsHelper
group = Group.find_by_full_path(group) group = Group.find_by_full_path(group)
end 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 end
def group_title(group, name = nil, url = nil) def group_title(group, name = nil, url = nil)
......
...@@ -138,6 +138,8 @@ module MilestonesHelper ...@@ -138,6 +138,8 @@ module MilestonesHelper
merge_requests_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json) merge_requests_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group elsif @group
merge_requests_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json) 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
end end
...@@ -146,6 +148,8 @@ module MilestonesHelper ...@@ -146,6 +148,8 @@ module MilestonesHelper
participants_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json) participants_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group elsif @group
participants_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json) 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
end end
...@@ -154,6 +158,8 @@ module MilestonesHelper ...@@ -154,6 +158,8 @@ module MilestonesHelper
labels_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json) labels_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group elsif @group
labels_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json) 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 end
end end
...@@ -13,7 +13,7 @@ module NavHelper ...@@ -13,7 +13,7 @@ module NavHelper
else else
"page-gutter right-sidebar-expanded" "page-gutter right-sidebar-expanded"
end end
elsif current_path?('builds#show') elsif current_path?('jobs#show')
"page-gutter build-sidebar right-sidebar-expanded" "page-gutter build-sidebar right-sidebar-expanded"
elsif current_path?('wikis#show') || elsif current_path?('wikis#show') ||
current_path?('wikis#edit') || current_path?('wikis#edit') ||
......
...@@ -90,14 +90,18 @@ module NotesHelper ...@@ -90,14 +90,18 @@ module NotesHelper
end end
end end
def note_url(note) def note_url(note, project = @project)
if note.noteable.is_a?(PersonalSnippet) if note.noteable.is_a?(PersonalSnippet)
snippet_note_path(note.noteable, note) snippet_note_path(note.noteable, note)
else else
namespace_project_note_path(@project.namespace, @project, note) namespace_project_note_path(project.namespace, project, note)
end end
end end
def noteable_note_url(note)
Gitlab::UrlBuilder.build(note)
end
def form_resources def form_resources
if @snippet.is_a?(PersonalSnippet) if @snippet.is_a?(PersonalSnippet)
[@note] [@note]
......
...@@ -146,7 +146,7 @@ module ProjectsHelper ...@@ -146,7 +146,7 @@ module ProjectsHelper
end end
options = options_for_select( options = options_for_select(
options, options.invert,
selected: highest_available_option || @project.project_feature.public_send(field), selected: highest_available_option || @project.project_feature.public_send(field),
disabled: disabled_option disabled: disabled_option
) )
...@@ -485,9 +485,9 @@ module ProjectsHelper ...@@ -485,9 +485,9 @@ module ProjectsHelper
def project_feature_options def project_feature_options
{ {
s_('ProjectFeature|Disabled') => ProjectFeature::DISABLED, ProjectFeature::DISABLED => s_('ProjectFeature|Disabled'),
s_('ProjectFeature|Only team members') => ProjectFeature::PRIVATE, ProjectFeature::PRIVATE => s_('ProjectFeature|Only team members'),
s_('ProjectFeature|Everyone with access') => ProjectFeature::ENABLED ProjectFeature::ENABLED => s_('ProjectFeature|Everyone with access')
} }
end end
......
class Approver < ActiveRecord::Base class Approver < ActiveRecord::Base
belongs_to :target, polymorphic: true belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user belongs_to :user
validates :user, presence: true validates :user, presence: true
......
class ApproverGroup < ActiveRecord::Base class ApproverGroup < ActiveRecord::Base
belongs_to :target, polymorphic: true belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :group belongs_to :group
validates :group, presence: true validates :group, presence: true
......
...@@ -5,7 +5,7 @@ class AwardEmoji < ActiveRecord::Base ...@@ -5,7 +5,7 @@ class AwardEmoji < ActiveRecord::Base
include Participable include Participable
include GhostUser include GhostUser
belongs_to :awardable, polymorphic: true belongs_to :awardable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user belongs_to :user
validates :awardable, :user, presence: true validates :awardable, :user, presence: true
......
...@@ -94,6 +94,10 @@ class Blob < SimpleDelegator ...@@ -94,6 +94,10 @@ class Blob < SimpleDelegator
end end
end end
def load_all_data!
super(project.repository) if project
end
def no_highlighting? def no_highlighting?
raw_size && raw_size > MAXIMUM_TEXT_HIGHLIGHT_SIZE raw_size && raw_size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
end end
...@@ -151,6 +155,10 @@ class Blob < SimpleDelegator ...@@ -151,6 +155,10 @@ class Blob < SimpleDelegator
@extension ||= extname.downcase.delete('.') @extension ||= extname.downcase.delete('.')
end end
def file_type
Gitlab::FileDetector.type_of(path)
end
def video? def video?
UploaderHelper::VIDEO_EXT.include?(extension) UploaderHelper::VIDEO_EXT.include?(extension)
end end
...@@ -176,16 +184,19 @@ class Blob < SimpleDelegator ...@@ -176,16 +184,19 @@ class Blob < SimpleDelegator
end end
def rendered_as_text?(ignore_errors: true) 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 end
def show_viewer_switcher? def show_viewer_switcher?
rendered_as_text? && rich_viewer rendered_as_text? && rich_viewer
end end
def expanded?
!!@expanded
end
def expand! def expand!
simple_viewer&.expanded = true @expanded = true
rich_viewer&.expanded = true
end end
private private
......
...@@ -6,15 +6,15 @@ module BlobViewer ...@@ -6,15 +6,15 @@ module BlobViewer
self.loading_partial_name = 'loading' 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_reader :blob
attr_accessor :expanded
delegate :project, to: :blob delegate :project, to: :blob
def initialize(blob) def initialize(blob)
@blob = blob @blob = blob
@initially_binary = blob.binary?
end end
def self.partial_path def self.partial_path
...@@ -52,19 +52,15 @@ module BlobViewer ...@@ -52,19 +52,15 @@ module BlobViewer
def self.can_render?(blob, verify_binary: true) def self.can_render?(blob, verify_binary: true)
return false if verify_binary && binary? != blob.binary? return false if verify_binary && binary? != blob.binary?
return true if extensions&.include?(blob.extension) 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 false
end end
def load_async?
self.class.load_async? && render_error.nil?
end
def collapsed? def collapsed?
return @collapsed if defined?(@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 end
def too_large? def too_large?
...@@ -73,6 +69,10 @@ module BlobViewer ...@@ -73,6 +69,10 @@ module BlobViewer
@too_large = size_limit && blob.raw_size > size_limit @too_large = size_limit && blob.raw_size > size_limit
end 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 # 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 # render the blob at all. Human-readable error messages are found in the
# `BlobHelper#blob_render_error_reason` helper. # `BlobHelper#blob_render_error_reason` helper.
......
...@@ -4,6 +4,5 @@ module BlobViewer ...@@ -4,6 +4,5 @@ module BlobViewer
include ServerSide include ServerSide
self.partial_name = 'empty' self.partial_name = 'empty'
self.binary = true
end end
end end
...@@ -9,9 +9,7 @@ module BlobViewer ...@@ -9,9 +9,7 @@ module BlobViewer
end end
def prepare! def prepare!
if blob.project blob.load_all_data!
blob.load_all_data!(blob.project.repository)
end
end end
def render_error def render_error
......
...@@ -6,6 +6,10 @@ class Board < ActiveRecord::Base ...@@ -6,6 +6,10 @@ class Board < ActiveRecord::Base
validates :name, :project, presence: true validates :name, :project, presence: true
def backlog_list
lists.merge(List.backlog).take
end
def closed_list def closed_list
lists.merge(List.closed).take lists.merge(List.closed).take
end end
......
...@@ -114,16 +114,16 @@ class Commit ...@@ -114,16 +114,16 @@ class Commit
# #
# Usually, the commit title is the first line of the commit message. # 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 # 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 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 end
# Returns the full commits title # Returns the full commits title
def full_title def full_title
return @full_title if @full_title @full_title ||=
@full_title =
if safe_message.blank? if safe_message.blank?
no_commit_message no_commit_message
else else
...@@ -131,19 +131,14 @@ class Commit ...@@ -131,19 +131,14 @@ class Commit
end end
end end
# Returns the commits description # Returns full commit message if title is truncated (greater than 99 characters)
# # otherwise returns commit message without first line
# cut off, ellipses (`&hellp;`) are prepended to the commit message.
def description def description
title_end = safe_message.index("\n") return safe_message if full_title.length >= 100
@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
safe_message.split("\n", 2)[1].try(:chomp)
end
def description? def description?
description.present? description.present?
end end
...@@ -326,12 +321,11 @@ class Commit ...@@ -326,12 +321,11 @@ class Commit
end end
def raw_diffs(*args) 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)
# if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs) Gitlab::GitalyClient::Commit.new(project.repository).diff_from_parent(self, *args)
# Gitlab::GitalyClient::Commit.new(project.repository).diff_from_parent(self, *args) else
# else raw.diffs(*args)
raw.diffs(*args) end
# end
end end
def raw_deltas def raw_deltas
......
...@@ -4,7 +4,7 @@ class Deployment < ActiveRecord::Base ...@@ -4,7 +4,7 @@ class Deployment < ActiveRecord::Base
belongs_to :project, required: true, validate: true belongs_to :project, required: true, validate: true
belongs_to :environment, required: true, validate: true belongs_to :environment, required: true, validate: true
belongs_to :user belongs_to :user
belongs_to :deployable, polymorphic: true belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
validates :sha, presence: true validates :sha, presence: true
validates :ref, presence: true validates :ref, presence: true
......
...@@ -47,7 +47,7 @@ class Event < ActiveRecord::Base ...@@ -47,7 +47,7 @@ class Event < ActiveRecord::Base
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :project belongs_to :project
belongs_to :target, polymorphic: true belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
# For Hash only # For Hash only
serialize :data # rubocop:disable Cop/ActiverecordSerialize serialize :data # rubocop:disable Cop/ActiverecordSerialize
......
class LabelLink < ActiveRecord::Base class LabelLink < ActiveRecord::Base
include Importable include Importable
belongs_to :target, polymorphic: true belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :label belongs_to :label
validates :target, presence: true, unless: :importing? validates :target, presence: true, unless: :importing?
......
...@@ -2,7 +2,7 @@ class List < ActiveRecord::Base ...@@ -2,7 +2,7 @@ class List < ActiveRecord::Base
belongs_to :board belongs_to :board
belongs_to :label 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 :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label? validates :label, :position, presence: true, if: :label?
......
...@@ -9,7 +9,7 @@ class Member < ActiveRecord::Base ...@@ -9,7 +9,7 @@ class Member < ActiveRecord::Base
belongs_to :created_by, class_name: "User" belongs_to :created_by, class_name: "User"
belongs_to :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 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