Commit 1cecc285 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into add-ci_variables-environment_scope

* upstream/master: (79 commits)
  Reset @full_path to nil when cache expires
  add margin between captcha and register button
  Eagerly create a milestone that is used in a feature spec
  Adjust readme repo width
  Resolve "Issue Board -> "Remove from board" button when viewing an issue gives js error and fails"
  Fix rubocop offenses
  Make entrypoint and command keys to be array of strings
  Add issuable-list class to shared mr/issue lists to fix new responsive layout
  New navigation breadcrumbs
  Restore timeago translations in renderTimeago.
  Fix curl example paths (missing the 'files' segment)
  Automatically hide sidebar on smaller screens
  Fix typo in IssuesFinder comment
  Remove placeholder note when award emoji slash command is applied
  Make setSidebarHeight more efficient with SidebarHeightManager.
  Update CHANGELOG.md for 9.3.3
  Resolve "More actions dropdown hidden by end of diff"
  Use Gitaly 0.14.0
  Improve support for external issue references
  Make issuables_count_for_state public
  ...
parents 2c74e73f 68ac3914
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
"gon": false, "gon": false,
"localStorage": false "localStorage": false
}, },
"parser": "babel-eslint",
"plugins": [ "plugins": [
"filenames", "filenames",
"import", "import",
......
...@@ -63,7 +63,7 @@ stages: ...@@ -63,7 +63,7 @@ stages:
.only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql .only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql
only: only:
- /mysql/ - /mysql/
- /-stable$/ - /-stable/
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
- master@gitlab/gitlabhq - master@gitlab/gitlabhq
- tags@gitlab-org/gitlab-ce - tags@gitlab-org/gitlab-ce
...@@ -474,9 +474,8 @@ codeclimate: ...@@ -474,9 +474,8 @@ codeclimate:
services: services:
- docker:dind - docker:dind
script: script:
- docker pull codeclimate/codeclimate - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
- sed -i.bak 's/\({"body":"\)[^"]*\("}\)/\1\2/g' codeclimate.json
artifacts: artifacts:
paths: [codeclimate.json] paths: [codeclimate.json]
......
...@@ -2,6 +2,15 @@ ...@@ -2,6 +2,15 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 9.3.3 (2017-06-30)
- Fix head pipeline stored in merge request for external pipelines. !12478
- Bring back branches badge to main project page. !12548
- Fix diff of requirements.txt file by not matching newlines as part of package names.
- Perform housekeeping only when an import of a fresh project is completed.
- Fixed issue boards closed list not showing all closed issues.
- Fixed multi-line markdown tooltip buttons in issue edit form.
## 9.3.2 (2017-06-27) ## 9.3.2 (2017-06-27)
- API: Fix optional arugments for POST :id/variables. !12474 - API: Fix optional arugments for POST :id/variables. !12474
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
/* global Flash */ /* global Flash */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import * as Emoji from './emoji';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
...@@ -24,27 +23,9 @@ const categoryLabelMap = { ...@@ -24,27 +23,9 @@ const categoryLabelMap = {
flags: 'Flags', flags: 'Flags',
}; };
function renderCategory(name, emojiList, opts = {}) { class AwardsHandler {
return ` constructor(emoji) {
<h5 class="emoji-menu-title"> this.emoji = emoji;
${name}
</h5>
<ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
${emojiList.map(emojiName => `
<li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
${Emoji.glEmojiTag(emojiName, {
sprite: true,
})}
</button>
</li>
`).join('\n')}
</ul>
`;
}
export default class AwardsHandler {
constructor() {
this.eventListeners = []; this.eventListeners = [];
// If the user shows intent let's pre-build the menu // If the user shows intent let's pre-build the menu
this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => { this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
...@@ -78,10 +59,10 @@ export default class AwardsHandler { ...@@ -78,10 +59,10 @@ export default class AwardsHandler {
const $target = $(e.currentTarget); const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji'); const $glEmojiElement = $target.find('gl-emoji');
const $spriteIconElement = $target.find('.icon'); const $spriteIconElement = $target.find('.icon');
const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name'); const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
$target.closest('.js-awards-block').addClass('current'); $target.closest('.js-awards-block').addClass('current');
this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji); this.addAward(this.getVotesBlock(), this.getAwardUrl(), emojiName);
}); });
} }
...@@ -139,16 +120,16 @@ export default class AwardsHandler { ...@@ -139,16 +120,16 @@ export default class AwardsHandler {
this.isCreatingEmojiMenu = true; this.isCreatingEmojiMenu = true;
// Render the first category // Render the first category
const categoryMap = Emoji.getEmojiCategoryMap(); const categoryMap = this.emoji.getEmojiCategoryMap();
const categoryNameKey = Object.keys(categoryMap)[0]; const categoryNameKey = Object.keys(categoryMap)[0];
const emojisInCategory = categoryMap[categoryNameKey]; const emojisInCategory = categoryMap[categoryNameKey];
const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); const firstCategory = this.renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
// Render the frequently used // Render the frequently used
const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
let frequentlyUsedCatgegory = ''; let frequentlyUsedCatgegory = '';
if (frequentlyUsedEmojis.length > 0) { if (frequentlyUsedEmojis.length > 0) {
frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, { frequentlyUsedCatgegory = this.renderCategory('Frequently used', frequentlyUsedEmojis, {
menuListClass: 'frequent-emojis', menuListClass: 'frequent-emojis',
}); });
} }
...@@ -179,7 +160,7 @@ export default class AwardsHandler { ...@@ -179,7 +160,7 @@ export default class AwardsHandler {
} }
this.isAddingRemainingEmojiMenuCategories = true; this.isAddingRemainingEmojiMenuCategories = true;
const categoryMap = Emoji.getEmojiCategoryMap(); const categoryMap = this.emoji.getEmojiCategoryMap();
// Avoid the jank and render the remaining categories separately // Avoid the jank and render the remaining categories separately
// This will take more time, but makes UI more responsive // This will take more time, but makes UI more responsive
...@@ -191,7 +172,7 @@ export default class AwardsHandler { ...@@ -191,7 +172,7 @@ export default class AwardsHandler {
promiseChain.then(() => promiseChain.then(() =>
new Promise((resolve) => { new Promise((resolve) => {
const emojisInCategory = categoryMap[categoryNameKey]; const emojisInCategory = categoryMap[categoryNameKey];
const categoryMarkup = renderCategory( const categoryMarkup = this.renderCategory(
categoryLabelMap[categoryNameKey], categoryLabelMap[categoryNameKey],
emojisInCategory, emojisInCategory,
); );
...@@ -216,6 +197,25 @@ export default class AwardsHandler { ...@@ -216,6 +197,25 @@ export default class AwardsHandler {
}); });
} }
renderCategory(name, emojiList, opts = {}) {
return `
<h5 class="emoji-menu-title">
${name}
</h5>
<ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
${emojiList.map(emojiName => `
<li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
${this.emoji.glEmojiTag(emojiName, {
sprite: true,
})}
</button>
</li>
`).join('\n')}
</ul>
`;
}
positionMenu($menu, $addBtn) { positionMenu($menu, $addBtn) {
const position = $addBtn.data('position'); const position = $addBtn.data('position');
// The menu could potentially be off-screen or in a hidden overflow element // The menu could potentially be off-screen or in a hidden overflow element
...@@ -234,7 +234,7 @@ export default class AwardsHandler { ...@@ -234,7 +234,7 @@ export default class AwardsHandler {
} }
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
const normalizedEmoji = Emoji.normalizeEmojiName(emoji); const normalizedEmoji = this.emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => { this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
...@@ -249,7 +249,7 @@ export default class AwardsHandler { ...@@ -249,7 +249,7 @@ export default class AwardsHandler {
this.checkMutuality(votesBlock, emoji); this.checkMutuality(votesBlock, emoji);
} }
this.addEmojiToFrequentlyUsedList(emoji); this.addEmojiToFrequentlyUsedList(emoji);
const normalizedEmoji = Emoji.normalizeEmojiName(emoji); const normalizedEmoji = this.emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
if ($emojiButton.length > 0) { if ($emojiButton.length > 0) {
if (this.isActive($emojiButton)) { if (this.isActive($emojiButton)) {
...@@ -374,7 +374,7 @@ export default class AwardsHandler { ...@@ -374,7 +374,7 @@ export default class AwardsHandler {
createAwardButtonForVotesBlock(votesBlock, emojiName) { createAwardButtonForVotesBlock(votesBlock, emojiName) {
const buttonHtml = ` const buttonHtml = `
<button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom"> <button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom">
${Emoji.glEmojiTag(emojiName)} ${this.emoji.glEmojiTag(emojiName)}
<span class="award-control-text js-counter">1</span> <span class="award-control-text js-counter">1</span>
</button> </button>
`; `;
...@@ -440,7 +440,7 @@ export default class AwardsHandler { ...@@ -440,7 +440,7 @@ export default class AwardsHandler {
} }
addEmojiToFrequentlyUsedList(emoji) { addEmojiToFrequentlyUsedList(emoji) {
if (Emoji.isEmojiNameValid(emoji)) { if (this.emoji.isEmojiNameValid(emoji)) {
this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji)); this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji));
Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 }); Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 });
} }
...@@ -450,7 +450,7 @@ export default class AwardsHandler { ...@@ -450,7 +450,7 @@ export default class AwardsHandler {
return this.frequentlyUsedEmojis || (() => { return this.frequentlyUsedEmojis || (() => {
const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(',')); const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter( this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
inputName => Emoji.isEmojiNameValid(inputName), inputName => this.emoji.isEmojiNameValid(inputName),
); );
return this.frequentlyUsedEmojis; return this.frequentlyUsedEmojis;
...@@ -493,7 +493,7 @@ export default class AwardsHandler { ...@@ -493,7 +493,7 @@ export default class AwardsHandler {
} }
findMatchingEmojiElements(query) { findMatchingEmojiElements(query) {
const emojiMatches = Emoji.filterEmojiNamesByAlias(query); const emojiMatches = this.emoji.filterEmojiNamesByAlias(query);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]'); const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const $matchingElements = $emojiElements const $matchingElements = $emojiElements
.filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0); .filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0);
...@@ -507,3 +507,12 @@ export default class AwardsHandler { ...@@ -507,3 +507,12 @@ export default class AwardsHandler {
$('.emoji-menu').remove(); $('.emoji-menu').remove();
} }
} }
let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) {
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji')
.then(Emoji => new AwardsHandler(Emoji));
}
return awardsHandlerPromise;
}
import installCustomElements from 'document-register-element'; import installCustomElements from 'document-register-element';
import { emojiImageTag, emojiFallbackImageSrc } from '../emoji';
import isEmojiUnicodeSupported from '../emoji/support'; import isEmojiUnicodeSupported from '../emoji/support';
installCustomElements(window); installCustomElements(window);
...@@ -32,11 +31,19 @@ export default function installGlEmojiElement() { ...@@ -32,11 +31,19 @@ export default function installGlEmojiElement() {
// IE 11 doesn't like adding multiple at once :( // IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon'); this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass); this.classList.add(fallbackSpriteClass);
} else if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc);
} else { } else {
const src = emojiFallbackImageSrc(name); import(/* webpackChunkName: 'emoji' */ '../emoji')
this.innerHTML = emojiImageTag(name, src); .then(({ emojiImageTag, emojiFallbackImageSrc }) => {
if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc);
} else {
const src = emojiFallbackImageSrc(name);
this.innerHTML = emojiImageTag(name, src);
}
})
.catch(() => {
// do nothing
});
} }
} }
}; };
......
...@@ -34,7 +34,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -34,7 +34,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({
}, },
milestoneTitle() { milestoneTitle() {
return this.issue.milestone ? this.issue.milestone.title : 'No Milestone'; return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
} },
canRemove() {
return !this.list.preset;
},
}, },
watch: { watch: {
detail: { detail: {
......
...@@ -46,8 +46,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ ...@@ -46,8 +46,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
}, },
template: ` template: `
<div <div
class="block list" class="block list">
v-if="list.type !== 'closed'">
<button <button
class="btn btn-default btn-block" class="btn btn-default btn-block"
type="button" type="button"
......
...@@ -85,9 +85,8 @@ window.Build = (function () { ...@@ -85,9 +85,8 @@ window.Build = (function () {
if (!this.hasBeenScrolled) { if (!this.hasBeenScrolled) {
this.scrollToBottom(); this.scrollToBottom();
} }
}); })
.then(() => this.verifyTopPosition());
this.verifyTopPosition();
} }
Build.prototype.canScroll = function () { Build.prototype.canScroll = function () {
...@@ -176,7 +175,7 @@ window.Build = (function () { ...@@ -176,7 +175,7 @@ window.Build = (function () {
} }
if ($flashError.length) { if ($flashError.length) {
topPostion += $flashError.outerHeight(); topPostion += $flashError.outerHeight() + prependTopDefault;
} }
this.$buildTrace.css({ this.$buildTrace.css({
...@@ -234,7 +233,8 @@ window.Build = (function () { ...@@ -234,7 +233,8 @@ window.Build = (function () {
if (!this.hasBeenScrolled) { if (!this.hasBeenScrolled) {
this.scrollToBottom(); this.scrollToBottom();
} }
}); })
.then(() => this.verifyTopPosition());
}, 4000); }, 4000);
} else { } else {
this.$buildRefreshAnimation.remove(); this.$buildRefreshAnimation.remove();
......
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import './lib/utils/url_utility'; import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
const UNFOLD_COUNT = 20; const UNFOLD_COUNT = 20;
let isBound = false; let isBound = false;
...@@ -8,8 +9,10 @@ let isBound = false; ...@@ -8,8 +9,10 @@ let isBound = false;
class Diff { class Diff {
constructor() { constructor() {
const $diffFile = $('.files .diff-file'); const $diffFile = $('.files .diff-file');
$diffFile.singleFileDiff(); $diffFile.singleFileDiff();
$diffFile.filesCommentButton();
FilesCommentButton.init($diffFile);
$diffFile.each((index, file) => new gl.ImageFile(file)); $diffFile.each((index, file) => new gl.ImageFile(file));
......
...@@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({ ...@@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({
const notesCount = this.notesCount; const notesCount = this.notesCount;
$(this.$el).closest('.js-avatar-container') $(this.$el).closest('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0) .toggleClass('no-comment-btn', notesCount > 0)
.nextUntil('.js-avatar-container') .nextUntil('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0); .toggleClass('no-comment-btn', notesCount > 0);
}, },
toggleDiscussionsToggleState() { toggleDiscussionsToggleState() {
const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
/* global FilesCommentButton */
/* global notes */ /* global notes */
let $commentButtonTemplate; /* Developer beware! Do not add logic to showButton or hideButton
* that will force a reflow. Doing so will create a signficant performance
window.FilesCommentButton = (function() { * bottleneck for pages with large diffs. For a comprehensive list of what
var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; * causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a
*/
COMMENT_BUTTON_CLASS = '.add-diff-note';
const LINE_NUMBER_CLASS = 'diff-line-num';
LINE_HOLDER_CLASS = '.line_holder'; const UNFOLDABLE_LINE_CLASS = 'js-unfold';
const NO_COMMENT_CLASS = 'no-comment-btn';
LINE_NUMBER_CLASS = 'diff-line-num'; const EMPTY_CELL_CLASS = 'empty-cell';
const OLD_LINE_CLASS = 'old_line';
LINE_CONTENT_CLASS = 'line_content'; const LINE_COLUMN_CLASSES = `.${LINE_NUMBER_CLASS}, .line_content`;
const DIFF_CONTAINER_SELECTOR = '.files';
UNFOLDABLE_LINE_CLASS = 'js-unfold'; const DIFF_EXPANDED_CLASS = 'diff-expanded';
EMPTY_CELL_CLASS = 'empty-cell'; export default {
init($diffFile) {
OLD_LINE_CLASS = 'old_line'; /* Caching is used only when the following members are *true*. This is because there are likely to be
* differently configured versions of diffs in the same session. However if these values are true, they
LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; * will be true in all cases */
TEXT_FILE_SELECTOR = '.text-file'; if (!this.userCanCreateNote) {
// data-can-create-note is an empty string when true, otherwise undefined
function FilesCommentButton(filesContainerElement) { this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === '';
this.render = this.render.bind(this);
this.hideButton = this.hideButton.bind(this);
this.isParallelView = notes.isParallelView();
filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render)
.on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton);
}
FilesCommentButton.prototype.render = function(e) {
var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
$currentTarget = $(e.currentTarget);
if ($currentTarget.hasClass('js-no-comment-btn')) return;
lineContentElement = this.getLineContent($currentTarget);
buttonParentElement = this.getButtonParent($currentTarget);
if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return;
$button = $(COMMENT_BUTTON_CLASS, buttonParentElement);
buttonParentElement.addClass('is-over')
.nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over');
if ($button.length) {
return;
} }
textFileElement = this.getTextFileElement($currentTarget); if (typeof notes !== 'undefined' && !this.isParallelView) {
buttonParentElement.append(this.buildButton({ this.isParallelView = notes.isParallelView && notes.isParallelView();
discussionID: lineContentElement.attr('data-discussion-id'),
lineType: lineContentElement.attr('data-line-type'),
noteableType: textFileElement.attr('data-noteable-type'),
noteableID: textFileElement.attr('data-noteable-id'),
commitID: textFileElement.attr('data-commit-id'),
noteType: lineContentElement.attr('data-note-type'),
// LegacyDiffNote
lineCode: lineContentElement.attr('data-line-code'),
// DiffNote
position: lineContentElement.attr('data-position')
}));
};
FilesCommentButton.prototype.hideButton = function(e) {
var $currentTarget = $(e.currentTarget);
var buttonParentElement = this.getButtonParent($currentTarget);
buttonParentElement.removeClass('is-over')
.nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over');
};
FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
return $commentButtonTemplate.clone().attr({
'data-discussion-id': buttonAttributes.discussionID,
'data-line-type': buttonAttributes.lineType,
'data-noteable-type': buttonAttributes.noteableType,
'data-noteable-id': buttonAttributes.noteableID,
'data-commit-id': buttonAttributes.commitID,
'data-note-type': buttonAttributes.noteType,
// LegacyDiffNote
'data-line-code': buttonAttributes.lineCode,
// DiffNote
'data-position': buttonAttributes.position
});
};
FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
return hoveredElement.closest(TEXT_FILE_SELECTOR);
};
FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
return hoveredElement;
}
if (!this.isParallelView) {
return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
} else {
return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
} }
};
FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { if (this.userCanCreateNote) {
if (!this.isParallelView) { $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
if (hoveredElement.hasClass(OLD_LINE_CLASS)) { .on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e));
return hoveredElement;
}
return hoveredElement.parent().find("." + OLD_LINE_CLASS);
} else {
if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) {
return hoveredElement;
}
return $(hoveredElement).prev("." + LINE_NUMBER_CLASS);
} }
}; },
FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { showButton(isParallelView, e) {
return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS); const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView);
};
FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { if (!this.validateButtonParent(buttonParentElement)) return;
return lineContentElement.attr('data-note-type') && lineContentElement.attr('data-note-type') !== '';
};
return FilesCommentButton; buttonParentElement.classList.add('is-over');
})(); buttonParentElement.nextElementSibling.classList.add('is-over');
},
$.fn.filesCommentButton = function() { hideButton(isParallelView, e) {
$commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>'); const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView);
if (!(this && (this.parent().data('can-create-note') != null))) { buttonParentElement.classList.remove('is-over');
return; buttonParentElement.nextElementSibling.classList.remove('is-over');
} },
return this.each(function() {
if (!$.data(this, 'filesCommentButton')) { getButtonParent(hoveredElement, isParallelView) {
return $.data(this, 'filesCommentButton', new FilesCommentButton($(this))); if (isParallelView) {
if (!hoveredElement.classList.contains(LINE_NUMBER_CLASS)) {
return hoveredElement.previousElementSibling;
}
} else if (!hoveredElement.classList.contains(OLD_LINE_CLASS)) {
return hoveredElement.parentNode.querySelector(`.${OLD_LINE_CLASS}`);
} }
}); return hoveredElement;
},
validateButtonParent(buttonParentElement) {
return !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) &&
!buttonParentElement.classList.contains(UNFOLDABLE_LINE_CLASS) &&
!buttonParentElement.classList.contains(NO_COMMENT_CLASS) &&
!buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS);
},
}; };
import { validEmojiNames, glEmojiTag } from './emoji';
import glRegexp from './lib/utils/regexp'; import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache'; import AjaxCache from './lib/utils/ajax_cache';
...@@ -373,7 +372,12 @@ class GfmAutoComplete { ...@@ -373,7 +372,12 @@ class GfmAutoComplete {
if (this.cachedData[at]) { if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]); this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
this.loadData($input, at, validEmojiNames); import(/* webpackChunkName: 'emoji' */ './emoji')
.then(({ validEmojiNames, glEmojiTag }) => {
this.loadData($input, at, validEmojiNames);
GfmAutoComplete.glEmojiTag = glEmojiTag;
})
.catch(() => { this.isLoadingData[at] = false; });
} else { } else {
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true) AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
.then((data) => { .then((data) => {
...@@ -396,6 +400,13 @@ class GfmAutoComplete { ...@@ -396,6 +400,13 @@ class GfmAutoComplete {
this.cachedData = {}; this.cachedData = {};
} }
destroy() {
this.input.each((i, input) => {
const $input = $(input);
$input.atwho('destroy');
});
}
static isLoading(data) { static isLoading(data) {
let dataToInspect = data; let dataToInspect = data;
if (data && data.length > 0) { if (data && data.length > 0) {
...@@ -421,12 +432,14 @@ GfmAutoComplete.atTypeMap = { ...@@ -421,12 +432,14 @@ GfmAutoComplete.atTypeMap = {
}; };
// Emoji // Emoji
GfmAutoComplete.glEmojiTag = null;
GfmAutoComplete.Emoji = { GfmAutoComplete.Emoji = {
templateFunction(name) { templateFunction(name) {
return `<li> // glEmojiTag helper is loaded on-demand in fetchData()
${name} ${glEmojiTag(name)} if (GfmAutoComplete.glEmojiTag) {
</li> return `<li>${name} ${GfmAutoComplete.glEmojiTag(name)}</li>`;
`; }
return `<li>${name}</li>`;
}, },
}; };
// Team Members // Team Members
......
...@@ -21,6 +21,9 @@ function GLForm(form, enableGFM = false) { ...@@ -21,6 +21,9 @@ function GLForm(form, enableGFM = false) {
GLForm.prototype.destroy = function() { GLForm.prototype.destroy = function() {
// Clean form listeners // Clean form listeners
this.clearEventListeners(); this.clearEventListeners();
if (this.autoComplete) {
this.autoComplete.destroy();
}
return this.form.data('gl-form', null); return this.form.data('gl-form', null);
}; };
...@@ -33,7 +36,8 @@ GLForm.prototype.setupForm = function() { ...@@ -33,7 +36,8 @@ GLForm.prototype.setupForm = function() {
this.form.addClass('gfm-form'); this.form.addClass('gfm-form');
// remove notify commit author checkbox for non-commit notes // remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion')); gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(this.form.find('.js-gfm-input'), { this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), {
emojis: true, emojis: true,
members: this.enableGFM, members: this.enableGFM,
issues: this.enableGFM, issues: this.enableGFM,
......
import Cookies from 'js-cookie';
import _ from 'underscore'; import _ from 'underscore';
export default class GroupName { export default class GroupName {
constructor() { constructor() {
this.titleContainer = document.querySelector('.title-container'); this.titleContainer = document.querySelector('.js-title-container');
this.title = document.querySelector('.title'); this.title = this.titleContainer.querySelector('.title');
this.titleWidth = this.title.offsetWidth; this.titleWidth = this.title.offsetWidth;
this.groupTitle = document.querySelector('.group-title'); this.groupTitle = this.titleContainer.querySelector('.group-title');
this.groups = document.querySelectorAll('.group-path'); this.groups = this.titleContainer.querySelectorAll('.group-path');
this.toggle = null; this.toggle = null;
this.isHidden = false; this.isHidden = false;
this.init(); this.init();
...@@ -33,11 +33,20 @@ export default class GroupName { ...@@ -33,11 +33,20 @@ export default class GroupName {
createToggle() { createToggle() {
this.toggle = document.createElement('button'); this.toggle = document.createElement('button');
this.toggle.setAttribute('type', 'button');
this.toggle.className = 'text-expander group-name-toggle'; this.toggle.className = 'text-expander group-name-toggle';
this.toggle.setAttribute('aria-label', 'Toggle full path'); this.toggle.setAttribute('aria-label', 'Toggle full path');
this.toggle.innerHTML = '...'; if (Cookies.get('new_nav') === 'true') {
this.toggle.innerHTML = '<i class="fa fa-ellipsis-h" aria-hidden="true"></i>';
} else {
this.toggle.innerHTML = '...';
}
this.toggle.addEventListener('click', this.toggleGroups.bind(this)); this.toggle.addEventListener('click', this.toggleGroups.bind(this));
this.titleContainer.insertBefore(this.toggle, this.title); if (Cookies.get('new_nav') === 'true') {
this.title.insertBefore(this.toggle, this.groupTitle);
} else {
this.titleContainer.insertBefore(this.toggle, this.title);
}
this.toggleGroups(); this.toggleGroups();
} }
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
/* global SubscriptionSelect */ /* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import SidebarHeightManager from './sidebar_height_manager';
const HIDDEN_CLASS = 'hidden'; const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content'; const DISABLED_CONTENT_CLASS = 'disabled-content';
...@@ -56,18 +57,6 @@ export default class IssuableBulkUpdateSidebar { ...@@ -56,18 +57,6 @@ export default class IssuableBulkUpdateSidebar {
return navbarHeight + layoutNavHeight + subNavScroll; return navbarHeight + layoutNavHeight + subNavScroll;
} }
initSidebar() {
if (!this.navHeight) {
this.navHeight = this.getNavHeight();
}
if (!this.sidebarInitialized) {
$(document).off('scroll').on('scroll', _.throttle(this.setSidebarHeight, 10).bind(this));
$(window).off('resize').on('resize', _.throttle(this.setSidebarHeight, 10).bind(this));
this.sidebarInitialized = true;
}
}
setupBulkUpdateActions() { setupBulkUpdateActions() {
IssuableBulkUpdateActions.setOriginalDropdownData(); IssuableBulkUpdateActions.setOriginalDropdownData();
} }
...@@ -97,7 +86,7 @@ export default class IssuableBulkUpdateSidebar { ...@@ -97,7 +86,7 @@ export default class IssuableBulkUpdateSidebar {
this.toggleCheckboxDisplay(enable); this.toggleCheckboxDisplay(enable);
if (enable) { if (enable) {
this.initSidebar(); SidebarHeightManager.init();
} }
} }
...@@ -143,17 +132,6 @@ export default class IssuableBulkUpdateSidebar { ...@@ -143,17 +132,6 @@ export default class IssuableBulkUpdateSidebar {
this.$bulkEditSubmitBtn.enable(); this.$bulkEditSubmitBtn.enable();
} }
} }
// loosely based on method of the same name in right_sidebar.js
setSidebarHeight() {
const currentScrollDepth = window.pageYOffset || 0;
const diff = this.navHeight - currentScrollDepth;
if (diff > 0) {
this.$sidebar.outerHeight(window.innerHeight - diff);
} else {
this.$sidebar.outerHeight('100%');
}
}
static getCheckedIssueIds() { static getCheckedIssueIds() {
const $checkedIssues = $('.selected_issue:checked'); const $checkedIssues = $('.selected_issue:checked');
......
...@@ -39,6 +39,17 @@ ...@@ -39,6 +39,17 @@
runnerId() { runnerId() {
return `#${this.job.runner.id}`; return `#${this.job.runner.id}`;
}, },
renderBlock() {
return this.job.merge_request ||
this.job.duration ||
this.job.finished_data ||
this.job.erased_at ||
this.job.queued ||
this.job.runner ||
this.job.coverage ||
this.job.tags.length ||
this.job.cancel_path;
},
}, },
}; };
</script> </script>
...@@ -63,7 +74,7 @@ ...@@ -63,7 +74,7 @@
Retry Retry
</a> </a>
</div> </div>
<div class="block"> <div :class="{block : renderBlock }">
<p <p
class="build-detail-row js-job-mr" class="build-detail-row js-job-mr"
v-if="job.merge_request"> v-if="job.merge_request">
......
...@@ -112,29 +112,11 @@ window.dateFormat = dateFormat; ...@@ -112,29 +112,11 @@ window.dateFormat = dateFormat;
return timefor; return timefor;
}; };
w.gl.utils.cachedTimeagoElements = [];
w.gl.utils.renderTimeago = function($els) { w.gl.utils.renderTimeago = function($els) {
if (!$els && !w.gl.utils.cachedTimeagoElements.length) { const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render'));
} else if ($els) {
w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray());
}
w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText);
};
w.gl.utils.updateTimeagoText = function(el) {
const formattedDate = gl.utils.getTimeago().format(el.getAttribute('datetime'), lang);
if (el.textContent !== formattedDate) {
el.textContent = formattedDate;
}
};
w.gl.utils.initTimeagoTimeout = function() {
gl.utils.renderTimeago();
gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000); // timeago.js sets timeouts internally for each timeago value to be updated in real time
gl.utils.getTimeago().render(timeagoEls, lang);
}; };
w.gl.utils.getDayDifference = function(a, b) { w.gl.utils.getDayDifference = function(a, b) {
......
...@@ -70,7 +70,7 @@ import './ajax_loading_spinner'; ...@@ -70,7 +70,7 @@ import './ajax_loading_spinner';
import './api'; import './api';
import './aside'; import './aside';
import './autosave'; import './autosave';
import AwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import './breakpoints'; import './breakpoints';
import './broadcast_message'; import './broadcast_message';
import './build'; import './build';
...@@ -355,10 +355,10 @@ $(function () { ...@@ -355,10 +355,10 @@ $(function () {
$window.off('resize.app').on('resize.app', function () { $window.off('resize.app').on('resize.app', function () {
return fitSidebarForSize(); return fitSidebarForSize();
}); });
gl.awardsHandler = new AwardsHandler(); loadAwardsHandler();
new Aside(); new Aside();
gl.utils.initTimeagoTimeout(); gl.utils.renderTimeago();
$(document).trigger('init.scrolling-tabs'); $(document).trigger('init.scrolling-tabs');
}); });
...@@ -144,7 +144,9 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -144,7 +144,9 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
this.resetViewContainer(); this.resetViewContainer();
this.mountPipelinesView(); this.mountPipelinesView();
} else { } else {
this.expandView(); if (Breakpoints.get().getBreakpointSize() !== 'xs') {
this.expandView();
}
this.resetViewContainer(); this.resetViewContainer();
this.destroyPipelinesView(); this.destroyPipelinesView();
} }
......
...@@ -18,6 +18,7 @@ import 'vendor/jquery.caret'; // required by jquery.atwho ...@@ -18,6 +18,7 @@ import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho'; import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache'; import AjaxCache from '~/lib/utils/ajax_cache';
import CommentTypeToggle from './comment_type_toggle'; import CommentTypeToggle from './comment_type_toggle';
import loadAwardsHandler from './awards_handler';
import './autosave'; import './autosave';
import './dropzone_input'; import './dropzone_input';
import './task_list'; import './task_list';
...@@ -291,8 +292,13 @@ export default class Notes { ...@@ -291,8 +292,13 @@ export default class Notes {
if ('emoji_award' in noteEntity.commands_changes) { if ('emoji_award' in noteEntity.commands_changes) {
votesBlock = $('.js-awards-block').eq(0); votesBlock = $('.js-awards-block').eq(0);
gl.awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
return gl.awardsHandler.scrollToAwards(); loadAwardsHandler().then((awardsHandler) => {
awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
awardsHandler.scrollToAwards();
}).catch(() => {
// ignore
});
} }
} }
} }
...@@ -337,6 +343,10 @@ export default class Notes { ...@@ -337,6 +343,10 @@ export default class Notes {
if (!noteEntity.valid) { if (!noteEntity.valid) {
if (noteEntity.errors.commands_only) { if (noteEntity.errors.commands_only) {
if (noteEntity.commands_changes &&
Object.keys(noteEntity.commands_changes).length > 0) {
$notesList.find('.system-note.being-posted').remove();
}
this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline); this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline);
this.refresh(); this.refresh();
} }
...@@ -829,6 +839,8 @@ export default class Notes { ...@@ -829,6 +839,8 @@ export default class Notes {
*/ */
setupDiscussionNoteForm(dataHolder, form) { setupDiscussionNoteForm(dataHolder, form) {
// setup note target // setup note target
const diffFileData = dataHolder.closest('.text-file');
var discussionID = dataHolder.data('discussionId'); var discussionID = dataHolder.data('discussionId');
if (discussionID) { if (discussionID) {
...@@ -839,9 +851,10 @@ export default class Notes { ...@@ -839,9 +851,10 @@ export default class Notes {
form.attr('data-line-code', dataHolder.data('lineCode')); form.attr('data-line-code', dataHolder.data('lineCode'));
form.find('#line_type').val(dataHolder.data('lineType')); form.find('#line_type').val(dataHolder.data('lineType'));
form.find('#note_noteable_type').val(dataHolder.data('noteableType')); form.find('#note_noteable_type').val(diffFileData.data('noteableType'));
form.find('#note_noteable_id').val(dataHolder.data('noteableId')); form.find('#note_noteable_id').val(diffFileData.data('noteableId'));
form.find('#note_commit_id').val(dataHolder.data('commitId')); form.find('#note_commit_id').val(diffFileData.data('commitId'));
form.find('#note_type').val(dataHolder.data('noteType')); form.find('#note_type').val(dataHolder.data('noteType'));
// LegacyDiffNote // LegacyDiffNote
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import SidebarHeightManager from './sidebar_height_manager';
(function() { (function() {
this.Sidebar = (function() { this.Sidebar = (function() {
...@@ -8,10 +9,6 @@ import Cookies from 'js-cookie'; ...@@ -8,10 +9,6 @@ import Cookies from 'js-cookie';
this.toggleTodo = this.toggleTodo.bind(this); this.toggleTodo = this.toggleTodo.bind(this);
this.sidebar = $('aside'); this.sidebar = $('aside');
this.$sidebarInner = this.sidebar.find('.issuable-sidebar');
this.$navGitlab = $('.navbar-gitlab');
this.$rightSidebar = $('.js-right-sidebar');
this.removeListeners(); this.removeListeners();
this.addEventListeners(); this.addEventListeners();
} }
...@@ -25,16 +22,14 @@ import Cookies from 'js-cookie'; ...@@ -25,16 +22,14 @@ import Cookies from 'js-cookie';
}; };
Sidebar.prototype.addEventListeners = function() { Sidebar.prototype.addEventListeners = function() {
SidebarHeightManager.init();
const $document = $(document); const $document = $(document);
const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20);
const debouncedSetSidebarHeight = _.debounce(this.setSidebarHeight.bind(this), 200);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$(window).on('resize', () => throttledSetSidebarHeight());
$document.on('scroll', () => debouncedSetSidebarHeight());
$document.on('click', '.js-sidebar-toggle', function(e, triggered) { $document.on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon; var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault(); e.preventDefault();
...@@ -212,18 +207,6 @@ import Cookies from 'js-cookie'; ...@@ -212,18 +207,6 @@ import Cookies from 'js-cookie';
} }
}; };
Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = this.$navGitlab.outerHeight();
const diff = $navHeight - $(window).scrollTop();
if (diff > 0) {
this.$rightSidebar.outerHeight($(window).height() - diff);
this.$sidebarInner.height('100%');
} else {
this.$rightSidebar.outerHeight('100%');
this.$sidebarInner.height('');
}
};
Sidebar.prototype.isOpen = function() { Sidebar.prototype.isOpen = function() {
return this.sidebar.is('.right-sidebar-expanded'); return this.sidebar.is('.right-sidebar-expanded');
}; };
......
export default {
init() {
if (!this.initialized) {
this.$window = $(window);
this.$rightSidebar = $('.js-right-sidebar');
this.$navHeight = $('.navbar-gitlab').outerHeight() +
$('.layout-nav').outerHeight() +
$('.sub-nav-scroll').outerHeight();
const throttledSetSidebarHeight = _.throttle(() => this.setSidebarHeight(), 20);
const debouncedSetSidebarHeight = _.debounce(() => this.setSidebarHeight(), 200);
this.$window.on('scroll', throttledSetSidebarHeight);
this.$window.on('resize', debouncedSetSidebarHeight);
this.initialized = true;
}
},
setSidebarHeight() {
const currentScrollDepth = window.pageYOffset || 0;
const diff = this.$navHeight - currentScrollDepth;
if (diff > 0) {
const newSidebarHeight = window.innerHeight - diff;
this.$rightSidebar.outerHeight(newSidebarHeight);
this.sidebarHeightIsCustom = true;
} else if (this.sidebarHeightIsCustom) {
this.$rightSidebar.outerHeight('100%');
this.sidebarHeightIsCustom = false;
}
},
};
/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */ /* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
import FilesCommentButton from './files_comment_button';
(function() { (function() {
window.SingleFileDiff = (function() { window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
...@@ -78,6 +80,8 @@ ...@@ -78,6 +80,8 @@
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
} }
FilesCommentButton.init($(_this.file));
if (cb) cb(); if (cb) cb();
}; };
})(this)); })(this));
......
...@@ -64,6 +64,12 @@ ...@@ -64,6 +64,12 @@
*/ */
return new gl.GLForm($(this.$refs['gl-form']), true); return new gl.GLForm($(this.$refs['gl-form']), true);
}, },
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('gl-form');
if (glForm) {
glForm.destroy();
}
},
}; };
</script> </script>
......
/**
* This is the first script loaded by webpack's runtime. It is used to manually configure
* config.output.publicPath to account for relative_url_root or CDN settings which cannot be
* baked-in to our webpack bundles.
*/
if (gon && gon.webpack_public_path) {
__webpack_public_path__ = gon.webpack_public_path; // eslint-disable-line
}
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
max-width: $limited-layout-width-sm; max-width: $limited-layout-width-sm;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
padding-top: 64px;
padding-bottom: 64px;
} }
} }
......
...@@ -11,20 +11,19 @@ header.navbar-gitlab-new { ...@@ -11,20 +11,19 @@ header.navbar-gitlab-new {
padding-left: 0; padding-left: 0;
.title-container { .title-container {
align-items: stretch;
padding-top: 0; padding-top: 0;
overflow: visible; overflow: visible;
} }
.title { .title {
display: block; display: flex;
height: 100%;
padding-right: 0; padding-right: 0;
color: currentColor; color: currentColor;
> a { > a {
display: flex; display: flex;
align-items: center; align-items: center;
height: 100%;
padding-top: 3px; padding-top: 3px;
padding-right: $gl-padding; padding-right: $gl-padding;
padding-left: $gl-padding; padding-left: $gl-padding;
...@@ -265,3 +264,127 @@ header.navbar-gitlab-new { ...@@ -265,3 +264,127 @@ header.navbar-gitlab-new {
} }
} }
} }
.breadcrumbs {
display: flex;
min-height: 60px;
padding-top: $gl-padding-top;
padding-bottom: $gl-padding-top;
color: $gl-text-color;
border-bottom: 1px solid $border-color;
.dropdown-toggle-caret {
position: relative;
top: -1px;
padding: 0 5px;
color: rgba($black, .65);
font-size: 10px;
line-height: 1;
background: none;
border: 0;
&:focus {
outline: 0;
}
}
}
.breadcrumbs-container {
display: flex;
width: 100%;
position: relative;
.dropdown-menu-projects {
margin-top: -$gl-padding;
margin-left: $gl-padding;
}
}
.breadcrumbs-links {
flex: 1;
align-self: center;
color: $black-transparent;
a {
color: rgba($black, .65);
&:not(:first-child),
&.group-path {
margin-left: 4px;
}
&:not(:last-of-type),
&.group-path {
margin-right: 3px;
}
}
.title {
white-space: nowrap;
> a {
&:last-of-type {
font-weight: 600;
}
}
}
.avatar-tile {
margin-right: 5px;
border: 1px solid $border-color;
border-radius: 50%;
vertical-align: sub;
&.identicon {
float: left;
width: 16px;
height: 16px;
margin-top: 2px;
font-size: 10px;
}
}
.text-expander {
margin-left: 4px;
margin-right: 4px;
> i {
position: relative;
top: 1px;
}
}
}
.breadcrumbs-extra {
flex: 0 0 auto;
margin-left: auto;
}
.breadcrumbs-sub-title {
margin: 2px 0 0;
font-size: 16px;
font-weight: normal;
ul {
margin: 0;
}
li {
display: inline-block;
&:not(:last-child) {
&::after {
content: "/";
margin: 0 2px 0 5px;
}
}
&:last-child a {
font-weight: 600;
}
}
a {
color: $gl-text-color;
}
}
...@@ -49,6 +49,7 @@ $new-sidebar-width: 220px; ...@@ -49,6 +49,7 @@ $new-sidebar-width: 220px;
position: fixed; position: fixed;
z-index: 400; z-index: 400;
width: $new-sidebar-width; width: $new-sidebar-width;
transition: width $sidebar-transition-duration;
top: 50px; top: 50px;
bottom: 0; bottom: 0;
left: 0; left: 0;
...@@ -62,6 +63,8 @@ $new-sidebar-width: 220px; ...@@ -62,6 +63,8 @@ $new-sidebar-width: 220px;
} }
li { li {
white-space: nowrap;
a { a {
display: block; display: block;
padding: 12px 14px; padding: 12px 14px;
...@@ -72,6 +75,10 @@ $new-sidebar-width: 220px; ...@@ -72,6 +75,10 @@ $new-sidebar-width: 220px;
color: $gl-text-color; color: $gl-text-color;
text-decoration: none; text-decoration: none;
} }
@media (max-width: $screen-xs-max) {
width: 0;
}
} }
.sidebar-sub-level-items { .sidebar-sub-level-items {
......
...@@ -20,8 +20,6 @@ ...@@ -20,8 +20,6 @@
} }
.diff-content { .diff-content {
overflow: auto;
overflow-y: hidden;
background: $white-light; background: $white-light;
color: $gl-text-color; color: $gl-text-color;
border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px;
...@@ -476,6 +474,7 @@ ...@@ -476,6 +474,7 @@
height: 19px; height: 19px;
width: 19px; width: 19px;
margin-left: -15px; margin-left: -15px;
z-index: 100;
&:hover { &:hover {
.diff-comment-avatar, .diff-comment-avatar,
...@@ -491,7 +490,7 @@ ...@@ -491,7 +490,7 @@
transform: translateX((($i * $x-pos) - $x-pos)); transform: translateX((($i * $x-pos) - $x-pos));
&:hover { &:hover {
transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2); transform: translateX((($i * $x-pos) - $x-pos));
} }
} }
} }
...@@ -542,6 +541,7 @@ ...@@ -542,6 +541,7 @@
height: 19px; height: 19px;
padding: 0; padding: 0;
transition: transform .1s ease-out; transition: transform .1s ease-out;
z-index: 100;
svg { svg {
position: absolute; position: absolute;
...@@ -555,10 +555,6 @@ ...@@ -555,10 +555,6 @@
fill: $white-light; fill: $white-light;
} }
&:hover {
transform: scale(1.2);
}
&:focus { &:focus {
outline: 0; outline: 0;
} }
......
...@@ -597,7 +597,38 @@ ...@@ -597,7 +597,38 @@
.issue-info-container { .issue-info-container {
-webkit-flex: 1; -webkit-flex: 1;
flex: 1; flex: 1;
display: flex;
padding-right: $gl-padding; padding-right: $gl-padding;
.issue-main-info {
flex: 1 auto;
margin-right: 10px;
}
.issuable-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
flex: 1 0 auto;
.controls {
margin-bottom: 2px;
line-height: 20px;
padding: 0;
}
.issue-updated-at {
line-height: 20px;
}
}
@media(max-width: $screen-xs-max) {
.issuable-meta {
.controls li {
margin-right: 0;
}
}
}
} }
.issue-check { .issue-check {
...@@ -609,6 +640,30 @@ ...@@ -609,6 +640,30 @@
vertical-align: text-top; vertical-align: text-top;
} }
} }
.issuable-milestone,
.issuable-info,
.task-status,
.issuable-updated-at {
font-weight: normal;
color: $gl-text-color-secondary;
a {
color: $gl-text-color;
.fa {
color: $gl-text-color-secondary;
}
}
}
@media(max-width: $screen-md-max) {
.task-status,
.issuable-due-date,
.project-ref-path {
display: none;
}
}
} }
} }
......
...@@ -279,5 +279,9 @@ ...@@ -279,5 +279,9 @@
.label-link { .label-link {
display: inline-block; display: inline-block;
vertical-align: text-top; vertical-align: top;
.label {
vertical-align: inherit;
}
} }
...@@ -628,8 +628,14 @@ ul.notes { ...@@ -628,8 +628,14 @@ ul.notes {
* Line note button on the side of diffs * Line note button on the side of diffs
*/ */
.line_holder .is-over:not(.no-comment-btn) {
.add-diff-note {
opacity: 1;
}
}
.add-diff-note { .add-diff-note {
display: none; opacity: 0;
margin-top: -2px; margin-top: -2px;
border-radius: 50%; border-radius: 50%;
background: $white-light; background: $white-light;
...@@ -642,13 +648,11 @@ ul.notes { ...@@ -642,13 +648,11 @@ ul.notes {
width: 23px; width: 23px;
height: 23px; height: 23px;
border: 1px solid $blue-500; border: 1px solid $blue-500;
transition: transform .1s ease-in-out;
&:hover { &:hover {
background: $blue-500; background: $blue-500;
border-color: $blue-600; border-color: $blue-600;
color: $white-light; color: $white-light;
transform: scale(1.15);
} }
&:active { &:active {
......
...@@ -483,11 +483,12 @@ a.deploy-project-label { ...@@ -483,11 +483,12 @@ a.deploy-project-label {
.project-stats { .project-stats {
font-size: 0; font-size: 0;
text-align: center; text-align: center;
max-width: 100%;
border-bottom: 1px solid $border-color;
.nav { .nav {
padding-top: 12px; padding-top: 12px;
padding-bottom: 12px; padding-bottom: 12px;
border-bottom: 1px solid $border-color;
} }
.nav > li { .nav > li {
......
.tree-holder { .tree-holder {
.nav-block { .nav-block {
margin: 10px 0; margin: 10px 0;
...@@ -15,6 +16,11 @@ ...@@ -15,6 +16,11 @@
.btn-group { .btn-group {
margin-left: 10px; margin-left: 10px;
} }
.control {
float: left;
margin-left: 10px;
}
} }
.tree-ref-holder { .tree-ref-holder {
......
...@@ -11,6 +11,9 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -11,6 +11,9 @@ class Groups::MilestonesController < Groups::ApplicationController
@milestone_states = GlobalMilestone.states_count(@projects) @milestone_states = GlobalMilestone.states_count(@projects)
@milestones = Kaminari.paginate_array(milestones).page(params[:page]) @milestones = Kaminari.paginate_array(milestones).page(params[:page])
end end
format.json do
render json: milestones.map { |m| m.for_display.slice(:title, :name) }
end
end end
end end
......
...@@ -227,7 +227,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -227,7 +227,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue def issue
return @issue if defined?(@issue) return @issue if defined?(@issue)
# The Sortable default scope causes performance issues when used with find_by # The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.find_by!(iid: params[:id]) @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
return render_404 unless can?(current_user, :read_issue, @issue) return render_404 unless can?(current_user, :read_issue, @issue)
...@@ -267,10 +267,22 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -267,10 +267,22 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def issue_params def issue_params
params.require(:issue).permit( params.require(:issue).permit(*issue_params_attributes)
:title, :assignee_id, :position, :description, :confidential, end
:milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [], assignee_ids: []
) def issue_params_attributes
%i[
title
assignee_id
position
description
confidential
milestone_id
due_date
state_event
task_num
lock_version
] + [{ label_ids: [], assignee_ids: [] }]
end end
def authenticate_user! def authenticate_user!
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
# #
class IssuableFinder class IssuableFinder
NONE = '0'.freeze NONE = '0'.freeze
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze
attr_accessor :current_user, :params attr_accessor :current_user, :params
...@@ -62,7 +63,7 @@ class IssuableFinder ...@@ -62,7 +63,7 @@ class IssuableFinder
# grouping and counting within that query. # grouping and counting within that query.
# #
def count_by_state def count_by_state
count_params = params.merge(state: nil, sort: nil) count_params = params.merge(state: nil, sort: nil, for_counting: true)
labels_count = label_names.any? ? label_names.count : 1 labels_count = label_names.any? ? label_names.count : 1
finder = self.class.new(current_user, count_params) finder = self.class.new(current_user, count_params)
counts = Hash.new(0) counts = Hash.new(0)
...@@ -86,6 +87,10 @@ class IssuableFinder ...@@ -86,6 +87,10 @@ class IssuableFinder
execute.find_by!(*params) execute.find_by!(*params)
end end
def state_counter_cache_key(state)
Digest::SHA1.hexdigest(state_counter_cache_key_components(state).flatten.join('-'))
end
def group def group
return @group if defined?(@group) return @group if defined?(@group)
...@@ -418,4 +423,13 @@ class IssuableFinder ...@@ -418,4 +423,13 @@ class IssuableFinder
def current_user_related? def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end end
def state_counter_cache_key_components(state)
opts = params.with_indifferent_access
opts[:state] = state
opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
opts.delete_if { |_, value| value.blank? }
['issuables_count', klass.to_ability_name, opts.sort]
end
end end
...@@ -16,14 +16,72 @@ ...@@ -16,14 +16,72 @@
# sort: string # sort: string
# #
class IssuesFinder < IssuableFinder class IssuesFinder < IssuableFinder
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
def klass def klass
Issue Issue
end end
def with_confidentiality_access_check
return Issue.all if user_can_see_all_confidential_issues?
return Issue.where('issues.confidential IS NOT TRUE') if user_cannot_see_confidential_issues?
Issue.where('
issues.confidential IS NOT TRUE
OR (issues.confidential = TRUE
AND (issues.author_id = :user_id
OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id)
OR issues.project_id IN(:project_ids)))',
user_id: current_user.id,
project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id))
end
private private
def init_collection def init_collection
IssuesFinder.not_restricted_by_confidentiality(current_user) with_confidentiality_access_check
end
def user_can_see_all_confidential_issues?
return @user_can_see_all_confidential_issues if defined?(@user_can_see_all_confidential_issues)
return @user_can_see_all_confidential_issues = false if current_user.blank?
return @user_can_see_all_confidential_issues = true if current_user.full_private_access?
@user_can_see_all_confidential_issues =
project? &&
project &&
project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL
end
# Anonymous users can't see any confidential issues.
#
# Users without access to see _all_ confidential issues (as in
# `user_can_see_all_confidential_issues?`) are more complicated, because they
# can see confidential issues where:
# 1. They are an assignee.
# 2. They are an author.
#
# That's fine for most cases, but if we're just counting, we need to cache
# effectively. If we cached this accurately, we'd have a cache key for every
# authenticated user without sufficient access to the project. Instead, when
# we are counting, we treat them as if they can't see any confidential issues.
#
# This does mean the counts may be wrong for those users, but avoids an
# explosion in cache keys.
def user_cannot_see_confidential_issues?(for_counting: false)
return false if user_can_see_all_confidential_issues?
current_user.blank? || for_counting || params[:for_counting]
end
def state_counter_cache_key_components(state)
extra_components = [
user_can_see_all_confidential_issues?,
user_cannot_see_confidential_issues?(for_counting: true)
]
super + extra_components
end end
def by_assignee(items) def by_assignee(items)
...@@ -38,21 +96,6 @@ class IssuesFinder < IssuableFinder ...@@ -38,21 +96,6 @@ class IssuesFinder < IssuableFinder
end end
end end
def self.not_restricted_by_confidentiality(user)
return Issue.where('issues.confidential IS NOT TRUE') if user.blank?
return Issue.all if user.full_private_access?
Issue.where('
issues.confidential IS NOT TRUE
OR (issues.confidential = TRUE
AND (issues.author_id = :user_id
OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id)
OR issues.project_id IN(:project_ids)))',
user_id: user.id,
project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
end
def item_project_ids(items) def item_project_ids(items)
items&.reorder(nil)&.select(:project_id) items&.reorder(nil)&.select(:project_id)
end end
......
...@@ -16,11 +16,12 @@ module GroupsHelper ...@@ -16,11 +16,12 @@ module GroupsHelper
full_title = '' full_title = ''
group.ancestors.reverse.each do |parent| group.ancestors.reverse.each do |parent|
full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable') full_title += group_title_link(parent, hidable: true)
full_title += '<span class="hidable"> / </span>'.html_safe full_title += '<span class="hidable"> / </span>'.html_safe
end end
full_title += link_to(simple_sanitize(group.name), group_path(group), class: 'group-path') full_title += group_title_link(group)
full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name
content_tag :span, class: 'group-title' do content_tag :span, class: 'group-title' do
...@@ -56,4 +57,20 @@ module GroupsHelper ...@@ -56,4 +57,20 @@ module GroupsHelper
def group_issues(group) def group_issues(group)
IssuesFinder.new(current_user, group_id: group.id).execute IssuesFinder.new(current_user, group_id: group.id).execute
end end
private
def group_title_link(group, hidable: false)
link_to(group_path(group), class: "group-path #{'hidable' if hidable}") do
output =
if show_new_nav?
image_tag(group_icon(group), class: "avatar-tile", width: 16, height: 16)
else
""
end
output << simple_sanitize(group.name)
output.html_safe
end
end
end end
...@@ -165,11 +165,7 @@ module IssuablesHelper ...@@ -165,11 +165,7 @@ module IssuablesHelper
} }
state_title = titles[state] || state.to_s.humanize state_title = titles[state] || state.to_s.humanize
count = issuables_count_for_state(issuable_type, state)
count =
Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do
issuables_count_for_state(issuable_type, state)
end
html = content_tag(:span, state_title) html = content_tag(:span, state_title)
html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge') html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge')
...@@ -237,6 +233,18 @@ module IssuablesHelper ...@@ -237,6 +233,18 @@ module IssuablesHelper
} }
end end
def issuables_count_for_state(issuable_type, state, finder: nil)
finder ||= public_send("#{issuable_type}_finder")
cache_key = finder.state_counter_cache_key(state)
@counts ||= {}
@counts[cache_key] ||= Rails.cache.fetch(cache_key, expires_in: 2.minutes) do
finder.count_by_state
end
@counts[cache_key][state]
end
private private
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?
...@@ -255,24 +263,6 @@ module IssuablesHelper ...@@ -255,24 +263,6 @@ module IssuablesHelper
end end
end end
def issuables_count_for_state(issuable_type, state)
@counts ||= {}
@counts[issuable_type] ||= public_send("#{issuable_type}_finder").count_by_state
@counts[issuable_type][state]
end
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze
private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY
def issuables_state_counter_cache_key(issuable_type, state)
opts = params.with_indifferent_access
opts[:state] = state
opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
opts.delete_if { |_, value| value.blank? }
hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-'))
end
def issuable_templates(issuable) def issuable_templates(issuable)
@issuable_templates ||= @issuable_templates ||=
case issuable case issuable
......
...@@ -74,6 +74,8 @@ module MilestonesHelper ...@@ -74,6 +74,8 @@ module MilestonesHelper
project = @target_project || @project project = @target_project || @project
if project if project
namespace_project_milestones_path(project.namespace, project, :json) namespace_project_milestones_path(project.namespace, project, :json)
elsif @group
group_milestones_path(@group, :json)
else else
dashboard_milestones_path(:json) dashboard_milestones_path(:json)
end end
......
...@@ -47,6 +47,18 @@ module NotesHelper ...@@ -47,6 +47,18 @@ module NotesHelper
data data
end end
def add_diff_note_button(line_code, position, line_type)
return if @diff_notes_disabled
button_tag '',
class: 'add-diff-note js-add-diff-note-button',
type: 'submit', name: 'button',
data: diff_view_line_data(line_code, position, line_type),
title: 'Add a comment to this line' do
icon('comment-o')
end
end
def link_to_reply_discussion(discussion, line_type = nil) def link_to_reply_discussion(discussion, line_type = nil)
return unless current_user return unless current_user
......
...@@ -58,7 +58,17 @@ module ProjectsHelper ...@@ -58,7 +58,17 @@ module ProjectsHelper
link_to(simple_sanitize(owner.name), user_path(owner)) link_to(simple_sanitize(owner.name), user_path(owner))
end end
project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } project_link = link_to project_path(project), { class: "project-item-select-holder" } do
output =
if show_new_nav?
project_icon(project, alt: project.name, class: 'avatar-tile', width: 16, height: 16)
else
""
end
output << simple_sanitize(project.name)
output.html_safe
end
if current_user if current_user
project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do
......
...@@ -11,20 +11,29 @@ module WebpackHelper ...@@ -11,20 +11,29 @@ module WebpackHelper
paths = Webpack::Rails::Manifest.asset_paths(source) paths = Webpack::Rails::Manifest.asset_paths(source)
if extension if extension
paths = paths.select { |p| p.ends_with? ".#{extension}" } paths.select! { |p| p.ends_with? ".#{extension}" }
end end
# include full webpack-dev-server url for rspec tests running locally force_host = webpack_public_host
if force_host
paths.map! { |p| "#{force_host}#{p}" }
end
paths
end
def webpack_public_host
if Rails.env.test? && Rails.configuration.webpack.dev_server.enabled if Rails.env.test? && Rails.configuration.webpack.dev_server.enabled
host = Rails.configuration.webpack.dev_server.host host = Rails.configuration.webpack.dev_server.host
port = Rails.configuration.webpack.dev_server.port port = Rails.configuration.webpack.dev_server.port
protocol = Rails.configuration.webpack.dev_server.https ? 'https' : 'http' protocol = Rails.configuration.webpack.dev_server.https ? 'https' : 'http'
"#{protocol}://#{host}:#{port}"
paths.map! do |p| else
"#{protocol}://#{host}:#{port}#{p}" ActionController::Base.asset_host.try(:chomp, '/')
end
end end
end
paths def webpack_public_path
"#{webpack_public_host}/#{Rails.application.config.webpack.public_path}/"
end end
end end
require 'declarative_policy' require_dependency 'declarative_policy'
class Ability class Ability
class << self class << self
......
module FeatureGate
def flipper_id
return nil if new_record?
"#{self.class.name}:#{id}"
end
end
...@@ -14,7 +14,7 @@ module Mentionable ...@@ -14,7 +14,7 @@ module Mentionable
end end
EXTERNAL_PATTERN = begin EXTERNAL_PATTERN = begin
issue_pattern = ExternalIssue.reference_pattern issue_pattern = IssueTrackerService.reference_pattern
link_patterns = URI.regexp(%w(http https)) link_patterns = URI.regexp(%w(http https))
reference_pattern(link_patterns, issue_pattern) reference_pattern(link_patterns, issue_pattern)
end end
......
...@@ -103,8 +103,12 @@ module Routable ...@@ -103,8 +103,12 @@ module Routable
def full_path def full_path
return uncached_full_path unless RequestStore.active? return uncached_full_path unless RequestStore.active?
key = "routable/full_path/#{self.class.name}/#{self.id}" RequestStore[full_path_key] ||= uncached_full_path
RequestStore[key] ||= uncached_full_path end
def expires_full_path_cache
RequestStore.delete(full_path_key) if RequestStore.active?
@full_path = nil
end end
def build_full_path def build_full_path
...@@ -135,6 +139,10 @@ module Routable ...@@ -135,6 +139,10 @@ module Routable
path_changed? || parent_changed? path_changed? || parent_changed?
end end
def full_path_key
@full_path_key ||= "routable/full_path/#{self.class.name}/#{self.id}"
end
def build_full_name def build_full_name
if parent && name if parent && name
parent.human_name + ' / ' + name parent.human_name + ' / ' + name
......
module ShaAttribute
extend ActiveSupport::Concern
module ClassMethods
def sha_attribute(name)
column = columns.find { |c| c.name == name.to_s }
# In case the table doesn't exist we won't be able to find the column,
# thus we will only check the type if the column is present.
if column && column.type != :binary
raise ArgumentError,
"sha_attribute #{name.inspect} is invalid since the column type is not :binary"
end
attribute(name, Gitlab::Database::ShaAttribute.new)
end
end
end
...@@ -5,25 +5,6 @@ ...@@ -5,25 +5,6 @@
module Sortable module Sortable
extend ActiveSupport::Concern extend ActiveSupport::Concern
module DropDefaultScopeOnFinders
# Override these methods to drop the `ORDER BY id DESC` default scope.
# See http://dba.stackexchange.com/a/110919 for why we do this.
%i[find find_by find_by!].each do |meth|
define_method meth do |*args, &block|
return super(*args, &block) if block
unordered_relation = unscope(:order)
# We cannot simply call `meth` on `unscope(:order)`, since that is also
# an instance of the same relation class this module is included into,
# which means we'd get infinite recursion.
# We explicitly use the original implementation to prevent this.
original_impl = method(__method__).super_method.unbind
original_impl.bind(unordered_relation).call(*args)
end
end
end
included do included do
# By default all models should be ordered # By default all models should be ordered
# by created_at field starting from newest # by created_at field starting from newest
...@@ -37,10 +18,6 @@ module Sortable ...@@ -37,10 +18,6 @@ module Sortable
scope :order_updated_asc, -> { reorder(updated_at: :asc) } scope :order_updated_asc, -> { reorder(updated_at: :asc) }
scope :order_name_asc, -> { reorder(name: :asc) } scope :order_name_asc, -> { reorder(name: :asc) }
scope :order_name_desc, -> { reorder(name: :desc) } scope :order_name_desc, -> { reorder(name: :desc) }
# All queries (relations) on this model are instances of this `relation_klass`.
relation_klass = relation_delegate_class(ActiveRecord::Relation)
relation_klass.prepend DropDefaultScopeOnFinders
end end
module ClassMethods module ClassMethods
......
...@@ -38,11 +38,6 @@ class ExternalIssue ...@@ -38,11 +38,6 @@ class ExternalIssue
@project.id @project.id
end end
# Pattern used to extract `JIRA-123` issue references from text
def self.reference_pattern
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
def to_reference(_from_project = nil, full: nil) def to_reference(_from_project = nil, full: nil)
id id
end end
......
class Namespace < ActiveRecord::Base class Namespace < ActiveRecord::Base
acts_as_paranoid acts_as_paranoid without_default_scope: true
include CacheMarkdownField include CacheMarkdownField
include Sortable include Sortable
...@@ -47,8 +47,6 @@ class Namespace < ActiveRecord::Base ...@@ -47,8 +47,6 @@ class Namespace < ActiveRecord::Base
before_destroy(prepend: true) { prepare_for_destroy } before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir after_destroy :rm_dir
default_scope { with_deleted }
scope :for_user, -> { where('type IS NULL') } scope :for_user, -> { where('type IS NULL') }
scope :with_statistics, -> do scope :with_statistics, -> do
...@@ -221,6 +219,12 @@ class Namespace < ActiveRecord::Base ...@@ -221,6 +219,12 @@ class Namespace < ActiveRecord::Base
parent.present? parent.present?
end end
def soft_delete_without_removing_associations
# We can't use paranoia's `#destroy` since this will hard-delete projects.
# Project uses `pending_delete` instead of the acts_as_paranoia gem.
self.deleted_at = Time.now
end
private private
def repository_storage_paths def repository_storage_paths
......
...@@ -727,8 +727,8 @@ class Project < ActiveRecord::Base ...@@ -727,8 +727,8 @@ class Project < ActiveRecord::Base
end end
end end
def issue_reference_pattern def external_issue_reference_pattern
issues_tracker.reference_pattern external_issue_tracker.class.reference_pattern
end end
def default_issues_tracker? def default_issues_tracker?
...@@ -815,7 +815,7 @@ class Project < ActiveRecord::Base ...@@ -815,7 +815,7 @@ class Project < ActiveRecord::Base
end end
def ci_service def ci_service
@ci_service ||= ci_services.find_by(active: true) @ci_service ||= ci_services.reorder(nil).find_by(active: true)
end end
def deployment_services def deployment_services
...@@ -823,7 +823,7 @@ class Project < ActiveRecord::Base ...@@ -823,7 +823,7 @@ class Project < ActiveRecord::Base
end end
def deployment_service def deployment_service
@deployment_service ||= deployment_services.find_by(active: true) @deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
end end
def monitoring_services def monitoring_services
...@@ -831,7 +831,7 @@ class Project < ActiveRecord::Base ...@@ -831,7 +831,7 @@ class Project < ActiveRecord::Base
end end
def monitoring_service def monitoring_service
@monitoring_service ||= monitoring_services.find_by(active: true) @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true)
end end
def jira_tracker? def jira_tracker?
...@@ -963,6 +963,7 @@ class Project < ActiveRecord::Base ...@@ -963,6 +963,7 @@ class Project < ActiveRecord::Base
begin begin
gitlab_shell.mv_repository(repository_storage_path, "#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki") gitlab_shell.mv_repository(repository_storage_path, "#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
send_move_instructions(old_path_with_namespace) send_move_instructions(old_path_with_namespace)
expires_full_path_cache
@old_path_with_namespace = old_path_with_namespace @old_path_with_namespace = old_path_with_namespace
......
...@@ -5,7 +5,10 @@ class IssueTrackerService < Service ...@@ -5,7 +5,10 @@ class IssueTrackerService < Service
# Pattern used to extract links from comments # Pattern used to extract links from comments
# Override this method on services that uses different patterns # Override this method on services that uses different patterns
def reference_pattern # This pattern does not support cross-project references
# The other code assumes that this pattern is a superset of all
# overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN
def self.reference_pattern
@reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)} @reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)}
end end
......
...@@ -18,7 +18,7 @@ class JiraService < IssueTrackerService ...@@ -18,7 +18,7 @@ class JiraService < IssueTrackerService
end end
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def reference_pattern def self.reference_pattern
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end end
......
...@@ -11,6 +11,7 @@ class User < ActiveRecord::Base ...@@ -11,6 +11,7 @@ class User < ActiveRecord::Base
include CaseSensitivity include CaseSensitivity
include TokenAuthenticatable include TokenAuthenticatable
include IgnorableColumn include IgnorableColumn
include FeatureGate
DEFAULT_NOTIFICATION_LEVEL = :participating DEFAULT_NOTIFICATION_LEVEL = :participating
......
require 'declarative_policy' require_dependency 'declarative_policy'
class BasePolicy < DeclarativePolicy::Base class BasePolicy < DeclarativePolicy::Base
desc "User is an instance admin" desc "User is an instance admin"
......
...@@ -3,8 +3,8 @@ class GitHooksService ...@@ -3,8 +3,8 @@ class GitHooksService
attr_accessor :oldrev, :newrev, :ref attr_accessor :oldrev, :newrev, :ref
def execute(user, repo_path, oldrev, newrev, ref) def execute(user, project, oldrev, newrev, ref)
@repo_path = repo_path @project = project
@user = Gitlab::GlId.gl_id(user) @user = Gitlab::GlId.gl_id(user)
@oldrev = oldrev @oldrev = oldrev
@newrev = newrev @newrev = newrev
...@@ -26,7 +26,7 @@ class GitHooksService ...@@ -26,7 +26,7 @@ class GitHooksService
private private
def run_hook(name) def run_hook(name)
hook = Gitlab::Git::Hook.new(name, @repo_path) hook = Gitlab::Git::Hook.new(name, @project)
hook.trigger(@user, oldrev, newrev, ref) hook.trigger(@user, oldrev, newrev, ref)
end end
end end
...@@ -120,7 +120,7 @@ class GitOperationService ...@@ -120,7 +120,7 @@ class GitOperationService
def with_hooks(ref, newrev, oldrev) def with_hooks(ref, newrev, oldrev)
GitHooksService.new.execute( GitHooksService.new.execute(
user, user,
repository.path_to_repo, repository.project,
oldrev, oldrev,
newrev, newrev,
ref) do |service| ref) do |service|
......
module Groups module Groups
class DestroyService < Groups::BaseService class DestroyService < Groups::BaseService
def async_execute def async_execute
# Soft delete via paranoia gem group.soft_delete_without_removing_associations
group.destroy
job_id = GroupDestroyWorker.perform_async(group.id, current_user.id) job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
end end
......
...@@ -78,6 +78,7 @@ module Projects ...@@ -78,6 +78,7 @@ module Projects
Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
project.old_path_with_namespace = @old_path project.old_path_with_namespace = @old_path
project.expires_full_path_cache
execute_system_hooks execute_system_hooks
end end
......
...@@ -93,10 +93,11 @@ module Projects ...@@ -93,10 +93,11 @@ module Projects
end end
# Requires UnZip at least 6.00 Info-ZIP. # Requires UnZip at least 6.00 Info-ZIP.
# -qq be (very) quiet
# -n never overwrite existing files # -n never overwrite existing files
# We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories
site_path = File.join(SITE_PATH, '*') site_path = File.join(SITE_PATH, '*')
unless system(*%W(unzip -n #{artifacts} #{site_path} -d #{temp_path})) unless system(*%W(unzip -qq -n #{artifacts} #{site_path} -d #{temp_path}))
raise 'pages failed to extract' raise 'pages failed to extract'
end end
end end
......
- @hide_top_links = true
- @no_container = true - @no_container = true
= content_for :meta_tags do = content_for :meta_tags do
......
- @hide_top_links = true
- page_title "Groups" - page_title "Groups"
- header_title "Groups", dashboard_groups_path - header_title "Groups", dashboard_groups_path
= render 'dashboard/groups_head' = render 'dashboard/groups_head'
......
- @hide_top_links = true
- page_title 'Milestones' - page_title 'Milestones'
- header_title 'Milestones', dashboard_milestones_path - header_title 'Milestones', dashboard_milestones_path
......
- @no_container = true - @no_container = true
- @hide_top_links = true
- @breadcrumb_title = "Projects"
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity") = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
......
- @hide_top_links = true
- page_title "Snippets" - page_title "Snippets"
- header_title "Snippets", dashboard_snippets_path - header_title "Snippets", dashboard_snippets_path
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
%div %div
- if Gitlab::Recaptcha.enabled? - if Gitlab::Recaptcha.enabled?
= recaptcha_tags = recaptcha_tags
%div .submit-container
= f.submit "Register", class: "btn-register btn" = f.submit "Register", class: "btn-register btn"
.clearfix.submit-container .clearfix.submit-container
%p %p
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
= Gon::Base.render_data = Gon::Base.render_data
= webpack_bundle_tag "runtime" = webpack_bundle_tag "webpack_runtime"
= webpack_bundle_tag "common" = webpack_bundle_tag "common"
= webpack_bundle_tag "locale" = webpack_bundle_tag "locale"
= webpack_bundle_tag "main" = webpack_bundle_tag "main"
......
...@@ -14,6 +14,8 @@ ...@@ -14,6 +14,8 @@
= render "layouts/broadcast" = render "layouts/broadcast"
= render "layouts/flash" = render "layouts/flash"
= yield :flash_message = yield :flash_message
- if show_new_nav?
= render "layouts/nav/breadcrumbs"
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" } %div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
.content{ id: "content-body" } .content{ id: "content-body" }
= yield = yield
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
= link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
= brand_header_logo = brand_header_logo
.title-container .title-container.js-title-container
%h1.title{ class: ('initializing' if @has_group_title) }= title %h1.title{ class: ('initializing' if @has_group_title) }= title
.navbar-collapse.collapse .navbar-collapse.collapse
......
...@@ -83,8 +83,6 @@ ...@@ -83,8 +83,6 @@
= icon('ellipsis-v', class: 'js-navbar-toggle-right') = icon('ellipsis-v', class: 'js-navbar-toggle-right')
= icon('times', class: 'js-navbar-toggle-left', style: 'display: none;') = icon('times', class: 'js-navbar-toggle-left', style: 'display: none;')
= yield :header_content
= render 'shared/outdated_browser' = render 'shared/outdated_browser'
- if @project && !@project.empty_repo? - if @project && !@project.empty_repo?
......
- breadcrumb_title = @breadcrumb_title || controller.controller_name.humanize
- hide_top_links = @hide_top_links || false
%nav.breadcrumbs{ role: "navigation" }
.breadcrumbs-container{ class: container_class }
.breadcrumbs-links.js-title-container
- unless hide_top_links
.title
= link_to "GitLab", root_path
\/
= header_title
%h2.breadcrumbs-sub-title
%ul.list-unstyled
- if content_for?(:sub_title_before)
= yield :sub_title_before
%li= link_to breadcrumb_title, request.path
- if content_for?(:breadcrumbs_extra)
.breadcrumbs-extra.hidden-xs= yield :breadcrumbs_extra
= yield :header_content
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
%span %span
Issues Issues
- if @project.default_issues_tracker? - if @project.default_issues_tracker?
%span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) %span.badge.count.issue_counter= number_with_delimiter(issuables_count_for_state(:issues, :opened, finder: IssuesFinder.new(current_user, project_id: @project.id)))
- if project_nav_tab? :merge_requests - if project_nav_tab? :merge_requests
- controllers = [:merge_requests, 'projects/merge_requests/conflicts'] - controllers = [:merge_requests, 'projects/merge_requests/conflicts']
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span %span
Merge Requests Merge Requests
%span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(issuables_count_for_state(:merge_requests, :opened, finder: MergeRequestsFinder.new(current_user, project_id: @project.id)))
- if project_nav_tab? :pipelines - if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do = nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
- @content_class = "issue-boards-content" - @content_class = "issue-boards-content"
- page_title "Boards" - page_title "Boards"
- if show_new_nav?
- content_for :sub_title_before do
%li= link_to "Issues", namespace_project_issues_path(@project.namespace, @project)
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search' = webpack_bundle_tag 'filtered_search'
......
...@@ -23,4 +23,5 @@ ...@@ -23,4 +23,5 @@
= render "projects/boards/components/sidebar/labels" = render "projects/boards/components/sidebar/labels"
= render "projects/boards/components/sidebar/notifications" = render "projects/boards/components/sidebar/notifications"
%remove-btn{ ":issue" => "issue", %remove-btn{ ":issue" => "issue",
":list" => "list" } ":list" => "list",
"v-if" => "canRemove" }
...@@ -8,27 +8,28 @@ ...@@ -8,27 +8,28 @@
= render "head" = render "head"
%div{ class: container_class } %div{ class: container_class }
.row-content-block.second-block.content-component-block.flex-container-block .tree-holder
.tree-ref-holder .nav-block
= render 'shared/ref_switcher', destination: 'commits' .tree-ref-container
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits'
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
.tree-controls.hidden-xs.hidden-sm
- if @merge_request.present?
.control
= link_to _("View open merge request"), namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn'
- elsif create_mr_button?(@repository.root_ref, @ref)
.control
= link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
.block-controls.hidden-xs.hidden-sm
- if @merge_request.present?
.control .control
= link_to _("View open merge request"), namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do
- elsif create_mr_button?(@repository.root_ref, @ref) = search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
.control .control
= link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' = link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do
= icon("rss")
.control
= form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do
= search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
.control
= link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do
= icon("rss")
%div{ id: dom_id(@project) } %div{ id: dom_id(@project) }
%ol#commits-list.list-unstyled.content_list %ol#commits-list.list-unstyled.content_list
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
- if plain - if plain
= link_text = link_text
- else - else
= add_diff_note_button(line_code, diff_file.position(line), type)
%a{ href: "##{line_code}", data: { linenumber: link_text } } %a{ href: "##{line_code}", data: { linenumber: link_text } }
- discussion = line_discussions.try(:first) - discussion = line_discussions.try(:first)
- if discussion && discussion.resolvable? && !plain - if discussion && discussion.resolvable? && !plain
...@@ -29,7 +30,7 @@ ...@@ -29,7 +30,7 @@
= link_text = link_text
- else - else
%a{ href: "##{line_code}", data: { linenumber: link_text } } %a{ href: "##{line_code}", data: { linenumber: link_text } }
%td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }< %td.line_content.noteable_line{ class: type }<
- if email - if email
%pre= line.text %pre= line.text
- else - else
......
/ Side-by-side diff view / Side-by-side diff view
.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data } .text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data }
%table %table
- diff_file.parallel_diff_lines.each do |line| - diff_file.parallel_diff_lines.each do |line|
...@@ -18,11 +19,12 @@ ...@@ -18,11 +19,12 @@
- left_line_code = diff_file.line_code(left) - left_line_code = diff_file.line_code(left)
- left_position = diff_file.position(left) - left_position = diff_file.position(left)
%td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } %td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } }
= add_diff_note_button(left_line_code, left_position, 'old')
%a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } } %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } }
- discussion_left = discussions_left.try(:first) - discussion_left = discussions_left.try(:first)
- if discussion_left && discussion_left.resolvable? - if discussion_left && discussion_left.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_left.id } %diff-note-avatars{ "discussion-id" => discussion_left.id }
%td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text) %td.line_content.parallel.noteable_line{ class: left.type }= diff_line_content(left.text)
- else - else
%td.old_line.diff-line-num.empty-cell %td.old_line.diff-line-num.empty-cell
%td.line_content.parallel %td.line_content.parallel
...@@ -38,11 +40,12 @@ ...@@ -38,11 +40,12 @@
- right_line_code = diff_file.line_code(right) - right_line_code = diff_file.line_code(right)
- right_position = diff_file.position(right) - right_position = diff_file.position(right)
%td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } %td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } }
= add_diff_note_button(right_line_code, right_position, 'new')
%a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } } %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } }
- discussion_right = discussions_right.try(:first) - discussion_right = discussions_right.try(:first)
- if discussion_right && discussion_right.resolvable? - if discussion_right && discussion_right.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_right.id } %diff-note-avatars{ "discussion-id" => discussion_right.id }
%td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text) %td.line_content.parallel.noteable_line{ class: right.type }= diff_line_content(right.text)
- else - else
%td.old_line.diff-line-num.empty-cell %td.old_line.diff-line-num.empty-cell
%td.line_content.parallel %td.line_content.parallel
......
...@@ -4,43 +4,49 @@ ...@@ -4,43 +4,49 @@
.issue-check.hidden .issue-check.hidden
= check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
.issue-info-container .issue-info-container
.issue-title.title .issue-main-info
%span.issue-title-text .issue-title.title
= confidential_icon(issue) %span.issue-title-text
= link_to issue.title, issue_path(issue) = confidential_icon(issue)
= link_to issue.title, issue_path(issue)
- if issue.tasks?
%span.task-status.hidden-xs
&nbsp;
= issue.task_status
.issuable-info
%span.issuable-reference
#{issuable_reference(issue)}
%span.issuable-authored.hidden-xs
&middot;
opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')}
by #{link_to_member(@project, issue.author, avatar: false)}
- if issue.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
= link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do
= icon('clock-o')
= issue.milestone.title
- if issue.due_date
%span.issuable-due-date.hidden-xs{ class: "#{'cred' if issue.overdue?}" }
&nbsp;
= icon('calendar')
= issue.due_date.to_s(:medium)
- if issue.labels.any?
&nbsp;
- issue.labels.each do |label|
= link_to_label(label, subject: issue.project, css_class: 'label-link')
.issuable-meta
%ul.controls %ul.controls
- if issue.closed? - if issue.closed?
%li %li.issuable-status
CLOSED CLOSED
- if issue.assignees.any? - if issue.assignees.any?
%li %li
= render 'shared/issuable/assignees', project: @project, issue: issue = render 'shared/issuable/assignees', project: @project, issue: issue
= render 'shared/issuable_meta_data', issuable: issue = render 'shared/issuable_meta_data', issuable: issue
.issue-info .pull-right.issuable-updated-at.hidden-xs
#{issuable_reference(issue)} &middot;
opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')}
by #{link_to_member(@project, issue.author, avatar: false)}
- if issue.milestone
&nbsp;
= link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do
= icon('clock-o')
= issue.milestone.title
- if issue.due_date
%span{ class: "#{'cred' if issue.overdue?}" }
&nbsp;
= icon('calendar')
= issue.due_date.to_s(:medium)
- if issue.labels.any?
&nbsp;
- issue.labels.each do |label|
= link_to_label(label, subject: issue.project, css_class: 'label-link')
- if issue.tasks?
&nbsp;
%span.task-status
= issue.task_status
.pull-right.issue-updated-at
%span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')} %span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss')
- if @can_bulk_update
= button_tag "Edit Issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
= link_to "New issue", new_namespace_project_issue_path(@project.namespace,
@project,
issue: { assignee_id: issues_finder.assignee.try(:id),
milestone_id: issues_finder.milestones.first.try(:id) }),
class: "btn btn-new",
title: "New issue",
id: "new_issue_link"
...@@ -13,23 +13,16 @@ ...@@ -13,23 +13,16 @@
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues") = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
- if show_new_nav?
- content_for :breadcrumbs_extra do
= render "projects/issues/nav_btns"
- if project_issues(@project).exists? - if project_issues(@project).exists?
%div{ class: (container_class) } %div{ class: (container_class) }
.top-area .top-area
= render 'shared/issuable/nav', type: :issues = render 'shared/issuable/nav', type: :issues
.nav-controls .nav-controls{ class: ("visible-xs" if show_new_nav?) }
= link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do = render "projects/issues/nav_btns"
= icon('rss')
- if @can_bulk_update
= button_tag "Edit Issues", class: "btn btn-default js-bulk-update-toggle"
= link_to new_namespace_project_issue_path(@project.namespace,
@project,
issue: { assignee_id: issues_finder.assignee.try(:id),
milestone_id: issues_finder.milestones.first.try(:id) }),
class: "btn btn-new",
title: "New issue",
id: "new_issue_link" do
New issue
= render 'shared/issuable/search_bar', type: :issues = render 'shared/issuable/search_bar', type: :issues
- if @can_bulk_update - if @can_bulk_update
......
...@@ -4,58 +4,60 @@ ...@@ -4,58 +4,60 @@
= check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue" = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
.issue-info-container .issue-info-container
.merge-request-title.title .issue-main-info
%span.merge-request-title-text .merge-request-title.title
= link_to merge_request.title, merge_request_path(merge_request) %span.merge-request-title-text
= link_to merge_request.title, merge_request_path(merge_request)
- if merge_request.tasks?
%span.task-status.hidden-xs
&nbsp;
= merge_request.task_status
.issuable-info
%span.issuable-reference
#{issuable_reference(merge_request)}
%span.issuable-authored.hidden-xs
&middot;
opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')}
by #{link_to_member(@project, merge_request.author, avatar: false)}
- if merge_request.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
= link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do
= icon('clock-o')
= merge_request.milestone.title
- if merge_request.target_project.default_branch != merge_request.target_branch
%span.project-ref-path
&nbsp;
= link_to project_ref_path(merge_request.project, merge_request.target_branch), class: 'ref-name' do
= icon('code-fork')
= merge_request.target_branch
- if merge_request.labels.any?
&nbsp;
- merge_request.labels.each do |label|
= link_to_label(label, subject: merge_request.project, type: :merge_request, css_class: 'label-link')
.issuable-meta
%ul.controls %ul.controls
- if merge_request.merged? - if merge_request.merged?
%li %li.issuable-status.hidden-xs
MERGED MERGED
- elsif merge_request.closed? - elsif merge_request.closed?
%li %li.issuable-status.hidden-xs
= icon('ban') = icon('ban')
CLOSED CLOSED
- if merge_request.head_pipeline - if merge_request.head_pipeline
%li %li.issuable-pipeline-status.hidden-xs
= render_pipeline_status(merge_request.head_pipeline) = render_pipeline_status(merge_request.head_pipeline)
- if merge_request.open? && merge_request.broken? - if merge_request.open? && merge_request.broken?
%li %li.issuable-pipeline-broken.hidden-xs
= link_to merge_request_path(merge_request), class: "has-tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do = link_to merge_request_path(merge_request), class: "has-tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do
= icon('exclamation-triangle') = icon('exclamation-triangle')
- if merge_request.assignee - if merge_request.assignee
%li %li
= link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name") = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name")
= render 'shared/issuable_meta_data', issuable: merge_request = render 'shared/issuable_meta_data', issuable: merge_request
.merge-request-info .pull-right.issuable-updated-at.hidden-xs
#{issuable_reference(merge_request)} &middot;
opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')}
by #{link_to_member(@project, merge_request.author, avatar: false)}
- if merge_request.target_project.default_branch != merge_request.target_branch
&nbsp;
= link_to project_ref_path(merge_request.project, merge_request.target_branch), class: 'ref-name' do
= icon('code-fork')
= merge_request.target_branch
- if merge_request.milestone
&nbsp;
= link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do
= icon('clock-o')
= merge_request.milestone.title
- if merge_request.labels.any?
&nbsp;
- merge_request.labels.each do |label|
= link_to_label(label, subject: merge_request.project, type: :merge_request, css_class: 'label-link')
- if merge_request.tasks?
&nbsp;
%span.task-status
= merge_request.task_status
.pull-right.hidden-xs
%span updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')} %span updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')}
- if @can_bulk_update
= button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle"
- if merge_project
= link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do
New merge request
- @no_container = true - @no_container = true
- @can_bulk_update = can?(current_user, :admin_merge_request, @project) - @can_bulk_update = can?(current_user, :admin_merge_request, @project)
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- new_merge_request_path = namespace_project_new_merge_request_path(merge_project.namespace, merge_project) if merge_project
- page_title "Merge Requests" - page_title "Merge Requests"
- unless @project.default_issues_tracker? - unless @project.default_issues_tracker?
...@@ -10,6 +12,9 @@ ...@@ -10,6 +12,9 @@
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search' = webpack_bundle_tag 'filtered_search'
- if show_new_nav?
- content_for :breadcrumbs_extra do
= render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path
= render 'projects/last_push' = render 'projects/last_push'
...@@ -17,13 +22,8 @@ ...@@ -17,13 +22,8 @@
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
= render 'shared/issuable/nav', type: :merge_requests = render 'shared/issuable/nav', type: :merge_requests
.nav-controls .nav-controls{ class: ("visible-xs" if show_new_nav?) }
- if @can_bulk_update = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path
= button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle"
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- if merge_project
= link_to namespace_project_new_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New merge request" do
New merge request
= render 'shared/issuable/search_bar', type: :merge_requests = render 'shared/issuable/search_bar', type: :merge_requests
...@@ -33,4 +33,4 @@ ...@@ -33,4 +33,4 @@
.merge-requests-holder .merge-requests-holder
= render 'merge_requests' = render 'merge_requests'
- else - else
= render 'shared/empty_states/merge_requests', button_path: namespace_project_new_merge_request_path(@project.namespace, @project) = render 'shared/empty_states/merge_requests', button_path: new_merge_request_path
...@@ -73,7 +73,7 @@ ...@@ -73,7 +73,7 @@
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do
#{ _('Set up auto deploy') } #{ _('Set up auto deploy') }
%div{ class: container_class } %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- if @project.archived? - if @project.archived?
.text-warning.center.prepend-top-20 .text-warning.center.prepend-top-20
%p %p
......
...@@ -5,21 +5,21 @@ ...@@ -5,21 +5,21 @@
- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count - issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count
- if issuable_mr > 0 - if issuable_mr > 0
%li %li.issuable-mr.hidden-xs
= image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged') = image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged')
= issuable_mr = issuable_mr
- if upvotes > 0 - if upvotes > 0
%li %li.issuable-upvotes.hidden-xs
= icon('thumbs-up') = icon('thumbs-up')
= upvotes = upvotes
- if downvotes > 0 - if downvotes > 0
%li %li.issuable-downvotes.hidden-xs
= icon('thumbs-down') = icon('thumbs-down')
= downvotes = downvotes
%li %li.issuable-comments.hidden-xs
= link_to issuable_url, class: ('no-comments' if note_count.zero?) do = link_to issuable_url, class: ('no-comments' if note_count.zero?) do
= icon('comments') = icon('comments')
= note_count = note_count
- if @issues.to_a.any? - if @issues.to_a.any?
.panel.panel-default.panel-small.panel-without-border .panel.panel-default.panel-small.panel-without-border
%ul.content-list.issues-list %ul.content-list.issues-list.issuable-list
= render partial: 'projects/issues/issue', collection: @issues = render partial: 'projects/issues/issue', collection: @issues
= paginate @issues, theme: "gitlab" = paginate @issues, theme: "gitlab"
- else - else
......
- if @merge_requests.to_a.any? - if @merge_requests.to_a.any?
.panel.panel-default.panel-small.panel-without-border .panel.panel-default.panel-small.panel-without-border
%ul.content-list.mr-list %ul.content-list.mr-list.issuable-list
= render partial: 'projects/merge_requests/merge_request', collection: @merge_requests = render partial: 'projects/merge_requests/merge_request', collection: @merge_requests
= paginate @merge_requests, theme: "gitlab" = paginate @merge_requests, theme: "gitlab"
......
- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
.dropdown.inline.prepend-left-10 .dropdown.inline.prepend-left-10
%button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } } %button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } }
- if @sort.present? - if @sort.present?
...@@ -23,7 +25,7 @@ ...@@ -23,7 +25,7 @@
= sort_title_milestone_soon = sort_title_milestone_soon
= link_to page_filter_path(sort: sort_value_milestone_later, label: true) do = link_to page_filter_path(sort: sort_value_milestone_later, label: true) do
= sort_title_milestone_later = sort_title_milestone_later
- if controller.controller_name == 'issues' || controller.action_name == 'issues' - if viewing_issues
= link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do = link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do
= sort_title_due_date_soon = sort_title_due_date_soon
= link_to page_filter_path(sort: sort_value_due_date_later, label: true) do = link_to page_filter_path(sort: sort_value_due_date_later, label: true) do
......
- model_name = source.model_name.to_s.downcase - model_name = source.model_name.to_s.downcase
.project-action-button.inline - if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id))
- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) .project-action-button.inline
- link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project') - link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project')
= link_to link_text, polymorphic_path([:leave, source, :members]), = link_to link_text, polymorphic_path([:leave, source, :members]),
method: :delete, method: :delete,
data: { confirm: leave_confirmation_message(source) }, data: { confirm: leave_confirmation_message(source) },
class: 'btn' class: 'btn'
- elsif requester = source.requesters.find_by(user_id: current_user.id) - elsif requester = source.requesters.find_by(user_id: current_user.id)
.project-action-button.inline
= link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]), = link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]),
method: :delete, method: :delete,
data: { confirm: remove_member_message(requester) }, data: { confirm: remove_member_message(requester) },
class: 'btn' class: 'btn'
- elsif source.request_access_enabled && can?(current_user, :request_access, source) - elsif source.request_access_enabled && can?(current_user, :request_access, source)
.project-action-button.inline
= link_to _('Request Access'), polymorphic_path([:request_access, source, :members]), = link_to _('Request Access'), polymorphic_path([:request_access, source, :members]),
method: :post, method: :post,
class: 'btn' class: 'btn'
---
title: Replace 'dashboard/new-project.feature' spinach with rspec
merge_request: 12550
author: Alexander Randa (@randaalex)
---
title: Supplement Traditional Chinese in Taiwan translation of Project Page & Repository Page
merge_request: 12514
author: Huang Tao
---
title: Allow the feature flags to be enabled/disabled with more granularity
merge_request: 12357
author:
---
title: Remove "Remove from board" button from backlog and closed list
merge_request: 12430
author:
---
title: Change milestone endpoint for groups
merge_request: 12374
author: Takuya Noguchi
---
title: Closes any open Autocomplete of the markdown editor when the form is closed
merge_request: 12521
author:
---
title: Improve support for external issue references
merge_request: 12485
author:
---
title: Fix diff of requirements.txt file by not matching newlines as part of package
names
merge_request:
author:
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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