Commit cbaf06db authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge remote-tracking branch 'ce-com/master' into ce-to-ee

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parents 6910064f 1e3bb74d
......@@ -8,13 +8,10 @@ entry.
## 9.2.4 (2017-06-02)
- No changes.
- Fix visibility when referencing snippets.
## 9.2.3 (2017-05-31)
- No changes.
- No changes.
- Move uploads from 'public/uploads' to 'public/uploads/system'.
- Escapes html content before appending it to the DOM.
- Restrict API X-Frame-Options to same origin.
......
......@@ -2,6 +2,7 @@ source 'https://rubygems.org'
gem 'rails', '4.2.8'
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
gem 'bootsnap', '~> 1.0.0'
# Responders respond_to and respond_with
gem 'responders', '~> 2.0'
......@@ -17,7 +18,7 @@ gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.25.1.1'
gem 'faraday', '~> 0.11.0'
gem 'faraday', '~> 0.12'
# Authentication libraries
gem 'devise', '~> 4.2'
......@@ -268,7 +269,7 @@ gem 'sentry-raven', '~> 2.4.0'
gem 'premailer-rails', '~> 1.9.0'
# I18n
gem 'ruby_parser', '~> 3.8.4', require: false
gem 'ruby_parser', '~> 3.8', require: false
gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development
......@@ -368,7 +369,7 @@ gem 'html2text'
gem 'ruby-prof', '~> 0.16.2'
# OAuth
gem 'oauth2', '~> 1.3.0'
gem 'oauth2', '~> 1.4'
# Soft deletion
gem 'paranoia', '~> 2.2'
......
......@@ -90,6 +90,8 @@ GEM
bindata (2.3.5)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (1.0.0)
msgpack (~> 1.0)
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
......@@ -212,7 +214,7 @@ GEM
factory_girl_rails (4.7.0)
factory_girl (~> 4.7.0)
railties (>= 3.0.0)
faraday (0.11.0)
faraday (0.12.1)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.11.0.1)
faraday (>= 0.7.4, < 1.0)
......@@ -487,6 +489,7 @@ GEM
minitest (5.7.0)
mmap2 (2.2.6)
mousetrap-rails (1.4.6)
msgpack (1.1.0)
multi_json (1.12.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
......@@ -502,8 +505,8 @@ GEM
mini_portile2 (~> 2.1.0)
numerizer (0.1.1)
oauth (0.5.1)
oauth2 (1.3.1)
faraday (>= 0.8, < 0.12)
oauth2 (1.4.0)
faraday (>= 0.8, < 0.13)
jwt (~> 1.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
......@@ -683,7 +686,7 @@ GEM
retriable (1.4.1)
rinku (2.0.0)
rotp (2.1.2)
rouge (2.0.7)
rouge (2.1.0)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
......@@ -731,7 +734,7 @@ GEM
ruby-progressbar (1.8.1)
ruby-saml (1.4.1)
nokogiri (>= 1.5.10)
ruby_parser (3.8.4)
ruby_parser (3.9.0)
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
......@@ -764,7 +767,7 @@ GEM
sentry-raven (2.4.0)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
sexp_processor (4.8.0)
sexp_processor (4.9.0)
sham_rack (1.3.6)
rack
shoulda-matchers (2.8.0)
......@@ -917,6 +920,7 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
bootsnap (~> 1.0.0)
bootstrap-sass (~> 3.3.0)
brakeman (~> 3.6.0)
browser (~> 2.2)
......@@ -948,8 +952,12 @@ DEPENDENCIES
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0)
<<<<<<< HEAD
faraday (~> 0.11.0)
faraday_middleware-aws-signers-v4
=======
faraday (~> 0.12)
>>>>>>> ce-com/master
ffaker (~> 2.4)
flay (~> 2.8.0)
flipper (~> 0.10.2)
......@@ -1011,7 +1019,7 @@ DEPENDENCIES
net-ldap
net-ssh (~> 3.0.1)
nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.3.0)
oauth2 (~> 1.4)
octokit (~> 4.6.2)
oj (~> 2.17.4)
omniauth (~> 1.4.2)
......@@ -1063,7 +1071,7 @@ DEPENDENCIES
rubocop-rspec (~> 1.15.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
ruby_parser (~> 3.8.4)
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.25.1.1)
sanitize (~> 2.0)
......
......@@ -88,6 +88,7 @@ function installGlEmojiElement() {
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
if (
emojiUnicode &&
isEmojiUnicode &&
!isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion)
) {
......
......@@ -28,7 +28,8 @@ function isSkinToneComboEmoji(emojiUnicode) {
// doesn't support the skin tone versions of horse racing
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
return Array.from(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
const firstCharacter = Array.from(emojiUnicode)[0];
return firstCharacter && firstCharacter.codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode);
}
......
......@@ -35,7 +35,7 @@ export default class BlobFileDropzone {
this.removeFile(file);
});
this.on('sending', function (file, xhr, formData) {
formData.append('branch_name', form.find('input[name="branch_name"]').val());
formData.append('branch_name', form.find('.js-branch-name').val());
formData.append('create_merge_request', form.find('.js-create-merge-request').val());
formData.append('commit_message', form.find('.js-commit-message').val());
});
......
class CreateBranchDropdown {
constructor(el, targetBranchDropdown) {
this.targetBranchDropdown = targetBranchDropdown;
this.el = el;
this.dropdownBack = this.el.closest('.dropdown').querySelector('.dropdown-menu-back');
this.cancelButton = this.el.querySelector('.js-cancel-branch-btn');
this.newBranchField = this.el.querySelector('#new_branch_name');
this.newBranchCreateButton = this.el.querySelector('.js-new-branch-btn');
this.newBranchCreateButton.setAttribute('disabled', '');
this.addBindings();
this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('beforeunload', this.cleanupWrapper);
}
cleanup() {
this.cleanBindings();
document.removeEventListener('beforeunload', this.cleanupWrapper);
}
cleanBindings() {
this.newBranchField.removeEventListener('keyup', this.enableBranchCreateButtonWrapper);
this.newBranchField.removeEventListener('change', this.enableBranchCreateButtonWrapper);
this.newBranchField.removeEventListener('keydown', this.handleNewBranchKeydownWrapper);
this.dropdownBack.removeEventListener('click', this.resetFormWrapper);
this.cancelButton.removeEventListener('click', this.handleCancelClickWrapper);
this.newBranchCreateButton.removeEventListener('click', this.createBranchWrapper);
}
addBindings() {
this.enableBranchCreateButtonWrapper = this.enableBranchCreateButton.bind(this);
this.handleNewBranchKeydownWrapper = this.handleNewBranchKeydown.bind(this);
this.resetFormWrapper = this.resetForm.bind(this);
this.handleCancelClickWrapper = this.handleCancelClick.bind(this);
this.createBranchWrapper = this.createBranch.bind(this);
this.newBranchField.addEventListener('keyup', this.enableBranchCreateButtonWrapper);
this.newBranchField.addEventListener('change', this.enableBranchCreateButtonWrapper);
this.newBranchField.addEventListener('keydown', this.handleNewBranchKeydownWrapper);
this.dropdownBack.addEventListener('click', this.resetFormWrapper);
this.cancelButton.addEventListener('click', this.handleCancelClickWrapper);
this.newBranchCreateButton.addEventListener('click', this.createBranchWrapper);
}
handleCancelClick(e) {
e.preventDefault();
e.stopPropagation();
this.resetForm();
this.dropdownBack.click();
}
handleNewBranchKeydown(e) {
const keyCode = e.which;
const ENTER_KEYCODE = 13;
if (keyCode === ENTER_KEYCODE) {
this.createBranch(e);
}
}
enableBranchCreateButton() {
if (this.newBranchField.value !== '') {
this.newBranchCreateButton.removeAttribute('disabled');
} else {
this.newBranchCreateButton.setAttribute('disabled', '');
}
}
resetForm() {
this.newBranchField.value = '';
this.enableBranchCreateButtonWrapper();
}
createBranch(e) {
e.preventDefault();
if (this.newBranchCreateButton.getAttribute('disabled') === '') {
return;
}
const newBranchName = this.newBranchField.value;
this.targetBranchDropdown.setNewBranch(newBranchName);
this.resetForm();
}
}
window.gl = window.gl || {};
gl.CreateBranchDropdown = CreateBranchDropdown;
/* eslint-disable class-methods-use-this */
const SELECT_ITEM_MSG = 'Select';
class TargetBranchDropDown {
constructor(dropdown) {
this.dropdown = dropdown;
this.$dropdown = $(dropdown);
this.fieldName = this.dropdown.getAttribute('data-field-name');
this.form = this.dropdown.closest('form');
this.createDropdown();
}
static bootstrap() {
const dropdowns = document.querySelectorAll('.js-project-branches-dropdown');
[].forEach.call(dropdowns, dropdown => new TargetBranchDropDown(dropdown));
}
createDropdown() {
const self = this;
this.$dropdown.glDropdown({
selectable: true,
filterable: true,
search: {
fields: ['title'],
},
data: (term, callback) => $.ajax({
url: self.dropdown.getAttribute('data-refs-url'),
data: {
ref: self.dropdown.getAttribute('data-ref'),
show_all: true,
},
dataType: 'json',
}).done(refs => callback(self.dropdownData(refs))),
toggleLabel(item, el) {
if (el.is('.is-active')) {
return item.text;
}
return SELECT_ITEM_MSG;
},
clicked(options) {
options.e.preventDefault();
self.onClick.call(self);
},
fieldName: self.fieldName,
});
return new gl.CreateBranchDropdown(this.form.querySelector('.dropdown-new-branch'), this);
}
onClick() {
this.enableSubmit();
this.$dropdown.trigger('change.branch');
}
enableSubmit() {
const submitBtn = this.form.querySelector('[type="submit"]');
if (this.branchInput && this.branchInput.value) {
submitBtn.removeAttribute('disabled');
} else {
submitBtn.setAttribute('disabled', '');
}
}
dropdownData(refs) {
const branchList = this.dropdownItems(refs);
this.cachedRefs = refs;
this.addDefaultBranch(branchList);
this.addNewBranch(branchList);
return { Branches: branchList };
}
dropdownItems(refs) {
return refs.map(this.dropdownItem);
}
dropdownItem(ref) {
return { id: ref, text: ref, title: ref };
}
addDefaultBranch(branchList) {
// when no branch is selected do nothing
if (!this.branchInput) {
return;
}
const branchInputVal = this.branchInput.value;
const currentBranchIndex = this.searchBranch(branchList, branchInputVal);
if (currentBranchIndex === -1) {
this.unshiftBranch(branchList, this.dropdownItem(branchInputVal));
}
}
addNewBranch(branchList) {
if (this.newBranch) {
this.unshiftBranch(branchList, this.newBranch);
}
}
searchBranch(branchList, branchName) {
return _.findIndex(branchList, el => branchName === el.id);
}
unshiftBranch(branchList, branch) {
const branchIndex = this.searchBranch(branchList, branch.id);
if (branchIndex === -1) {
branchList.unshift(branch);
}
}
setNewBranch(newBranchName) {
this.newBranch = this.dropdownItem(newBranchName);
this.refreshData();
this.selectBranch(this.searchBranch(this.glDropdown.fullData.Branches, newBranchName));
}
refreshData() {
this.glDropdown.fullData = this.dropdownData(this.cachedRefs);
this.clearFilter();
}
clearFilter() {
// apply an empty filter in order to refresh the data
this.glDropdown.filter.filter('');
this.dropdown.closest('.dropdown').querySelector('.dropdown-page-one .dropdown-input-field').value = '';
}
selectBranch(index) {
const branch = this.dropdown.closest('.dropdown').querySelectorAll('li a')[index];
if (!branch.classList.contains('is-active')) {
branch.click();
} else {
this.closeDropdown();
}
}
closeDropdown() {
this.dropdown.closest('.dropdown').querySelector('.dropdown-menu-close').click();
}
get branchInput() {
return this.form.querySelector(`input[name="${this.fieldName}"]`);
}
get glDropdown() {
return this.$dropdown.data('glDropdown');
}
}
window.gl = window.gl || {};
gl.TargetBranchDropDown = TargetBranchDropDown;
......@@ -105,6 +105,8 @@ $(() => {
if (list.type === 'closed') {
list.position = Infinity;
list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' };
} else if (list.type === 'backlog') {
list.position = -1;
}
});
......@@ -147,7 +149,7 @@ $(() => {
},
computed: {
disabled() {
return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length;
return !this.store.lists.filter(list => !list.preset).length;
},
tooltipTitle() {
if (this.disabled) {
......
/* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* global Sortable */
import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list';
import boardBlankState from './board_blank_state';
import './board_delete';
......@@ -22,6 +23,10 @@ gl.issueBoards.Board = Vue.extend({
disabled: Boolean,
issueLinkBase: String,
rootPath: String,
boardId: {
type: String,
required: true,
},
},
data () {
return {
......@@ -78,7 +83,16 @@ gl.issueBoards.Board = Vue.extend({
methods: {
showNewIssueForm() {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
}
},
toggleExpanded(e) {
if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) {
this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) {
localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded);
}
}
},
},
mounted () {
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
......@@ -102,4 +116,11 @@ gl.issueBoards.Board = Vue.extend({
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
},
created() {
if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) {
const isCollapsed = localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
this.list.isExpanded = !isCollapsed;
}
},
});
......@@ -32,9 +32,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({
showSidebar () {
return Object.keys(this.issue).length;
},
assigneeId() {
return this.issue.assignee ? this.issue.assignee.id : 0;
},
milestoneTitle() {
return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
}
......
......@@ -152,6 +152,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
<div class="card-assignee">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
:key="assignee.id"
v-if="shouldRenderAssignee(index)"
class="js-no-trigger"
:link-href="assigneeUrl(assignee)"
......
......@@ -26,7 +26,8 @@ gl.issueBoards.ModalFooter = Vue.extend({
},
methods: {
addIssues() {
const list = this.modal.selectedList || this.state.lists[0];
const firstListIndex = 1;
const list = this.modal.selectedList || this.state.lists[firstListIndex];
const selectedIssues = ModalStore.getSelectedIssues();
const issueIds = selectedIssues.map(issue => issue.globalId);
......
......@@ -11,7 +11,7 @@ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
},
computed: {
selected() {
return this.modal.selectedList || this.state.lists[0];
return this.modal.selectedList || this.state.lists[1];
},
},
destroyed() {
......
......@@ -12,7 +12,9 @@ class List {
this.position = obj.position;
this.title = obj.title;
this.type = obj.list_type;
this.preset = ['closed', 'blank'].indexOf(this.type) > -1;
this.preset = ['backlog', 'closed', 'blank'].indexOf(this.type) > -1;
this.isExpandable = ['backlog', 'closed'].indexOf(this.type) > -1;
this.isExpanded = true;
this.page = 1;
this.loading = true;
this.loadingMore = false;
......
......@@ -37,10 +37,14 @@ gl.issueBoards.BoardsStore = {
},
new (listObj) {
const list = this.addList(listObj);
const backlogList = this.findList('type', 'backlog', 'backlog');
list
.save()
.then(() => {
// Remove any new issues from the backlog
// as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList));
this.state.lists = _.sortBy(this.state.lists, 'position');
})
.catch(() => {
......@@ -53,7 +57,7 @@ gl.issueBoards.BoardsStore = {
},
shouldAddBlankState () {
// Decide whether to add the blank state
return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0]);
},
addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
......@@ -106,7 +110,7 @@ gl.issueBoards.BoardsStore = {
issueTo.removeLabel(listFrom.label);
}
if (listTo.type === 'closed') {
if (listTo.type === 'closed' && listFrom.type !== 'backlog') {
issueLists.forEach((list) => {
list.removeIssue(issue);
});
......
......@@ -20,6 +20,7 @@ window.Build = (function () {
this.$document = $(document);
this.logBytes = 0;
this.scrollOffsetPadding = 30;
this.hasBeenScrolled = false;
this.updateDropdown = this.updateDropdown.bind(this);
this.getBuildTrace = this.getBuildTrace.bind(this);
......@@ -62,6 +63,15 @@ window.Build = (function () {
.off('click')
.on('click', this.scrollToBottom.bind(this));
const scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$scrollContainer
.off('scroll')
.on('scroll', () => {
this.hasBeenScrolled = true;
scrollThrottled();
});
$(window)
.off('resize.build')
.on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
......@@ -70,25 +80,16 @@ window.Build = (function () {
// eslint-disable-next-line
this.getBuildTrace()
.then(() => this.makeTraceScrollable())
.then(() => this.scrollToBottom());
.then(() => this.toggleScroll())
.then(() => {
if (!this.hasBeenScrolled) {
this.scrollToBottom();
}
});
this.verifyTopPosition();
}
Build.prototype.makeTraceScrollable = function () {
this.$scrollContainer.niceScroll({
cursorcolor: '#fff',
cursoropacitymin: 1,
cursorwidth: '3px',
railpadding: { top: 5, bottom: 5, right: 5 },
});
this.$scrollContainer.on('scroll', _.throttle(this.toggleScroll.bind(this), 100));
this.toggleScroll();
};
Build.prototype.canScroll = function () {
return (this.$scrollContainer.prop('scrollHeight') - this.scrollOffsetPadding) > this.$scrollContainer.height();
};
......@@ -104,12 +105,11 @@ window.Build = (function () {
*
*/
Build.prototype.toggleScroll = function () {
const bottomScroll = this.$scrollContainer.scrollTop() +
this.scrollOffsetPadding +
this.$scrollContainer.height();
const currentPosition = this.$scrollContainer.scrollTop();
const bottomScroll = currentPosition + this.$scrollContainer.innerHeight();
if (this.canScroll()) {
if (this.$scrollContainer.scrollTop() === 0) {
if (currentPosition === 0) {
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (bottomScroll === this.$scrollContainer.prop('scrollHeight')) {
......@@ -123,12 +123,14 @@ window.Build = (function () {
};
Build.prototype.scrollToTop = function () {
this.$scrollContainer.getNiceScroll(0).doScrollTop(0);
this.hasBeenScrolled = true;
this.$scrollContainer.scrollTop(0);
this.toggleScroll();
};
Build.prototype.scrollToBottom = function () {
this.$scrollContainer.getNiceScroll(0).doScrollTo(this.$scrollContainer.prop('scrollHeight'));
this.hasBeenScrolled = true;
this.$scrollContainer.scrollTop(this.$scrollContainer.prop('scrollHeight'));
this.toggleScroll();
};
......@@ -216,7 +218,11 @@ window.Build = (function () {
Build.timeout = setTimeout(() => {
//eslint-disable-next-line
this.getBuildTrace()
.then(() => this.scrollToBottom());
.then(() => {
if (!this.hasBeenScrolled) {
this.scrollToBottom();
}
});
}, 4000);
} else {
this.$buildRefreshAnimation.remove();
......@@ -238,7 +244,7 @@ window.Build = (function () {
};
Build.prototype.toggleSidebar = function (shouldHide) {
const shouldShow = !shouldHide;
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
this.$buildTrace
.toggleClass('sidebar-expanded', shouldShow)
......@@ -253,7 +259,7 @@ window.Build = (function () {
this.verifyTopPosition();
if (this.$scrollContainer.getNiceScroll(0)) {
if (this.canScroll()) {
this.toggleScroll();
}
};
......
// ECMAScript polyfills
import 'core-js/fn/array/find';
import 'core-js/fn/array/find-index';
import 'core-js/fn/array/from';
import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign';
......
......@@ -80,21 +80,27 @@
v-if="isLoading && !hasKeys"
size="2"
label="Loading deploy keys"
/>
/>
<div v-else-if="hasKeys">
<keys-panel
title="Enabled deploy keys for this project"
:keys="keys.enabled_keys"
:store="store" />
:store="store"
:endpoint="endpoint"
/>
<keys-panel
title="Deploy keys from projects you have access to"
:keys="keys.available_project_keys"
:store="store" />
:store="store"
:endpoint="endpoint"
/>
<keys-panel
v-if="keys.public_keys.length"
title="Public deploy keys available to any project"
:keys="keys.public_keys"
:store="store" />
:store="store"
:endpoint="endpoint"
/>
</div>
</div>
</template>
......@@ -11,6 +11,10 @@
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
},
components: {
actionBtn,
......@@ -19,6 +23,9 @@
timeagoDate() {
return gl.utils.getTimeago().format(this.deployKey.created_at);
},
editDeployKeyPath() {
return `${this.endpoint}/${this.deployKey.id}/edit`;
},
},
methods: {
isEnabled(id) {
......@@ -33,7 +40,8 @@
<div class="pull-left append-right-10 hidden-xs">
<i
aria-hidden="true"
class="fa fa-key key-icon">
class="fa fa-key key-icon"
>
</i>
</div>
<div class="deploy-key-content key-list-item-info">
......@@ -45,7 +53,8 @@
</div>
<div
v-if="deployKey.can_push"
class="write-access-allowed">
class="write-access-allowed"
>
Write access allowed
</div>
</div>
......@@ -53,7 +62,8 @@
<a
v-for="project in deployKey.projects"
class="label deploy-project-label"
:href="project.full_path">
:href="project.full_path"
>
{{ project.full_name }}
</a>
</div>
......@@ -61,20 +71,30 @@
<span class="key-created-at">
created {{ timeagoDate }}
</span>
<a
v-if="deployKey.can_edit"
class="btn btn-small"
:href="editDeployKeyPath"
>
Edit
</a>
<action-btn
v-if="!isEnabled(deployKey.id)"
:deploy-key="deployKey"
type="enable"/>
type="enable"
/>
<action-btn
v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned"
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="remove" />
type="remove"
/>
<action-btn
v-else
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="disable" />
type="disable"
/>
</div>
</div>
</template>
......@@ -20,6 +20,10 @@
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
},
components: {
key,
......@@ -34,18 +38,22 @@
({{ keys.length }})
</h5>
<ul class="well-list"
v-if="keys.length">
v-if="keys.length"
>
<li
v-for="deployKey in keys"
:key="deployKey.id">
<key
:deploy-key="deployKey"
:store="store" />
:store="store"
:endpoint="endpoint"
/>
</li>
</ul>
<div
class="settings-message text-center"
v-else-if="showHelpBox">
v-else-if="showHelpBox"
>
No deploy keys found. Create one with the form above.
</div>
</div>
......
......@@ -167,9 +167,6 @@ import AuditLogs from './audit_logs';
case 'admin:projects:index':
new ProjectsList();
break;
case 'dashboard:groups:index':
new GroupsList();
break;
case 'explore:groups:index':
new GroupsList();
......@@ -343,25 +340,14 @@ import AuditLogs from './audit_logs';
shortcut_handler = new ShortcutsNavigation();
new TreeView();
new BlobViewer();
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:find_file:show':
shortcut_handler = true;
break;
case 'projects:blob:new':
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blob:create':
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blob:show':
new BlobViewer();
gl.TargetBranchDropDown.bootstrap();
initBlob();
break;
case 'projects:blob:edit':
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blame:show':
initBlob();
break;
......@@ -385,9 +371,11 @@ import AuditLogs from './audit_logs';
new ProjectFork();
break;
case 'projects:artifacts:browse':
new ShortcutsNavigation();
new BuildArtifacts();
break;
case 'projects:artifacts:file':
new ShortcutsNavigation();
new BlobViewer();
break;
case 'help:index':
......
......@@ -252,7 +252,7 @@ export default {
</div>
</div>
<div class="content-list environments-container">
<div class="environments-container">
<loading-icon
label="Loading environments"
size="3"
......
......@@ -69,7 +69,7 @@ export default {
</span>
</button>
<ul class="dropdown-menu">
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<button
type="button"
......
......@@ -423,11 +423,13 @@ export default {
</script>
<template>
<div
:class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }">
:class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }"
role="row">
<div class="table-section section-10" role="gridcell">
<div
v-if="!model.isFolder"
class="table-mobile-header">
class="table-mobile-header"
role="rowheader">
Environment
</div>
<span
......@@ -513,6 +515,7 @@ export default {
<div class="table-section section-25" role="gridcell">
<div
v-if="!model.isFolder"
role="rowheader"
class="table-mobile-header">
Commit
</div>
......@@ -529,7 +532,7 @@ export default {
</div>
<div
v-if="!model.isFolder && !hasLastDeploymentKey"
class="commit-title">
class="commit-title table-mobile-content">
No deployments yet
</div>
</div>
......@@ -537,6 +540,7 @@ export default {
<div class="table-section section-10" role="gridcell">
<div
v-if="!model.isFolder"
role="rowheader"
class="table-mobile-header">
Updated
</div>
......
......@@ -66,19 +66,19 @@ export default {
<template>
<div class="ci-table" role="grid">
<div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section section-10 environments-name" role="rowheader">
<div class="table-section section-10 environments-name" role="columnheader">
Environment
</div>
<div class="table-section section-10 environments-deploy" role="rowheader">
<div class="table-section section-10 environments-deploy" role="columnheader">
Deployment
</div>
<div class="table-section section-15 environments-build" role="rowheader">
<div class="table-section section-15 environments-build" role="columnheader">
Job
</div>
<div class="table-section section-25 environments-commit" role="rowheader">
<div class="table-section section-25 environments-commit" role="columnheader">
Commit
</div>
<div class="table-section section-10 environments-date" role="rowheader">
<div class="table-section section-10 environments-date" role="columnheader">
Updated
</div>
</div>
......
......@@ -8,39 +8,87 @@ export default class FilterableList {
this.filterForm = form;
this.listFilterElement = filter;
this.listHolderElement = holder;
this.isBusy = false;
}
getFilterEndpoint() {
return `${this.filterForm.getAttribute('action')}?${$(this.filterForm).serialize()}`;
}
getPagePath() {
return this.getFilterEndpoint();
}
initSearch() {
this.debounceFilter = _.debounce(this.filterResults.bind(this), 500);
// Wrap to prevent passing event arguments to .filterResults;
this.debounceFilter = _.debounce(this.onFilterInput.bind(this), 500);
this.listFilterElement.removeEventListener('input', this.debounceFilter);
this.unbindEvents();
this.bindEvents();
}
onFilterInput() {
const $form = $(this.filterForm);
const queryData = {};
const filterGroupsParam = $form.find('[name="filter_groups"]').val();
if (filterGroupsParam) {
queryData.filter_groups = filterGroupsParam;
}
this.filterResults(queryData);
if (this.setDefaultFilterOption) {
this.setDefaultFilterOption();
}
}
bindEvents() {
this.listFilterElement.addEventListener('input', this.debounceFilter);
}
filterResults() {
const form = this.filterForm;
const filterUrl = `${form.getAttribute('action')}?${$(form).serialize()}`;
unbindEvents() {
this.listFilterElement.removeEventListener('input', this.debounceFilter);
}
filterResults(queryData) {
if (this.isBusy) {
return false;
}
$(this.listHolderElement).fadeTo(250, 0.5);
return $.ajax({
url: form.getAttribute('action'),
data: $(form).serialize(),
url: this.getFilterEndpoint(),
data: queryData,
type: 'GET',
dataType: 'json',
context: this,
complete() {
$(this.listHolderElement).fadeTo(250, 1);
complete: this.onFilterComplete,
beforeSend: () => {
this.isBusy = true;
},
success(data) {
this.listHolderElement.innerHTML = data.html;
// Change url so if user reload a page - search results are saved
return window.history.replaceState({
page: filterUrl,
}, document.title, filterUrl);
success: (response, textStatus, xhr) => {
this.onFilterSuccess(response, xhr, queryData);
},
});
}
onFilterSuccess(response, xhr, queryData) {
if (response.html) {
this.listHolderElement.innerHTML = response.html;
}
// Change url so if user reload a page - search results are saved
const currentPath = this.getPagePath(queryData);
return window.history.replaceState({
page: currentPath,
}, document.title, currentPath);
}
onFilterComplete() {
this.isBusy = false;
$(this.listHolderElement).fadeTo(250, 1);
}
}
......@@ -81,6 +81,41 @@ class FilteredSearchManager {
}
}
bindStateEvents() {
this.stateFilters = document.querySelector('.container-fluid .issues-state-filters');
if (this.stateFilters) {
this.searchStateWrapper = this.searchState.bind(this);
this.stateFilters.querySelector('[data-state="opened"]')
.addEventListener('click', this.searchStateWrapper);
this.stateFilters.querySelector('[data-state="closed"]')
.addEventListener('click', this.searchStateWrapper);
this.stateFilters.querySelector('[data-state="all"]')
.addEventListener('click', this.searchStateWrapper);
this.mergedState = this.stateFilters.querySelector('[data-state="merged"]');
if (this.mergedState) {
this.mergedState.addEventListener('click', this.searchStateWrapper);
}
}
}
unbindStateEvents() {
if (this.stateFilters) {
this.stateFilters.querySelector('[data-state="opened"]')
.removeEventListener('click', this.searchStateWrapper);
this.stateFilters.querySelector('[data-state="closed"]')
.removeEventListener('click', this.searchStateWrapper);
this.stateFilters.querySelector('[data-state="all"]')
.removeEventListener('click', this.searchStateWrapper);
if (this.mergedState) {
this.mergedState.removeEventListener('click', this.searchStateWrapper);
}
}
}
bindEvents() {
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
......@@ -116,6 +151,8 @@ class FilteredSearchManager {
document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
this.bindStateEvents();
}
unbindEvents() {
......@@ -136,6 +173,8 @@ class FilteredSearchManager {
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
this.unbindStateEvents();
}
checkForBackspace(e) {
......@@ -451,7 +490,19 @@ class FilteredSearchManager {
}
}
search() {
searchState(e) {
const target = e.currentTarget;
// remove focus outline after click
target.blur();
const state = target.dataset && target.dataset.state;
if (state) {
this.search(state);
}
}
search(state = null) {
const paths = [];
const searchQuery = gl.DropdownUtils.getSearchQuery();
......@@ -459,7 +510,7 @@ class FilteredSearchManager {
const { tokens, searchToken }
= this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys());
const currentState = gl.utils.getParameterByName('state') || 'opened';
const currentState = state || gl.utils.getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
tokens.forEach((token) => {
......
......@@ -248,7 +248,7 @@ GitLabDropdown = (function() {
return function(data) {
_this.fullData = data;
_this.parseData(_this.fullData);
_this.focusTextInput();
_this.focusTextInput(true);
if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
return _this.filter.input.trigger('input');
}
......@@ -743,8 +743,20 @@ GitLabDropdown = (function() {
return [selectedObject, isMarking];
};
GitLabDropdown.prototype.focusTextInput = function() {
if (this.options.filterable) { this.filterInput.focus(); }
GitLabDropdown.prototype.focusTextInput = function(triggerFocus = false) {
if (this.options.filterable) {
$(':focus').blur();
this.dropdown.one('transitionend', () => {
this.filterInput.focus();
});
if (triggerFocus) {
// This triggers after a ajax request
// in case of slow requests, the dropdown transition could already be finished
this.dropdown.trigger('transitionend');
}
}
};
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
......
<script>
export default {
props: {
groups: {
type: Object,
required: true,
},
baseGroup: {
type: Object,
required: false,
default: () => ({}),
},
},
};
</script>
<template>
<ul class="content-list group-list-tree">
<group-item
v-for="(group, index) in groups"
:key="index"
:group="group"
:base-group="baseGroup"
:collection="groups"
/>
</ul>
</template>
<script>
import eventHub from '../event_hub';
export default {
props: {
group: {
type: Object,
required: true,
},
baseGroup: {
type: Object,
required: false,
default: () => ({}),
},
collection: {
type: Object,
required: false,
default: () => ({}),
},
},
methods: {
onClickRowGroup(e) {
e.stopPropagation();
// Skip for buttons
if (!(e.target.tagName === 'A') && !(e.target.tagName === 'I' && e.target.parentElement.tagName === 'A')) {
if (this.group.hasSubgroups) {
eventHub.$emit('toggleSubGroups', this.group);
} else {
window.location.href = this.group.webUrl;
}
}
},
onLeaveGroup(e) {
e.preventDefault();
// eslint-disable-next-line no-alert
if (confirm(`Are you sure you want to leave the "${this.group.fullName}" group?`)) {
this.leaveGroup();
}
},
leaveGroup() {
eventHub.$emit('leaveGroup', this.group, this.collection);
},
},
computed: {
groupDomId() {
return `group-${this.group.id}`;
},
rowClass() {
return {
'group-row': true,
'is-open': this.group.isOpen,
'has-subgroups': this.group.hasSubgroups,
'no-description': !this.group.description,
};
},
visibilityIcon() {
return {
fa: true,
'fa-globe': this.group.visibility === 'public',
'fa-shield': this.group.visibility === 'internal',
'fa-lock': this.group.visibility === 'private',
};
},
fullPath() {
let fullPath = '';
if (this.group.isOrphan) {
// check if current group is baseGroup
if (Object.keys(this.baseGroup).length > 0 && this.baseGroup !== this.group) {
// Remove baseGroup prefix from our current group.fullName. e.g:
// baseGroup.fullName: `level1`
// group.fullName: `level1 / level2 / level3`
// Result: `level2 / level3`
const gfn = this.group.fullName;
const bfn = this.baseGroup.fullName;
const length = bfn.length;
const start = gfn.indexOf(bfn);
const extraPrefixChars = 3;
fullPath = gfn.substr(start + length + extraPrefixChars);
} else {
fullPath = this.group.fullName;
}
} else {
fullPath = this.group.name;
}
return fullPath;
},
hasGroups() {
return Object.keys(this.group.subGroups).length > 0;
},
},
};
</script>
<template>
<li
@click.stop="onClickRowGroup"
:id="groupDomId"
:class="rowClass"
>
<div
class="group-row-contents">
<div
class="controls">
<a
v-if="group.canEdit"
class="edit-group btn"
:href="group.editPath">
<i
class="fa fa-cogs"
aria-hidden="true"
>
</i>
</a>
<a
@click="onLeaveGroup"
:href="group.leavePath"
class="leave-group btn"
title="Leave this group">
<i
class="fa fa-sign-out"
aria-hidden="true"
>
</i>
</a>
</div>
<div
class="stats">
<span
class="number-projects">
<i
class="fa fa-bookmark"
aria-hidden="true"
>
</i>
{{group.numberProjects}}
</span>
<span
class="number-users">
<i
class="fa fa-users"
aria-hidden="true"
>
</i>
{{group.numberUsers}}
</span>
<span
class="group-visibility">
<i
:class="visibilityIcon"
aria-hidden="true"
>
</i>
</span>
</div>
<div
class="folder-toggle-wrap">
<span
class="folder-caret"
v-if="group.hasSubgroups">
<i
v-if="group.isOpen"
class="fa fa-caret-down"
aria-hidden="true"
>
</i>
<i
v-if="!group.isOpen"
class="fa fa-caret-right"
aria-hidden="true"
>
</i>
</span>
<span class="folder-icon">
<i
v-if="group.isOpen"
class="fa fa-folder-open"
aria-hidden="true"
>
</i>
<i
v-if="!group.isOpen"
class="fa fa-folder"
aria-hidden="true">
</i>
</span>
</div>
<div
class="avatar-container s40 hidden-xs">
<a
:href="group.webUrl">
<img
class="avatar s40"
:src="group.avatarUrl"
/>
</a>
</div>
<div
class="title">
<a
:href="group.webUrl">{{fullPath}}</a>
<template v-if="group.permissions.humanGroupAccess">
as
<span class="access-type">{{group.permissions.humanGroupAccess}}</span>
</template>
</div>
<div
class="description">{{group.description}}</div>
</div>
<group-folder
v-if="group.isOpen && hasGroups"
:groups="group.subGroups"
:baseGroup="group"
/>
</li>
</template>
<script>
import tablePagination from '~/vue_shared/components/table_pagination.vue';
import eventHub from '../event_hub';
export default {
props: {
groups: {
type: Object,
required: true,
},
pageInfo: {
type: Object,
required: true,
},
},
components: {
tablePagination,
},
methods: {
change(page) {
const filterGroupsParam = gl.utils.getParameterByName('filter_groups');
const sortParam = gl.utils.getParameterByName('sort');
eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam);
},
},
};
</script>
<template>
<div class="groups-list-tree-container">
<group-folder
:groups="groups"
/>
<table-pagination
:change="change"
:pageInfo="pageInfo"
/>
</div>
</template>
import Vue from 'vue';
export default new Vue();
import FilterableList from '~/filterable_list';
import eventHub from './event_hub';
export default class GroupFilterableList extends FilterableList {
constructor({ form, filter, holder, filterEndpoint, pagePath }) {
super(form, filter, holder);
this.form = form;
this.filterEndpoint = filterEndpoint;
this.pagePath = pagePath;
this.$dropdown = $('.js-group-filter-dropdown-wrap');
}
getFilterEndpoint() {
return this.filterEndpoint;
}
getPagePath(queryData) {
const params = queryData ? $.param(queryData) : '';
const queryString = params ? `?${params}` : '';
return `${this.pagePath}${queryString}`;
}
bindEvents() {
super.bindEvents();
this.onFormSubmitWrapper = this.onFormSubmit.bind(this);
this.onFilterOptionClikWrapper = this.onOptionClick.bind(this);
this.filterForm.addEventListener('submit', this.onFormSubmitWrapper);
this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper);
}
onFormSubmit(e) {
e.preventDefault();
const $form = $(this.form);
const filterGroupsParam = $form.find('[name="filter_groups"]').val();
const queryData = {};
if (filterGroupsParam) {
queryData.filter_groups = filterGroupsParam;
}
this.filterResults(queryData);
this.setDefaultFilterOption();
}
setDefaultFilterOption() {
const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu a:first-child').text());
this.$dropdown.find('.dropdown-label').text(defaultOption);
}
onOptionClick(e) {
e.preventDefault();
const queryData = {};
const sortParam = gl.utils.getParameterByName('sort', e.currentTarget.href);
if (sortParam) {
queryData.sort = sortParam;
}
this.filterResults(queryData);
// Active selected option
this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text));
// Clear current value on search form
this.form.querySelector('[name="filter_groups"]').value = '';
}
onFilterSuccess(data, xhr, queryData) {
super.onFilterSuccess(data, xhr, queryData);
const paginationData = {
'X-Per-Page': xhr.getResponseHeader('X-Per-Page'),
'X-Page': xhr.getResponseHeader('X-Page'),
'X-Total': xhr.getResponseHeader('X-Total'),
'X-Total-Pages': xhr.getResponseHeader('X-Total-Pages'),
'X-Next-Page': xhr.getResponseHeader('X-Next-Page'),
'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'),
};
eventHub.$emit('updateGroups', data);
eventHub.$emit('updatePagination', paginationData);
}
}
/* global Flash */
import Vue from 'vue';
import GroupFilterableList from './groups_filterable_list';
import GroupsComponent from './components/groups.vue';
import GroupFolder from './components/group_folder.vue';
import GroupItem from './components/group_item.vue';
import GroupsStore from './stores/groups_store';
import GroupsService from './services/groups_service';
import eventHub from './event_hub';
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('dashboard-group-app');
// Don't do anything if element doesn't exist (No groups)
// This is for when the user enters directly to the page via URL
if (!el) {
return;
}
Vue.component('groups-component', GroupsComponent);
Vue.component('group-folder', GroupFolder);
Vue.component('group-item', GroupItem);
// eslint-disable-next-line no-new
new Vue({
el,
data() {
this.store = new GroupsStore();
this.service = new GroupsService(el.dataset.endpoint);
return {
store: this.store,
isLoading: true,
state: this.store.state,
loading: true,
};
},
computed: {
isEmpty() {
return Object.keys(this.state.groups).length === 0;
},
},
methods: {
fetchGroups(parentGroup) {
let parentId = null;
let getGroups = null;
let page = null;
let sort = null;
let pageParam = null;
let sortParam = null;
let filterGroups = null;
let filterGroupsParam = null;
if (parentGroup) {
parentId = parentGroup.id;
} else {
this.isLoading = true;
}
pageParam = gl.utils.getParameterByName('page');
if (pageParam) {
page = pageParam;
}
filterGroupsParam = gl.utils.getParameterByName('filter_groups');
if (filterGroupsParam) {
filterGroups = filterGroupsParam;
}
sortParam = gl.utils.getParameterByName('sort');
if (sortParam) {
sort = sortParam;
}
getGroups = this.service.getGroups(parentId, page, filterGroups, sort);
getGroups
.then(response => response.json())
.then((response) => {
this.isLoading = false;
this.updateGroups(response, parentGroup);
})
.catch(this.handleErrorResponse);
return getGroups;
},
fetchPage(page, filterGroups, sort) {
this.isLoading = true;
return this.service
.getGroups(null, page, filterGroups, sort)
.then((response) => {
this.isLoading = false;
$.scrollTo(0);
const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href);
window.history.replaceState({
page: currentPath,
}, document.title, currentPath);
this.updateGroups(response.json());
this.updatePagination(response.headers);
})
.catch(this.handleErrorResponse);
},
toggleSubGroups(parentGroup = null) {
if (!parentGroup.isOpen) {
this.store.resetGroups(parentGroup);
this.fetchGroups(parentGroup);
}
this.store.toggleSubGroups(parentGroup);
},
leaveGroup(group, collection) {
this.service.leaveGroup(group.leavePath)
.then((response) => {
$.scrollTo(0);
this.store.removeGroup(group, collection);
// eslint-disable-next-line no-new
new Flash(response.json().notice, 'notice');
})
.catch((response) => {
let message = 'An error occurred. Please try again.';
if (response.status === 403) {
message = 'Failed to leave the group. Please make sure you are not the only owner';
}
// eslint-disable-next-line no-new
new Flash(message);
});
},
updateGroups(groups, parentGroup) {
this.store.setGroups(groups, parentGroup);
},
updatePagination(headers) {
this.store.storePagination(headers);
},
handleErrorResponse() {
this.isLoading = false;
$.scrollTo(0);
// eslint-disable-next-line no-new
new Flash('An error occurred. Please try again.');
},
},
created() {
eventHub.$on('fetchPage', this.fetchPage);
eventHub.$on('toggleSubGroups', this.toggleSubGroups);
eventHub.$on('leaveGroup', this.leaveGroup);
eventHub.$on('updateGroups', this.updateGroups);
eventHub.$on('updatePagination', this.updatePagination);
},
beforeMount() {
let groupFilterList = null;
const form = document.querySelector('form#group-filter-form');
const filter = document.querySelector('.js-groups-list-filter');
const holder = document.querySelector('.js-groups-list-holder');
const opts = {
form,
filter,
holder,
filterEndpoint: el.dataset.endpoint,
pagePath: el.dataset.path,
};
groupFilterList = new GroupFilterableList(opts);
groupFilterList.initSearch();
},
mounted() {
this.fetchGroups()
.then((response) => {
this.updatePagination(response.headers);
this.isLoading = false;
})
.catch(this.handleErrorResponse);
},
beforeDestroy() {
eventHub.$off('fetchPage', this.fetchPage);
eventHub.$off('toggleSubGroups', this.toggleSubGroups);
eventHub.$off('leaveGroup', this.leaveGroup);
eventHub.$off('updateGroups', this.updateGroups);
eventHub.$off('updatePagination', this.updatePagination);
},
});
});
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class GroupsService {
constructor(endpoint) {
this.groups = Vue.resource(endpoint);
}
getGroups(parentId, page, filterGroups, sort) {
const data = {};
if (parentId) {
data.parent_id = parentId;
} else {
// Do not send the following param for sub groups
if (page) {
data.page = page;
}
if (filterGroups) {
data.filter_groups = filterGroups;
}
if (sort) {
data.sort = sort;
}
}
return this.groups.get(data);
}
// eslint-disable-next-line class-methods-use-this
leaveGroup(endpoint) {
return Vue.http.delete(endpoint);
}
}
import Vue from 'vue';
export default class GroupsStore {
constructor() {
this.state = {};
this.state.groups = {};
this.state.pageInfo = {};
}
setGroups(rawGroups, parent) {
const parentGroup = parent;
const tree = this.buildTree(rawGroups, parentGroup);
if (parentGroup) {
parentGroup.subGroups = tree;
} else {
this.state.groups = tree;
}
return tree;
}
// eslint-disable-next-line class-methods-use-this
resetGroups(parent) {
const parentGroup = parent;
parentGroup.subGroups = {};
}
storePagination(pagination = {}) {
let paginationInfo;
if (Object.keys(pagination).length) {
const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
} else {
paginationInfo = pagination;
}
this.state.pageInfo = paginationInfo;
}
buildTree(rawGroups, parentGroup) {
const groups = this.decorateGroups(rawGroups);
const tree = {};
const mappedGroups = {};
const orphans = [];
// Map groups to an object
groups.map((group) => {
mappedGroups[group.id] = group;
mappedGroups[group.id].subGroups = {};
return group;
});
Object.keys(mappedGroups).map((key) => {
const currentGroup = mappedGroups[key];
if (currentGroup.parentId) {
// If the group is not at the root level, add it to its parent array of subGroups.
const findParentGroup = mappedGroups[currentGroup.parentId];
if (findParentGroup) {
mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup;
mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups
} else if (parentGroup && parentGroup.id === currentGroup.parentId) {
tree[currentGroup.id] = currentGroup;
} else {
// Means the groups hast no direct parent.
// Save for later processing, we will add them to its corresponding base group
orphans.push(currentGroup);
}
} else {
// If the group is at the root level, add it to first level elements array.
tree[currentGroup.id] = currentGroup;
}
return key;
});
// Hopefully this array will be empty for most cases
if (orphans.length) {
orphans.map((orphan) => {
let found = false;
const currentOrphan = orphan;
Object.keys(tree).map((key) => {
const group = tree[key];
if (currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0) {
group.subGroups[currentOrphan.id] = currentOrphan;
group.isOpen = true;
currentOrphan.isOrphan = true;
found = true;
}
return key;
});
if (!found) {
currentOrphan.isOrphan = true;
tree[currentOrphan.id] = currentOrphan;
}
return orphan;
});
}
return tree;
}
decorateGroups(rawGroups) {
this.groups = rawGroups.map(this.decorateGroup);
return this.groups;
}
// eslint-disable-next-line class-methods-use-this
decorateGroup(rawGroup) {
return {
id: rawGroup.id,
fullName: rawGroup.full_name,
fullPath: rawGroup.full_path,
avatarUrl: rawGroup.avatar_url,
name: rawGroup.name,
hasSubgroups: rawGroup.has_subgroups,
canEdit: rawGroup.can_edit,
description: rawGroup.description,
webUrl: rawGroup.web_url,
parentId: rawGroup.parent_id,
visibility: rawGroup.visibility,
leavePath: rawGroup.leave_path,
editPath: rawGroup.edit_path,
isOpen: false,
isOrphan: false,
numberProjects: rawGroup.number_projects_with_delimiter,
numberUsers: rawGroup.number_users_with_delimiter,
permissions: {
humanGroupAccess: rawGroup.permissions.human_group_access,
},
subGroups: {},
};
}
// eslint-disable-next-line class-methods-use-this
removeGroup(group, collection) {
Vue.delete(collection, group.id);
}
// eslint-disable-next-line class-methods-use-this
toggleSubGroups(toggleGroup) {
const group = toggleGroup;
group.isOpen = !group.isOpen;
return group;
}
}
......@@ -167,8 +167,8 @@
if the name does not exist this function will return `null`
otherwise it will return the value of the param key provided
*/
w.gl.utils.getParameterByName = (name) => {
const url = window.location.href;
w.gl.utils.getParameterByName = (name, parseUrl) => {
const url = parseUrl || window.location.href;
name = name.replace(/[[\]]/g, '\\$&');
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
const results = regex.exec(url);
......
var locales = locales || {}; locales['bg'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-05 09:40-0400","Last-Translator":"Lyubomir Vasilev <lyubomirv@abv.bg>","Language-Team":"Bulgarian","Language":"bg","X-Generator":"Zanata 3.9.6","Plural-Forms":"nplurals=2; plural=(n != 1)","lang":"bg","domain":"app","plural_forms":"nplurals=2; plural=(n != 1)"},"ByAuthor|by":["от"],"Commit":["Подаване","Подавания"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Анализът на циклите дава общ поглед върху това колко време е нужно на една идея да се превърне в завършена функционалност в проекта."],"CycleAnalyticsStage|Code":["Програмиране"],"CycleAnalyticsStage|Issue":["Проблем"],"CycleAnalyticsStage|Plan":["Планиране"],"CycleAnalyticsStage|Production":["Издаване"],"CycleAnalyticsStage|Review":["Преглед и одобрение"],"CycleAnalyticsStage|Staging":["Подготовка за издаване"],"CycleAnalyticsStage|Test":["Тестване"],"Deploy":["Внедряване","Внедрявания"],"FirstPushedBy|First":["Първо"],"FirstPushedBy|pushed by":["изпращане на промени от"],"From issue creation until deploy to production":["От създаването на проблема до внедряването в крайната версия"],"From merge request merge until deploy to production":["От прилагането на заявката за сливане до внедряването в крайната версия"],"Introducing Cycle Analytics":["Представяме Ви анализът на циклите"],"Last %d day":["Последния %d ден","Последните %d дни"],"Limited to showing %d event at most":["Ограничено до показване на последното %d събитие","Ограничено до показване на последните %d събития"],"Median":["Медиана"],"New Issue":["Нов проблем","Нови проблема"],"Not available":["Не е налично"],"Not enough data":["Няма достатъчно данни"],"OpenedNDaysAgo|Opened":["Отворен"],"Pipeline Health":["Състояние"],"ProjectLifecycle|Stage":["Етап"],"Read more":["Прочетете повече"],"Related Commits":["Свързани подавания"],"Related Deployed Jobs":["Свързани задачи за внедряване"],"Related Issues":["Свързани проблеми"],"Related Jobs":["Свързани задачи"],"Related Merge Requests":["Свързани заявки за сливане"],"Related Merged Requests":["Свързани приложени заявки за сливане"],"Showing %d event":["Показване на %d събитие","Показване на %d събития"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["Етапът на програмиране показва времето от първото подаване до създаването на заявката за сливане. Данните ще бъдат добавени тук автоматично след като бъде създадена първата заявка за сливане."],"The collection of events added to the data gathered for that stage.":["Съвкупността от събития добавени към данните събрани за този етап."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["Етапът на проблемите показва колко е времето от създаването на проблем до определянето на целеви етап на проекта за него, или до добавянето му в списък на дъската за проблеми. Започнете да добавяте проблеми, за да видите данните за този етап."],"The phase of the development lifecycle.":["Етапът от цикъла на разработка"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["Етапът на планиране показва колко е времето от преходната стъпка до изпращането на първото подаване. Това време ще бъде добавено автоматично след като изпратите първото си подаване."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["Етапът на издаване показва общото време, което е нужно от създаването на проблем до внедряването на кода в крайната версия."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["Етапът на преглед и одобрение показва времето от създаването на заявката за сливане до прилагането ѝ. Данните ще бъдат добавени автоматично след като приложите първата си заявка за сливане."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["Етапът на подготовка за издаване показва времето между прилагането на заявката за сливане и внедряването на кода в средата на работещата крайна версия. Данните ще бъдат добавени автоматично след като направите първото си внедряване в крайната версия."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["Етапът на тестване показва времето, което е нужно на „Gitlab CI“ да изпълни всички задачи за свързаната заявка за сливане. Данните ще бъдат добавени автоматично след като приключи изпълнените на първата Ви такава задача."],"The time taken by each data entry gathered by that stage.":["Времето, което отнема всеки запис от данни за съответния етап."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["Стойността, която се намира в средата на последователността от наблюдавани данни. Например: медианата на 3, 5 и 9 е 5, а медианата на 3, 5, 7 и 8 е (5+7)/2 = 6."],"Time before an issue gets scheduled":["Време преди един проблем да бъде планиран за работа"],"Time before an issue starts implementation":["Време преди работата по проблем да започне"],"Time between merge request creation and merge/close":["Време между създаване на заявка за сливане и прилагането/отхвърлянето ѝ"],"Time until first merge request":["Време преди първата заявка за сливане"],"Time|hr":["час","часа"],"Time|min":["мин","мин"],"Time|s":["сек"],"Total Time":["Общо време"],"Total test time for all commits/merges":["Общо време за тестване на всички подавания/сливания"],"Want to see the data? Please ask an administrator for access.":["Искате ли да видите данните? Помолете администратор за достъп."],"We don't have enough data to show this stage.":["Няма достатъчно данни за този етап."],"You need permission.":["Нуждаете се от разрешение."],"day":["ден","дни"]}}};
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
var locales = locales || {}; locales['pt_BR'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-05 03:29-0400","Last-Translator":"Alexandre Alencar <alexandre.alencar@gmail.com>","Language-Team":"Portuguese (Brazil)","Language":"pt-BR","X-Generator":"Zanata 3.9.6","Plural-Forms":"nplurals=2; plural=(n != 1)","lang":"pt_BR","domain":"app","plural_forms":"nplurals=2; plural=(n != 1)"},"ByAuthor|by":["por"],"Commit":["Commit","Commits"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["A Análise de Ciclo fornece uma visão geral de quanto tempo uma ideia demora para ir para produção em seu projeto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Tarefa"],"CycleAnalyticsStage|Plan":["Plano"],"CycleAnalyticsStage|Production":["Produção"],"CycleAnalyticsStage|Review":["Revisão"],"CycleAnalyticsStage|Staging":["Homologação"],"CycleAnalyticsStage|Test":["Teste"],"Deploy":["Implantação","Implantações"],"FirstPushedBy|First":["Primeiro"],"FirstPushedBy|pushed by":["publicado por"],"From issue creation until deploy to production":["Da criação de tarefas até a implantação para a produção"],"From merge request merge until deploy to production":["Da incorporação do merge request até a implantação em produção"],"Introducing Cycle Analytics":["Apresentando a Análise de Ciclo"],"Last %d day":["Último %d dia","Últimos %d dias"],"Limited to showing %d event at most":["Limitado a mostrar %d evento no máximo","Limitado a mostrar %d eventos no máximo"],"Median":["Mediana"],"New Issue":["Nova Tarefa","Novas Tarefas"],"Not available":["Não disponível"],"Not enough data":["Dados insuficientes"],"OpenedNDaysAgo|Opened":["Aberto"],"Pipeline Health":["Saúde da Pipeline"],"ProjectLifecycle|Stage":["Etapa"],"Read more":["Ler mais"],"Related Commits":["Commits Relacionados"],"Related Deployed Jobs":["Jobs Relacionados Incorporados"],"Related Issues":["Tarefas Relacionadas"],"Related Jobs":["Jobs Relacionados"],"Related Merge Requests":["Merge Requests Relacionados"],"Related Merged Requests":["Merge Requests Relacionados"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["O estágio de codificação mostra o tempo desde o primeiro commit até a criação do merge request. \\nOs dados serão automaticamente adicionados aqui uma vez que você tenha criado seu primeiro merge request."],"The collection of events added to the data gathered for that stage.":["A coleção de eventos adicionados aos dados coletados para esse estágio."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["O estágio em questão mostra o tempo que leva desde a criação de uma tarefa até a sua assinatura para um milestone, ou a sua adição para a lista no seu Painel de Tarefas. Comece a criar tarefas para ver dados para esta etapa."],"The phase of the development lifecycle.":["A fase do ciclo de vida do desenvolvimento."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["A fase de planejamento mostra o tempo do passo anterior até empurrar o seu primeiro commit. Este tempo será adicionado automaticamente assim que você realizar seu primeiro commit."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["O estágio de produção mostra o tempo total que leva entre criar uma tarefa e implantar o código na produção. Os dados serão adicionados automaticamente até que você complete todo o ciclo de produção."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["A etapa de revisão mostra o tempo de criação de um merge request até que o merge seja feito. Os dados serão automaticamente adicionados depois que você fizer seu primeiro merge request."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["O estágio de estágio mostra o tempo entre a fusão do MR e o código de implantação para o ambiente de produção. Os dados serão automaticamente adicionados depois de implantar na produção pela primeira vez."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["A fase de teste mostra o tempo que o GitLab CI leva para executar cada pipeline para o merge request relacionado. Os dados serão automaticamente adicionados após a conclusão do primeiro pipeline."],"The time taken by each data entry gathered by that stage.":["O tempo necessário para cada entrada de dados reunida por essa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["O valor situado no ponto médio de uma série de valores observados. Ex., entre 3, 5, 9, a mediana é 5. Entre 3, 5, 7, 8, a mediana é (5 + 7) / 2 = 6."],"Time before an issue gets scheduled":["Tempo até que uma tarefa seja planejada"],"Time before an issue starts implementation":["Tempo até que uma tarefa comece a ser implementada"],"Time between merge request creation and merge/close":["Tempo entre a criação do merge request e o merge/fechamento"],"Time until first merge request":["Tempo até o primeiro merge request"],"Time|hr":["h","hs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tempo Total"],"Total test time for all commits/merges":["Tempo de teste total para todos os commits/merges"],"Want to see the data? Please ask an administrator for access.":["Precisa visualizar os dados? Solicite acesso ao administrador."],"We don't have enough data to show this stage.":["Não temos dados suficientes para mostrar esta fase."],"You need permission.":["Você precisa de permissão."],"day":["dia","dias"]}}};
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -39,10 +39,6 @@ import './shortcuts_network';
// behaviors
import './behaviors/';
// blob
import './blob/create_branch_dropdown';
import './blob/target_branch_dropdown';
// templates
import './templates/issuable_template_selector';
import './templates/issuable_template_selectors';
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */
(function() {
this.NewCommitForm = (function() {
function NewCommitForm(form, targetBranchName = 'target_branch') {
function NewCommitForm(form) {
this.form = form;
this.targetBranchName = targetBranchName;
this.renderDestination = this.renderDestination.bind(this);
this.targetBranchDropdown = form.find('button.js-target-branch');
this.branchName = form.find('.js-branch-name');
this.originalBranch = form.find('.js-original-branch');
this.createMergeRequest = form.find('.js-create-merge-request');
this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
this.targetBranchDropdown.on('change.branch', this.renderDestination);
this.branchName.keyup(this.renderDestination);
this.renderDestination();
}
NewCommitForm.prototype.renderDestination = function() {
var different;
var targetBranch = this.form.find(`input[name="${this.targetBranchName}"]`);
different = targetBranch.val() !== this.originalBranch.val();
different = this.branchName.val() !== this.originalBranch.val();
if (different) {
this.createMergeRequestContainer.show();
if (!this.wasDifferent) {
......
......@@ -1478,7 +1478,7 @@ const normalizeNewlines = function(str) {
const cachedNoteBodyText = $noteBodyText.html();
// Show updated comment content temporarily
$noteBodyText.html(formContent);
$noteBodyText.html(_.escape(formContent));
$editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half');
$editingNote.find('.note-headline-meta a').html('<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>');
......@@ -1491,7 +1491,7 @@ const normalizeNewlines = function(str) {
})
.fail(() => {
// Submission failed, revert back to original note
$noteBodyText.html(cachedNoteBodyText);
$noteBodyText.html(_.escape(cachedNoteBodyText));
$editingNote.removeClass('being-posted fade-in');
$editingNote.find('.fa.fa-spinner').remove();
......
......@@ -16,6 +16,7 @@
/* global Flash */
import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
export default {
props: {
......@@ -31,6 +32,10 @@ export default {
},
},
mixins: [
tooltipMixin,
],
data() {
return {
isLoading: false,
......@@ -127,9 +132,10 @@ export default {
<template>
<div class="dropdown">
<button
ref="tooltip"
:class="triggerButtonClass"
@click="onClickStage"
class="mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button"
class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button"
:title="stage.title"
data-placement="top"
data-toggle="dropdown"
......
......@@ -284,9 +284,7 @@ export default {
<table-pagination
v-if="shouldRenderPagination"
:pagenum="pagenum"
:change="change"
:count="state.count.all"
:pageInfo="state.pageInfo"
/>
</div>
......
.awards {
display: flex;
flex-wrap: wrap;
.emoji-icon {
width: 20px;
height: 20px;
......@@ -100,7 +103,6 @@
.award-menu-holder {
display: inline-block;
position: absolute;
.tooltip {
white-space: nowrap;
......@@ -108,9 +110,11 @@
}
.award-control {
margin: 0 5px 6px 0;
margin: 4px 8px 4px 0;
outline: 0;
position: relative;
display: block;
float: left;
&.disabled {
cursor: default;
......
......@@ -48,6 +48,10 @@
@include chevron-active;
border-color: $gray-darkest;
}
[data-toggle="dropdown"] {
outline: 0;
}
}
.dropdown-toggle {
......@@ -109,6 +113,7 @@
&:focus:active {
@include chevron-active;
border-color: $dropdown-toggle-active-border-color;
outline: 0;
}
}
......@@ -201,6 +206,11 @@
width: 100%;
}
&.dropdown-open-left {
right: 0;
left: auto;
}
&.is-loading {
.dropdown-content {
display: none;
......@@ -261,7 +271,14 @@
text-transform: capitalize;
}
.separator + .dropdown-header {
.dropdown-bold-header {
font-weight: 600;
line-height: 22px;
padding: 0 16px;
}
.separator + .dropdown-header,
.separator + .dropdown-bold-header {
padding-top: 2px;
}
......
......@@ -270,3 +270,103 @@ ul.controls {
ul.indent-list {
padding: 10px 0 0 30px;
}
// Specific styles for tree list
.group-list-tree {
.folder-toggle-wrap {
float: left;
line-height: $list-text-height;
font-size: 0;
span {
font-size: $gl-font-size;
}
}
.folder-caret,
.folder-icon {
display: inline-block;
}
.folder-caret {
width: 15px;
}
.folder-icon {
width: 20px;
}
> .group-row:not(.has-subgroups) {
.folder-caret .fa {
opacity: 0;
}
}
.content-list li:last-child {
padding-bottom: 0;
}
.group-list-tree {
margin-bottom: 0;
margin-left: 30px;
position: relative;
&::before {
content: '';
display: block;
width: 0;
position: absolute;
top: 5px;
bottom: 0;
left: -16px;
border-left: 2px solid $border-white-normal;
}
.group-row {
position: relative;
&::before {
content: "";
display: block;
width: 10px;
height: 0;
border-top: 2px solid $border-white-normal;
position: absolute;
top: 30px;
left: -16px;
}
&:last-child::before {
background: $white-light;
height: auto;
top: 30px;
bottom: 0;
}
}
}
.group-row {
padding: 0;
border: none;
}
.group-row-contents {
padding: 10px 10px 8px;
border-top: solid 1px transparent;
border-bottom: solid 1px $white-normal;
&:hover {
border-color: $row-hover-border;
background-color: $row-hover;
cursor: pointer;
}
}
}
.js-groups-list-holder {
.groups-list-loading {
font-size: 34px;
text-align: center;
}
}
......@@ -45,7 +45,8 @@
li {
display: flex;
a {
a,
.btn-link {
padding: $gl-btn-padding;
padding-bottom: 11px;
font-size: 14px;
......@@ -67,7 +68,29 @@
}
}
&.active a {
.btn-link {
padding-top: 16px;
padding-left: 15px;
padding-right: 15px;
border-left: none;
border-right: none;
border-top: none;
border-radius: 0;
&:hover,
&:active,
&:focus {
background-color: transparent;
}
&:active {
outline: 0;
box-shadow: none;
}
}
&.active a,
&.active .btn-link {
border-bottom: 2px solid $link-underline-blue;
color: $black;
font-weight: 600;
......
......@@ -19,10 +19,6 @@
.table-section {
white-space: nowrap;
.branch-commit {
max-width: 100%;
}
$section-widths: 10 15 20 25 30 40;
@each $width in $section-widths {
&.section-#{$width} {
......@@ -87,4 +83,9 @@
@media (min-width: $screen-md-min) {
flex: 0 0 90%;
}
.avatar {
float: none;
margin-right: 4px;
}
}
......@@ -125,9 +125,51 @@
@media (min-width: $screen-sm-min) {
width: 400px;
}
&.is-expandable {
.board-header {
cursor: pointer;
}
}
&.is-collapsed {
width: 50px;
.board-header {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.board-title {
position: initial;
padding: 0;
border-bottom: 0;
> span {
display: block;
transform: rotate(90deg) translate(25px, 0);
}
}
.board-title-expandable-toggle {
position: absolute;
top: 50%;
left: 50%;
margin-left: -10px;
}
.board-list-component,
.board-issue-count-holder {
display: none;
}
}
}
.board-inner {
position: relative;
height: 100%;
font-size: $issue-boards-font-size;
background: $gray-light;
......
......@@ -71,7 +71,9 @@
height: 35px;
display: flex;
justify-content: flex-end;
border-bottom: 1px outset $white-light;
background: $gray-light;
border: 1px solid $gray-normal;
color: $gl-text-color;
.truncated-info {
margin: 0 auto;
......@@ -82,7 +84,7 @@
}
.raw-link {
color: inherit;
color: $gl-text-color;
margin-left: 5px;
text-decoration: underline;
}
......@@ -93,17 +95,25 @@
display: flex;
align-self: center;
font-size: 15px;
margin-bottom: 4px;
svg {
height: 15px;
display: block;
fill: $white-light;
fill: $gl-text-color;
}
a,
.controllers-buttons,
.btn-scroll {
margin: 0 8px;
color: $white-light;
color: $gl-text-color;
height: 15px;
vertical-align: middle;
padding: 0;
width: 12px;
}
.controllers-buttons {
margin: 1px 10px;
}
.btn-scroll.animate {
......@@ -137,9 +147,9 @@
top: 35px;
left: 10px;
bottom: 0;
overflow-y: hidden;
padding-bottom: 20px;
padding-right: 20px;
overflow-y: scroll;
overflow-x: hidden;
padding: 10px 20px 20px 5px;
}
.environment-information {
......
......@@ -228,7 +228,7 @@
margin: 10px 0;
background: $gray-light;
display: none;
white-space: pre-line;
white-space: pre-wrap;
word-break: normal;
pre {
......
......@@ -285,7 +285,7 @@
border-top: 1px solid $border-color;
.environment-action-buttons {
padding: 10px;
padding: 10px 5px;
display: flex;
.btn {
......@@ -293,15 +293,20 @@
}
> .btn-group,
.external-url,
.btn {
> .external-url,
> .btn {
flex: 1;
flex-basis: 28px;
margin: 0 5px;
}
.dropdown-new {
width: 100%;
}
.dropdown-menu {
min-width: initial;
}
}
}
}
......
......@@ -58,7 +58,7 @@
}
.emoji-block {
padding: 10px 0 4px;
padding: 10px 0;
}
}
......@@ -727,3 +727,33 @@
}
}
}
.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;
}
}
}
......@@ -264,14 +264,19 @@ ul.related-merge-requests > li {
}
@media (min-width: $screen-sm-min) {
.new-branch-col {
padding-top: 0;
text-align: right;
}
.emoji-block .row {
display: flex;
.create-mr-dropdown-wrap {
.btn-group:not(.hide) {
display: inline-block;
.new-branch-col {
padding-top: 0;
text-align: right;
align-self: center;
}
.create-mr-dropdown-wrap {
.btn-group:not(.hide) {
display: inline-block;
}
}
}
}
......@@ -128,6 +128,7 @@
a {
width: 100%;
font-size: 18px;
margin-right: 0;
&:hover {
border: 1px solid transparent;
......@@ -140,6 +141,7 @@
a {
border: none;
border-bottom: 2px solid $link-underline-blue;
margin-right: 0;
color: $black;
&:hover {
......
......@@ -3,6 +3,41 @@
border-bottom: 1px solid $border-color;
}
.project-member-tabs {
background: $gray-light;
border: 1px solid $border-color;
li {
width: 50%;
&.active {
background: $white-light;
}
&:first-child {
border-right: 1px solid $border-color;
}
a {
width: 100%;
text-align: center;
}
}
}
.users-project-form {
.btn-create {
margin-right: 10px;
}
}
.project-member-tab-content {
padding: $gl-padding;
border: 1px solid $border-color;
border-top: 0;
margin-bottom: $gl-padding;
}
.member {
&.is-overriden {
.btn-ldap-override {
......
......@@ -103,41 +103,6 @@
}
}
.confidential-issue-warning {
background-color: $gray-normal;
border-radius: 3px;
padding: 3px 12px;
margin: auto;
margin-top: 0;
text-align: center;
font-size: 12px;
align-items: center;
@media (max-width: $screen-md-max) {
// On smaller devices the warning becomes the fourth item in the list,
// rather than centering, and grows to span the full width of the
// comment area.
order: 4;
margin: 6px auto;
width: 100%;
}
.fa {
margin-right: 8px;
}
}
.right-sidebar-expanded {
.confidential-issue-warning {
// When the sidebar is open the warning becomes the fourth item in the list,
// rather than centering, and grows to span the full width of the
// comment area.
order: 4;
margin: 6px auto;
width: 100%;
}
}
.discussion-form {
padding: $gl-padding-top $gl-padding $gl-padding;
background-color: $white-light;
......
......@@ -38,9 +38,12 @@ ul.notes {
}
.discussion {
overflow: hidden;
display: block;
position: relative;
.diff-content {
overflow: visible;
}
}
> li {
......@@ -443,6 +446,52 @@ ul.notes {
.note-action-button {
margin-left: 8px;
}
.more-actions-toggle {
margin-left: 2px;
}
}
.more-actions {
display: inline;
.tooltip {
white-space: nowrap;
}
}
.more-actions-toggle {
padding: 0;
&:hover .icon,
&:focus .icon {
color: $blue-600;
}
.icon {
padding: 0 6px;
}
}
.more-actions-dropdown {
width: 180px;
min-width: 180px;
margin-top: $gl-btn-padding;
li > a,
li > .btn {
color: $gl-text-color;
padding: $gl-btn-padding;
width: 100%;
text-align: left;
&:hover,
&:focus {
color: $gl-text-color;
background-color: $blue-25;
border-radius: $border-radius-default;
}
}
}
.discussion-actions {
......
......@@ -793,8 +793,7 @@ a.allowed-to-push {
}
.project-refs-form .dropdown-menu,
.dropdown-menu-projects,
.dropdown-menu-branches {
.dropdown-menu-projects {
width: 300px;
@media (min-width: $screen-sm-min) {
......
class Admin::DeployKeysController < Admin::ApplicationController
before_action :deploy_keys, only: [:index]
before_action :deploy_key, only: [:destroy]
before_action :deploy_key, only: [:destroy, :edit, :update]
def index
end
......@@ -10,12 +10,24 @@ class Admin::DeployKeysController < Admin::ApplicationController
end
def create
@deploy_key = deploy_keys.new(deploy_key_params.merge(user: current_user))
@deploy_key = deploy_keys.new(create_params.merge(user: current_user))
if @deploy_key.save
redirect_to admin_deploy_keys_path
else
render "new"
render 'new'
end
end
def edit
end
def update
if deploy_key.update_attributes(update_params)
flash[:notice] = 'Deploy key was successfully updated.'
redirect_to admin_deploy_keys_path
else
render 'edit'
end
end
......@@ -38,7 +50,11 @@ class Admin::DeployKeysController < Admin::ApplicationController
@deploy_keys ||= DeployKey.are_public
end
def deploy_key_params
def create_params
params.require(:deploy_key).permit(:key, :title, :can_push)
end
def update_params
params.require(:deploy_key).permit(:title, :can_push)
end
end
......@@ -21,7 +21,7 @@ class AutocompleteController < ApplicationController
@users = [current_user, *@users].uniq
end
if params[:author_id].present?
if params[:author_id].present? && current_user
author = User.find_by_id(params[:author_id])
@users = [author, *@users].uniq if author
end
......
module CreatesCommit
extend ActiveSupport::Concern
def set_start_branch_to_branch_name
branch_exists = @repository.find_branch(@branch_name)
@start_branch = @branch_name if branch_exists
end
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
if can?(current_user, :push_code, @project)
@project_to_commit_into = @project
......
......@@ -54,11 +54,20 @@ module MembershipActions
"You left the \"#{membershipable.human_name}\" #{source_type}."
end
<<<<<<< HEAD
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
>>>>>>> ce-com/master
redirect_to redirect_path, notice: notice
format.json { render json: { notice: notice } }
end
end
protected
......
......@@ -46,8 +46,10 @@ module MilestoneActions
def milestone_redirect_path
if @project
namespace_project_milestone_path(@project.namespace, @project, @milestone)
else
elsif @group
group_milestone_path(@group, @milestone.safe_title, title: @milestone.title)
else
dashboard_milestone_path(@milestone.safe_title, title: @milestone.title)
end
end
end
class Dashboard::GroupsController < Dashboard::ApplicationController
def index
@group_members = current_user.group_members.includes(source: :route).joins(:group)
@group_members = @group_members.merge(Group.search(params[:filter_groups])) if params[:filter_groups].present?
@group_members = @group_members.merge(Group.sort(@sort = params[:sort]))
@group_members = @group_members.page(params[:page])
@groups =
if params[:parent_id] && Group.supports_nested_groups?
parent = Group.find_by(id: params[:parent_id])
if can?(current_user, :read_group, parent)
GroupsFinder.new(current_user, parent: parent).execute
else
Group.none
end
else
current_user.groups
end
@groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present?
@groups = @groups.includes(:route)
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.page(params[:page])
respond_to do |format|
format.html
format.json do
render json: {
html: view_to_html_string("dashboard/groups/_groups", locals: { group_members: @group_members })
}
render json: GroupSerializer
.new(current_user: @current_user)
.with_pagination(request, response)
.represent(@groups)
end
end
end
......
class Dashboard::MilestonesController < Dashboard::ApplicationController
include MilestoneActions
before_action :projects
before_action :milestone, only: [:show]
before_action :milestone, only: [:show, :merge_requests, :participants, :labels]
def index
respond_to do |format|
......
......@@ -39,7 +39,7 @@ class JwtController < ApplicationController
errors: [
{ code: 'UNAUTHORIZED',
message: "HTTP Basic: Access denied\n" \
"You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
"You must use a personal access token with 'api' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}" }
]
}, status: 401
......
......@@ -26,8 +26,6 @@ class Projects::BlobController < Projects::ApplicationController
end
def create
set_start_branch_to_branch_name
create_commit(Files::CreateService, success_notice: "The file has been successfully created.",
success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@branch_name, @file_path)) },
failure_view: :new,
......@@ -55,7 +53,7 @@ class Projects::BlobController < Projects::ApplicationController
def edit
if can_collaborate_with_project?
blob.load_all_data!(@repository)
blob.load_all_data!
else
redirect_to action: 'show'
end
......@@ -74,7 +72,7 @@ class Projects::BlobController < Projects::ApplicationController
def preview
@content = params[:content]
@blob.load_all_data!(@repository)
@blob.load_all_data!
diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true)
diff_lines = diffy.diff.scan(/.*\n/)[2..-1]
diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines)
......@@ -93,9 +91,11 @@ class Projects::BlobController < Projects::ApplicationController
def diff
apply_diff_view_cookie!
@form = UnfoldForm.new(params)
@lines = Gitlab::Highlight.highlight_lines(repository, @ref, @path)
@lines = @lines[@form.since - 1..@form.to - 1]
@blob.load_all_data!
@lines = Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: @repository).lines
@form = UnfoldForm.new(params)
@lines = @lines[@form.since - 1..@form.to - 1].map(&:html_safe)
if @form.bottom?
@match_line = ''
......@@ -111,7 +111,7 @@ class Projects::BlobController < Projects::ApplicationController
private
def blob
@blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path), @project)
@blob ||= @repository.blob_at(@commit.id, @path)
if @blob
@blob
......
......@@ -5,7 +5,9 @@ module Projects
before_action :authorize_read_list!, only: [:index]
def index
render json: serialize_as_json(board.lists)
lists = ::Boards::Lists::ListService.new(project, current_user).execute(board)
render json: serialize_as_json(lists)
end
def create
......
......@@ -10,10 +10,10 @@ class Projects::BranchesController < Projects::ApplicationController
def index
@sort = params[:sort].presence || sort_value_name
@branches = BranchesFinder.new(@repository, params).execute
@branches = Kaminari.paginate_array(@branches).page(params[:page])
respond_to do |format|
format.html do
paginate_branches
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
@max_commits = @branches.reduce(0) do |memo, branch|
......@@ -22,7 +22,6 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
format.json do
paginate_branches unless params[:show_all]
render json: @branches.map(&:name)
end
end
......@@ -106,10 +105,6 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
def paginate_branches
@branches = Kaminari.paginate_array(@branches).page(params[:page])
end
def url_to_autodeploy_setup(project, branch_name)
namespace_project_new_blob_path(
project.namespace,
......
......@@ -4,6 +4,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project!
before_action :authorize_update_deploy_key!, only: [:edit, :update]
layout "project_settings"
......@@ -21,7 +22,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def create
@key = DeployKey.new(deploy_key_params.merge(user: current_user))
@key = DeployKey.new(create_params.merge(user: current_user))
unless @key.valid? && @project.deploy_keys << @key
flash[:alert] = @key.errors.full_messages.join(', ').html_safe
......@@ -32,6 +33,18 @@ class Projects::DeployKeysController < Projects::ApplicationController
redirect_to_repository_settings(@project)
end
def edit
end
def update
if deploy_key.update_attributes(update_params)
flash[:notice] = 'Deploy key was successfully updated.'
redirect_to_repository_settings(@project)
else
render 'edit'
end
end
def enable
load_key
Projects::EnableDeployKeyService.new(@project, current_user, params).execute
......@@ -59,10 +72,15 @@ class Projects::DeployKeysController < Projects::ApplicationController
protected
def deploy_key_params
def deploy_key
@deploy_key ||= @project.deploy_keys.find(params[:id])
end
def create_params
params.require(:deploy_key).permit(:key, :title, :can_push)
end
<<<<<<< HEAD
def log_audit_event(key_title, options = {})
AuditEventService.new(current_user, @project, options)
.for_deploy_key(key_title).security_event
......@@ -70,5 +88,13 @@ class Projects::DeployKeysController < Projects::ApplicationController
def load_key
@key ||= current_user.accessible_deploy_keys.find(params[:id])
=======
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)
>>>>>>> ce-com/master
end
end
......@@ -104,7 +104,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
def render_missing_personal_token
render plain: "HTTP Basic: Access denied\n" \
"You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
"You must use a personal access token with 'api' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}",
status: 401
end
......
......@@ -8,7 +8,7 @@ class Projects::LabelsController < Projects::ApplicationController
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update,
:generate, :destroy, :remove_priority,
:set_priorities]
before_action :authorize_admin_group!, only: [:promote]
before_action :authorize_admin_group_labels!, only: [:promote]
respond_to :js, :html
......@@ -161,7 +161,7 @@ class Projects::LabelsController < Projects::ApplicationController
return render_404 unless can?(current_user, :admin_label, @project)
end
def authorize_admin_group!
return render_404 unless can?(current_user, :admin_group, @project.group)
def authorize_admin_group_labels!
return render_404 unless can?(current_user, :admin_label, @project.group)
end
end
......@@ -43,7 +43,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
if schedule.update(owner: current_user)
redirect_to pipeline_schedules_path(@project)
else
redirect_to pipeline_schedules_path(@project), alert: "Failed to change the owner"
redirect_to pipeline_schedules_path(@project), alert: _("Failed to change the owner")
end
end
......@@ -53,7 +53,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
else
redirect_to pipeline_schedules_path(@project),
status: 302,
alert: "Failed to remove the pipeline schedule"
alert: _("Failed to remove the pipeline schedule")
end
end
......
......@@ -36,7 +36,6 @@ class Projects::TreeController < Projects::ApplicationController
def create_dir
return render_404 unless @commit_params.values.all?
set_start_branch_to_branch_name
create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.",
success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@branch_name, @dir_name)),
failure_path: namespace_project_tree_path(@project.namespace, @project, @ref))
......
......@@ -61,8 +61,12 @@ module ButtonHelper
html: true,
placement: placement,
container: 'body',
<<<<<<< HEAD
title: _("Set a password on your account to pull or push via %{protocol}") % { protocol: protocol },
primary_url: (geo_primary_http_url_to_repo(project) if Gitlab::Geo.secondary?)
=======
title: _("Set a password on your account to pull or push via %{protocol}") % { protocol: protocol }
>>>>>>> ce-com/master
}
end
......
......@@ -8,7 +8,7 @@ module GroupsHelper
group = Group.find_by_full_path(group)
end
group.try(:avatar_url) || image_path('no_group_avatar.png')
group.try(:avatar_url) || ActionController::Base.helpers.image_path('no_group_avatar.png')
end
def group_title(group, name = nil, url = nil)
......
......@@ -138,6 +138,8 @@ module MilestonesHelper
merge_requests_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
merge_requests_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
else
merge_requests_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
end
end
......@@ -146,6 +148,8 @@ module MilestonesHelper
participants_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
participants_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
else
participants_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
end
end
......@@ -154,6 +158,8 @@ module MilestonesHelper
labels_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
labels_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
else
labels_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
end
end
end
......@@ -13,7 +13,7 @@ module NavHelper
else
"page-gutter right-sidebar-expanded"
end
elsif current_path?('builds#show')
elsif current_path?('jobs#show')
"page-gutter build-sidebar right-sidebar-expanded"
elsif current_path?('wikis#show') ||
current_path?('wikis#edit') ||
......
......@@ -90,14 +90,18 @@ module NotesHelper
end
end
def note_url(note)
def note_url(note, project = @project)
if note.noteable.is_a?(PersonalSnippet)
snippet_note_path(note.noteable, note)
else
namespace_project_note_path(@project.namespace, @project, note)
namespace_project_note_path(project.namespace, project, note)
end
end
def noteable_note_url(note)
Gitlab::UrlBuilder.build(note)
end
def form_resources
if @snippet.is_a?(PersonalSnippet)
[@note]
......
......@@ -146,7 +146,7 @@ module ProjectsHelper
end
options = options_for_select(
options,
options.invert,
selected: highest_available_option || @project.project_feature.public_send(field),
disabled: disabled_option
)
......@@ -485,9 +485,15 @@ module ProjectsHelper
def project_feature_options
{
<<<<<<< HEAD
s_('ProjectFeature|Disabled') => ProjectFeature::DISABLED,
s_('ProjectFeature|Only team members') => ProjectFeature::PRIVATE,
s_('ProjectFeature|Everyone with access') => ProjectFeature::ENABLED
=======
ProjectFeature::DISABLED => s_('ProjectFeature|Disabled'),
ProjectFeature::PRIVATE => s_('ProjectFeature|Only team members'),
ProjectFeature::ENABLED => s_('ProjectFeature|Everyone with access')
>>>>>>> ce-com/master
}
end
......
......@@ -5,7 +5,7 @@ class AwardEmoji < ActiveRecord::Base
include Participable
include GhostUser
belongs_to :awardable, polymorphic: true
belongs_to :awardable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user
validates :awardable, :user, presence: true
......
......@@ -94,6 +94,10 @@ class Blob < SimpleDelegator
end
end
def load_all_data!
super(project.repository) if project
end
def no_highlighting?
raw_size && raw_size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
end
......@@ -151,6 +155,10 @@ class Blob < SimpleDelegator
@extension ||= extname.downcase.delete('.')
end
def file_type
Gitlab::FileDetector.type_of(path)
end
def video?
UploaderHelper::VIDEO_EXT.include?(extension)
end
......@@ -176,16 +184,19 @@ class Blob < SimpleDelegator
end
def rendered_as_text?(ignore_errors: true)
simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?)
simple_viewer.is_a?(BlobViewer::Text) && (ignore_errors || simple_viewer.render_error.nil?)
end
def show_viewer_switcher?
rendered_as_text? && rich_viewer
end
def expanded?
!!@expanded
end
def expand!
simple_viewer&.expanded = true
rich_viewer&.expanded = true
@expanded = true
end
private
......
......@@ -6,15 +6,15 @@ module BlobViewer
self.loading_partial_name = 'loading'
delegate :partial_path, :loading_partial_path, :rich?, :simple?, :text?, :binary?, to: :class
delegate :partial_path, :loading_partial_path, :rich?, :simple?, :load_async?, :text?, :binary?, to: :class
attr_reader :blob
attr_accessor :expanded
delegate :project, to: :blob
def initialize(blob)
@blob = blob
@initially_binary = blob.binary?
end
def self.partial_path
......@@ -52,19 +52,15 @@ module BlobViewer
def self.can_render?(blob, verify_binary: true)
return false if verify_binary && binary? != blob.binary?
return true if extensions&.include?(blob.extension)
return true if file_types&.include?(Gitlab::FileDetector.type_of(blob.path))
return true if file_types&.include?(blob.file_type)
false
end
def load_async?
self.class.load_async? && render_error.nil?
end
def collapsed?
return @collapsed if defined?(@collapsed)
@collapsed = !expanded && collapse_limit && blob.raw_size > collapse_limit
@collapsed = !blob.expanded? && collapse_limit && blob.raw_size > collapse_limit
end
def too_large?
......@@ -73,6 +69,10 @@ module BlobViewer
@too_large = size_limit && blob.raw_size > size_limit
end
def binary_detected_after_load?
!@initially_binary && blob.binary?
end
# This method is used on the server side to check whether we can attempt to
# render the blob at all. Human-readable error messages are found in the
# `BlobHelper#blob_render_error_reason` helper.
......
......@@ -4,6 +4,5 @@ module BlobViewer
include ServerSide
self.partial_name = 'empty'
self.binary = true
end
end
......@@ -9,9 +9,7 @@ module BlobViewer
end
def prepare!
if blob.project
blob.load_all_data!(blob.project.repository)
end
blob.load_all_data!
end
def render_error
......
......@@ -6,6 +6,10 @@ class Board < ActiveRecord::Base
validates :name, :project, presence: true
def backlog_list
lists.merge(List.backlog).take
end
def closed_list
lists.merge(List.closed).take
end
......
......@@ -114,16 +114,16 @@ class Commit
#
# Usually, the commit title is the first line of the commit message.
# In case this first line is longer than 100 characters, it is cut off
# after 80 characters and ellipses (`&hellp;`) are appended.
# after 80 characters + `...`
def title
full_title.length > 100 ? full_title[0..79] << "…" : full_title
return full_title if full_title.length < 100
full_title.truncate(81, separator: ' ', omission: '…')
end
# Returns the full commits title
def full_title
return @full_title if @full_title
@full_title =
@full_title ||=
if safe_message.blank?
no_commit_message
else
......@@ -131,19 +131,14 @@ class Commit
end
end
# Returns the commits description
#
# cut off, ellipses (`&hellp;`) are prepended to the commit message.
# Returns full commit message if title is truncated (greater than 99 characters)
# otherwise returns commit message without first line
def description
title_end = safe_message.index("\n")
@description ||=
if (!title_end && safe_message.length > 100) || (title_end && title_end > 100)
"…" << safe_message[80..-1]
else
safe_message.split("\n", 2)[1].try(:chomp)
end
end
return safe_message if full_title.length >= 100
safe_message.split("\n", 2)[1].try(:chomp)
end
def description?
description.present?
end
......@@ -326,12 +321,11 @@ class Commit
end
def raw_diffs(*args)
# Uncomment when https://gitlab.com/gitlab-org/gitaly/merge_requests/170 is merged
# if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
# Gitlab::GitalyClient::Commit.new(project.repository).diff_from_parent(self, *args)
# else
raw.diffs(*args)
# end
if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
Gitlab::GitalyClient::Commit.new(project.repository).diff_from_parent(self, *args)
else
raw.diffs(*args)
end
end
def raw_deltas
......
......@@ -4,7 +4,7 @@ class Deployment < ActiveRecord::Base
belongs_to :project, required: true, validate: true
belongs_to :environment, required: true, validate: true
belongs_to :user
belongs_to :deployable, polymorphic: true
belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
validates :sha, presence: true
validates :ref, presence: true
......
......@@ -47,7 +47,7 @@ class Event < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :project
belongs_to :target, polymorphic: true
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
# For Hash only
serialize :data # rubocop:disable Cop/ActiverecordSerialize
......
class LabelLink < ActiveRecord::Base
include Importable
belongs_to :target, polymorphic: true
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :label
validates :target, presence: true, unless: :importing?
......
......@@ -2,7 +2,7 @@ class List < ActiveRecord::Base
belongs_to :board
belongs_to :label
enum list_type: { label: 1, closed: 2 }
enum list_type: { backlog: 0, label: 1, closed: 2 }
validates :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label?
......
......@@ -9,7 +9,7 @@ class Member < ActiveRecord::Base
belongs_to :created_by, class_name: "User"
belongs_to :user
belongs_to :source, polymorphic: true
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
delegate :name, :username, :email, to: :user, prefix: true
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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