From 502bd6c033aaf05eba23122681852b63a90ad4a6 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Tue, 27 Jun 2017 12:35:13 -0500 Subject: Fixed sidebar not collapsing on merge request in mobile screens --- app/assets/javascripts/merge_request_tabs.js | 1 - .../unreleased/fix-sidebar-showing-mobile-merge-requests.yml | 4 ++++ spec/features/issues/issue_sidebar_spec.rb | 7 +++++-- 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 786b6014dc6..c25d9a95d14 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -144,7 +144,6 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; this.resetViewContainer(); this.mountPipelinesView(); } else { - this.expandView(); this.resetViewContainer(); this.destroyPipelinesView(); } diff --git a/changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml b/changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml new file mode 100644 index 00000000000..856990a6126 --- /dev/null +++ b/changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml @@ -0,0 +1,4 @@ +--- +title: Fixed sidebar not collapsing on merge requests in mobile screens +merge_request: +author: diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 163bc4bb32f..cd06b5af675 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -6,6 +6,7 @@ feature 'Issue Sidebar', feature: true do let(:group) { create(:group, :nested) } let(:project) { create(:project, :public, namespace: group) } let(:issue) { create(:issue, project: project) } + let(:merge_request) { create(:merge_request, source_project: project) } let!(:user) { create(:user)} let!(:label) { create(:label, project: project, title: 'bug') } @@ -158,11 +159,13 @@ feature 'Issue Sidebar', feature: true do before do project.team << [user, :developer] resize_screen_xs - visit_issue(project, issue) end context 'mobile sidebar' do - it 'collapses the sidebar for small screens' do + it 'collapses the sidebar for small screens on an issue/merge_request' do + visit_issue(project, issue) + expect(page).not_to have_css('aside.right-sidebar.right-sidebar-collapsed') + visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) expect(page).not_to have_css('aside.right-sidebar.right-sidebar-collapsed') end end -- cgit v1.2.1 From b37923235bd67542d8f7a02a08c710b6ef3b339d Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 11 Apr 2017 16:48:22 -0500 Subject: ensure eslint recognizes es2015 dynamic import() syntax --- .eslintrc | 1 + package.json | 1 + yarn.lock | 15 ++++++++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 73cd7ecf66d..c72a5e0335b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -11,6 +11,7 @@ "gon": false, "localStorage": false }, + "parser": "babel-eslint", "plugins": [ "filenames", "import", diff --git a/package.json b/package.json index 045f07ee2f9..5a997e813f8 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "babel-core": "^6.22.1", + "babel-eslint": "^7.2.1", "babel-loader": "^6.2.10", "babel-plugin-transform-define": "^1.2.0", "babel-preset-latest": "^6.24.0", diff --git a/yarn.lock b/yarn.lock index b902d5235d0..b04eebe60af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -265,6 +265,15 @@ babel-core@^6.22.1, babel-core@^6.23.0: slash "^1.0.0" source-map "^0.5.0" +babel-eslint@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.1.tgz#079422eb73ba811e3ca0865ce87af29327f8c52f" + dependencies: + babel-code-frame "^6.22.0" + babel-traverse "^6.23.1" + babel-types "^6.23.0" + babylon "^6.16.1" + babel-generator@^6.18.0, babel-generator@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.23.0.tgz#6b8edab956ef3116f79d8c84c5a3c05f32a74bc5" @@ -816,10 +825,14 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.22.0, babel-types@^6.23 lodash "^4.2.0" to-fast-properties "^1.0.1" -babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0: +babylon@^6.11.0: version "6.15.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e" +babylon@^6.13.0, babylon@^6.15.0, babylon@^6.16.1: + version "6.16.1" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3" + backo2@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" -- cgit v1.2.1 From 3f3993c3d3da135ef32ba0abbff66d910a189bb6 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 14 Jun 2017 15:08:02 -0500 Subject: dynamically set webpack publicPath when relative_url_root enabled --- app/assets/javascripts/main.js | 8 ++++++++ config/webpack.config.js | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index d27b4ec78c6..c8761e8fe63 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -164,6 +164,14 @@ import './visibility_select'; import './wikis'; import './zen_mode'; +// set url root for webpack async chunks (assumes config.output.publicPath is an absolute path) +if (gon && gon.relative_url_root) { + const basePath = gon.relative_url_root.replace(/\/$/, ''); + + // eslint-disable-next-line camelcase, no-undef + __webpack_public_path__ = basePath + __webpack_public_path__; +} + // eslint-disable-next-line global-require, import/no-commonjs if (process.env.NODE_ENV !== 'production') require('./test_utils/'); diff --git a/config/webpack.config.js b/config/webpack.config.js index 2e8c94655c1..39e665d5c14 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -244,7 +244,6 @@ if (IS_DEV_SERVER) { hot: DEV_SERVER_LIVERELOAD, inline: DEV_SERVER_LIVERELOAD }; - config.output.publicPath = '//' + DEV_SERVER_HOST + ':' + DEV_SERVER_PORT + config.output.publicPath; config.plugins.push( // watch node_modules for changes if we encounter a missing module compile error new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules')) -- cgit v1.2.1 From f26d4778656c32f550391d1986c4af1ed150364d Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 26 Jun 2017 22:43:32 -0500 Subject: dynamically import emoji helpers for AwardsHandler class --- app/assets/javascripts/awards_handler.js | 79 ++++++++++++++++++-------------- app/assets/javascripts/main.js | 4 +- app/assets/javascripts/notes.js | 10 +++- spec/javascripts/awards_handler_spec.js | 15 +++--- 4 files changed, 61 insertions(+), 47 deletions(-) diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index c34d80f0601..18cd04b176a 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -2,7 +2,6 @@ /* global Flash */ import Cookies from 'js-cookie'; -import * as Emoji from './emoji'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; @@ -24,27 +23,9 @@ const categoryLabelMap = { flags: 'Flags', }; -function renderCategory(name, emojiList, opts = {}) { - return ` -
- ${name} -
-
    - ${emojiList.map(emojiName => ` -
  • - -
  • - `).join('\n')} -
- `; -} - -export default class AwardsHandler { - constructor() { +class AwardsHandler { + constructor(emoji) { + this.emoji = emoji; this.eventListeners = []; // If the user shows intent let's pre-build the menu this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => { @@ -78,10 +59,10 @@ export default class AwardsHandler { const $target = $(e.currentTarget); const $glEmojiElement = $target.find('gl-emoji'); 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'); - this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji); + this.addAward(this.getVotesBlock(), this.getAwardUrl(), emojiName); }); } @@ -139,16 +120,16 @@ export default class AwardsHandler { this.isCreatingEmojiMenu = true; // Render the first category - const categoryMap = Emoji.getEmojiCategoryMap(); + const categoryMap = this.emoji.getEmojiCategoryMap(); const categoryNameKey = Object.keys(categoryMap)[0]; const emojisInCategory = categoryMap[categoryNameKey]; - const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); + const firstCategory = this.renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); // Render the frequently used const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); let frequentlyUsedCatgegory = ''; if (frequentlyUsedEmojis.length > 0) { - frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, { + frequentlyUsedCatgegory = this.renderCategory('Frequently used', frequentlyUsedEmojis, { menuListClass: 'frequent-emojis', }); } @@ -179,7 +160,7 @@ export default class AwardsHandler { } this.isAddingRemainingEmojiMenuCategories = true; - const categoryMap = Emoji.getEmojiCategoryMap(); + const categoryMap = this.emoji.getEmojiCategoryMap(); // Avoid the jank and render the remaining categories separately // This will take more time, but makes UI more responsive @@ -191,7 +172,7 @@ export default class AwardsHandler { promiseChain.then(() => new Promise((resolve) => { const emojisInCategory = categoryMap[categoryNameKey]; - const categoryMarkup = renderCategory( + const categoryMarkup = this.renderCategory( categoryLabelMap[categoryNameKey], emojisInCategory, ); @@ -216,6 +197,25 @@ export default class AwardsHandler { }); } + renderCategory(name, emojiList, opts = {}) { + return ` +
+ ${name} +
+
    + ${emojiList.map(emojiName => ` +
  • + +
  • + `).join('\n')} +
+ `; + } + positionMenu($menu, $addBtn) { const position = $addBtn.data('position'); // The menu could potentially be off-screen or in a hidden overflow element @@ -234,7 +234,7 @@ export default class AwardsHandler { } addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { - const normalizedEmoji = Emoji.normalizeEmojiName(emoji); + const normalizedEmoji = this.emoji.normalizeEmojiName(emoji); const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => { this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); @@ -249,7 +249,7 @@ export default class AwardsHandler { this.checkMutuality(votesBlock, emoji); } this.addEmojiToFrequentlyUsedList(emoji); - const normalizedEmoji = Emoji.normalizeEmojiName(emoji); + const normalizedEmoji = this.emoji.normalizeEmojiName(emoji); const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); if ($emojiButton.length > 0) { if (this.isActive($emojiButton)) { @@ -374,7 +374,7 @@ export default class AwardsHandler { createAwardButtonForVotesBlock(votesBlock, emojiName) { const buttonHtml = ` `; @@ -440,7 +440,7 @@ export default class AwardsHandler { } addEmojiToFrequentlyUsedList(emoji) { - if (Emoji.isEmojiNameValid(emoji)) { + if (this.emoji.isEmojiNameValid(emoji)) { this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji)); Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 }); } @@ -450,7 +450,7 @@ export default class AwardsHandler { return this.frequentlyUsedEmojis || (() => { const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(',')); this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter( - inputName => Emoji.isEmojiNameValid(inputName), + inputName => this.emoji.isEmojiNameValid(inputName), ); return this.frequentlyUsedEmojis; @@ -493,7 +493,7 @@ export default class AwardsHandler { } findMatchingEmojiElements(query) { - const emojiMatches = Emoji.filterEmojiNamesByAlias(query); + const emojiMatches = this.emoji.filterEmojiNamesByAlias(query); const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]'); const $matchingElements = $emojiElements .filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0); @@ -507,3 +507,12 @@ export default class AwardsHandler { $('.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; +} diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index c8761e8fe63..e11d11f87af 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -70,7 +70,7 @@ import './ajax_loading_spinner'; import './api'; import './aside'; import './autosave'; -import AwardsHandler from './awards_handler'; +import loadAwardsHandler from './awards_handler'; import './breakpoints'; import './broadcast_message'; import './build'; @@ -363,7 +363,7 @@ $(function () { $window.off('resize.app').on('resize.app', function () { return fitSidebarForSize(); }); - gl.awardsHandler = new AwardsHandler(); + loadAwardsHandler(); new Aside(); gl.utils.initTimeagoTimeout(); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 34476f3303f..f636b7602cb 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -18,6 +18,7 @@ import 'vendor/jquery.caret'; // required by jquery.atwho import 'vendor/jquery.atwho'; import AjaxCache from '~/lib/utils/ajax_cache'; import CommentTypeToggle from './comment_type_toggle'; +import loadAwardsHandler from './awards_handler'; import './autosave'; import './dropzone_input'; import './task_list'; @@ -291,8 +292,13 @@ export default class Notes { if ('emoji_award' in noteEntity.commands_changes) { 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 + }); } } } diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 3fc03324d16..8e056882108 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */ import Cookies from 'js-cookie'; -import AwardsHandler from '~/awards_handler'; +import loadAwardsHandler from '~/awards_handler'; import '~/lib/utils/common_utils'; @@ -26,14 +26,13 @@ import '~/lib/utils/common_utils'; describe('AwardsHandler', function() { preloadFixtures('issues/issue_with_comment.html.raw'); - beforeEach(function() { + beforeEach(function(done) { loadFixtures('issues/issue_with_comment.html.raw'); - awardsHandler = new AwardsHandler; - spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { - return function(button, url, emoji, cb) { - return cb(); - }; - })(this)); + loadAwardsHandler(true).then((obj) => { + awardsHandler = obj; + spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb()); + done(); + }).catch(fail); let isEmojiMenuBuilt = false; openAndWaitForEmojiMenu = function() { -- cgit v1.2.1 From 2874bab422f5b8342884c019bc39e0922beca469 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 27 Jun 2017 01:26:38 -0500 Subject: dynamically import emoji helpers for GfmAutoComplete class --- app/assets/javascripts/gfm_auto_complete.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index f99bac7da1a..1ff175f981c 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,4 +1,3 @@ -import { validEmojiNames, glEmojiTag } from './emoji'; import glRegexp from './lib/utils/regexp'; import AjaxCache from './lib/utils/ajax_cache'; @@ -373,7 +372,12 @@ class GfmAutoComplete { if (this.cachedData[at]) { this.loadData($input, at, this.cachedData[at]); } 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 { AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true) .then((data) => { @@ -421,12 +425,14 @@ GfmAutoComplete.atTypeMap = { }; // Emoji +GfmAutoComplete.glEmojiTag = null; GfmAutoComplete.Emoji = { templateFunction(name) { - return `
  • - ${name} ${glEmojiTag(name)} -
  • - `; + // glEmojiTag helper is loaded on-demand in fetchData() + if (GfmAutoComplete.glEmojiTag) { + return `
  • ${name} ${GfmAutoComplete.glEmojiTag(name)}
  • `; + } + return `
  • ${name}
  • `; }, }; // Team Members -- cgit v1.2.1 From 8bb2013279a0f9116c01374b55401f3ca0d51c24 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 27 Jun 2017 01:27:06 -0500 Subject: dynamically import emoji helpers for gl-emoji custom tag prototype --- app/assets/javascripts/behaviors/gl_emoji.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js index 8156e491a42..7e98e04303a 100644 --- a/app/assets/javascripts/behaviors/gl_emoji.js +++ b/app/assets/javascripts/behaviors/gl_emoji.js @@ -1,5 +1,4 @@ import installCustomElements from 'document-register-element'; -import { emojiImageTag, emojiFallbackImageSrc } from '../emoji'; import isEmojiUnicodeSupported from '../emoji/support'; installCustomElements(window); @@ -32,11 +31,19 @@ export default function installGlEmojiElement() { // IE 11 doesn't like adding multiple at once :( this.classList.add('emoji-icon'); this.classList.add(fallbackSpriteClass); - } else if (hasImageFallback) { - this.innerHTML = emojiImageTag(name, fallbackSrc); } else { - const src = emojiFallbackImageSrc(name); - this.innerHTML = emojiImageTag(name, src); + import(/* webpackChunkName: 'emoji' */ '../emoji') + .then(({ emojiImageTag, emojiFallbackImageSrc }) => { + if (hasImageFallback) { + this.innerHTML = emojiImageTag(name, fallbackSrc); + } else { + const src = emojiFallbackImageSrc(name); + this.innerHTML = emojiImageTag(name, src); + } + }) + .catch(() => { + // do nothing + }); } } }; -- cgit v1.2.1 From 5b43aa955737f002be3d197c9f7b4d3374d0ad69 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 28 Jun 2017 15:15:50 -0500 Subject: move webpack publicPath setup earlier in the bootstrap processes to avoid ES module execution order issues --- app/assets/javascripts/main.js | 8 -------- app/assets/javascripts/webpack.js | 13 +++++++++++++ app/views/layouts/_head.html.haml | 2 +- config/webpack.config.js | 3 ++- 4 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 app/assets/javascripts/webpack.js diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index e11d11f87af..a4bcb14b636 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -164,14 +164,6 @@ import './visibility_select'; import './wikis'; import './zen_mode'; -// set url root for webpack async chunks (assumes config.output.publicPath is an absolute path) -if (gon && gon.relative_url_root) { - const basePath = gon.relative_url_root.replace(/\/$/, ''); - - // eslint-disable-next-line camelcase, no-undef - __webpack_public_path__ = basePath + __webpack_public_path__; -} - // eslint-disable-next-line global-require, import/no-commonjs if (process.env.NODE_ENV !== 'production') require('./test_utils/'); diff --git a/app/assets/javascripts/webpack.js b/app/assets/javascripts/webpack.js new file mode 100644 index 00000000000..37420dcafa0 --- /dev/null +++ b/app/assets/javascripts/webpack.js @@ -0,0 +1,13 @@ +/** + * 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 settings which cannot be baked-in + * to our webpack bundles. + */ + +if (gon && gon.relative_url_root) { + // this assumes config.output.publicPath is an absolute path + const basePath = gon.relative_url_root.replace(/\/$/, ''); + + // eslint-disable-next-line camelcase, no-undef + __webpack_public_path__ = basePath + __webpack_public_path__; +} diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index eabc9a3b01c..a966349523f 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -36,7 +36,7 @@ = Gon::Base.render_data - = webpack_bundle_tag "runtime" + = webpack_bundle_tag "webpack_runtime" = webpack_bundle_tag "common" = webpack_bundle_tag "locale" = webpack_bundle_tag "main" diff --git a/config/webpack.config.js b/config/webpack.config.js index 39e665d5c14..a2e7a93d501 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -71,6 +71,7 @@ var config = { vue_merge_request_widget: './vue_merge_request_widget/index.js', test: './test.js', peek: './peek.js', + webpack_runtime: './webpack.js', }, output: { @@ -189,7 +190,7 @@ var config = { // create cacheable common library bundles new webpack.optimize.CommonsChunkPlugin({ - names: ['main', 'locale', 'common', 'runtime'], + names: ['main', 'locale', 'common', 'webpack_runtime'], }), ], -- cgit v1.2.1 From 3137b886b84e2c3670b305a8194a3532afe541be Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 28 Jun 2017 21:57:35 -0500 Subject: configure webpack publicPath dynamically to account for CDN or relative path settings --- app/assets/javascripts/webpack.js | 12 ++++-------- app/helpers/webpack_helper.rb | 23 ++++++++++++++++------- lib/gitlab/gon_helper.rb | 3 +++ 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/webpack.js b/app/assets/javascripts/webpack.js index 37420dcafa0..9a9cf395fb8 100644 --- a/app/assets/javascripts/webpack.js +++ b/app/assets/javascripts/webpack.js @@ -1,13 +1,9 @@ /** * 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 settings which cannot be baked-in - * to our webpack bundles. + * config.output.publicPath to account for relative_url_root or CDN settings which cannot be + * baked-in to our webpack bundles. */ -if (gon && gon.relative_url_root) { - // this assumes config.output.publicPath is an absolute path - const basePath = gon.relative_url_root.replace(/\/$/, ''); - - // eslint-disable-next-line camelcase, no-undef - __webpack_public_path__ = basePath + __webpack_public_path__; +if (gon && gon.webpack_public_path) { + __webpack_public_path__ = gon.webpack_public_path; // eslint-disable-line } diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb index 6bacda9fe75..0386df22374 100644 --- a/app/helpers/webpack_helper.rb +++ b/app/helpers/webpack_helper.rb @@ -11,20 +11,29 @@ module WebpackHelper paths = Webpack::Rails::Manifest.asset_paths(source) if extension - paths = paths.select { |p| p.ends_with? ".#{extension}" } + paths.select! { |p| p.ends_with? ".#{extension}" } 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 host = Rails.configuration.webpack.dev_server.host port = Rails.configuration.webpack.dev_server.port protocol = Rails.configuration.webpack.dev_server.https ? 'https' : 'http' - - paths.map! do |p| - "#{protocol}://#{host}:#{port}#{p}" - end + "#{protocol}://#{host}:#{port}" + else + ActionController::Base.asset_host.try(:chomp, '/') end + end - paths + def webpack_public_path + "#{webpack_public_host}/#{Rails.application.config.webpack.public_path}/" end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 319633656ff..2d1ae6a5925 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -2,11 +2,14 @@ module Gitlab module GonHelper + include WebpackHelper + def add_gon_variables gon.api_version = 'v4' gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s gon.max_file_size = current_application_settings.max_attachment_size gon.asset_host = ActionController::Base.asset_host + gon.webpack_public_path = webpack_public_path gon.relative_url_root = Gitlab.config.gitlab.relative_url_root gon.shortcuts_path = help_page_path('shortcuts') gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class -- cgit v1.2.1 From 598baa554c29c7f69191b4ea46a049c558c3dbe5 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 29 Jun 2017 02:31:09 -0500 Subject: add CHANGELOG.md entry for !12032 --- changelogs/unreleased/enable-webpack-code-splitting.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/enable-webpack-code-splitting.yml diff --git a/changelogs/unreleased/enable-webpack-code-splitting.yml b/changelogs/unreleased/enable-webpack-code-splitting.yml new file mode 100644 index 00000000000..d61c3b97d11 --- /dev/null +++ b/changelogs/unreleased/enable-webpack-code-splitting.yml @@ -0,0 +1,5 @@ +--- +title: Enable support for webpack code-splitting by dynamically setting publicPath + at runtime +merge_request: 12032 +author: -- cgit v1.2.1 From 76be9f9c68199f258ca7f82966471ff6cf24fe3e Mon Sep 17 00:00:00 2001 From: Diego de Souza Mendes Date: Thu, 29 Jun 2017 11:23:05 -0300 Subject: updated version of issues baord images (doc) --- doc/user/project/img/issue_board.png | Bin 76461 -> 51439 bytes doc/user/project/img/issue_board_add_list.png | Bin 23632 -> 17312 bytes .../project/img/issue_board_welcome_message.png | Bin 120751 -> 26533 bytes .../project/img/issue_boards_add_issues_modal.png | Bin 177057 -> 29176 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png index b636cb294b8..cf7f519f783 100644 Binary files a/doc/user/project/img/issue_board.png and b/doc/user/project/img/issue_board.png differ diff --git a/doc/user/project/img/issue_board_add_list.png b/doc/user/project/img/issue_board_add_list.png index cdfc466d23f..973d9f7cde4 100644 Binary files a/doc/user/project/img/issue_board_add_list.png and b/doc/user/project/img/issue_board_add_list.png differ diff --git a/doc/user/project/img/issue_board_welcome_message.png b/doc/user/project/img/issue_board_welcome_message.png index 5318e6ea4a9..127b9b08cc7 100644 Binary files a/doc/user/project/img/issue_board_welcome_message.png and b/doc/user/project/img/issue_board_welcome_message.png differ diff --git a/doc/user/project/img/issue_boards_add_issues_modal.png b/doc/user/project/img/issue_boards_add_issues_modal.png index 33049dce74f..bedaf724a15 100644 Binary files a/doc/user/project/img/issue_boards_add_issues_modal.png and b/doc/user/project/img/issue_boards_add_issues_modal.png differ -- cgit v1.2.1 From 81e9c2842574b10d694a8e29665c77fde7fd6ae5 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 12 Jun 2017 14:43:21 -0400 Subject: Render add-diff-note button with server. This commit moves the rendering of the button back to the server, and shows/hides it using opacity rather than display. It also removes the transform applied to the button on hover (scale). Previously, both of these factors automatically triggered a reflow, which creates a performance bottleneck on pages with larger DOM size. MR: !12103 --- app/assets/javascripts/diff.js | 5 +- .../diff_notes/components/diff_note_avatars.js | 4 +- app/assets/javascripts/files_comment_button.js | 193 +++++++-------------- app/assets/javascripts/notes.js | 9 +- app/assets/javascripts/single_file_diff.js | 4 + app/assets/stylesheets/pages/diff.scss | 8 +- app/assets/stylesheets/pages/notes.scss | 10 +- app/helpers/notes_helper.rb | 12 ++ app/views/projects/diffs/_line.html.haml | 3 +- app/views/projects/diffs/_parallel_view.html.haml | 7 +- features/steps/shared/diff_note.rb | 2 +- spec/features/expand_collapse_diffs_spec.rb | 2 +- 12 files changed, 105 insertions(+), 154 deletions(-) diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 725ec7b9c70..1be9df19c81 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -1,6 +1,7 @@ /* eslint-disable class-methods-use-this */ import './lib/utils/url_utility'; +import FilesCommentButton from './files_comment_button'; const UNFOLD_COUNT = 20; let isBound = false; @@ -8,8 +9,10 @@ let isBound = false; class Diff { constructor() { const $diffFile = $('.files .diff-file'); + $diffFile.singleFileDiff(); - $diffFile.filesCommentButton(); + + FilesCommentButton.init($diffFile); $diffFile.each((index, file) => new gl.ImageFile(file)); diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js index 517bdb6be09..c37249c060a 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({ const notesCount = this.notesCount; $(this.$el).closest('.js-avatar-container') - .toggleClass('js-no-comment-btn', notesCount > 0) + .toggleClass('no-comment-btn', notesCount > 0) .nextUntil('.js-avatar-container') - .toggleClass('js-no-comment-btn', notesCount > 0); + .toggleClass('no-comment-btn', notesCount > 0); }, toggleDiscussionsToggleState() { const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 534e651b030..d02e4cd5876 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -1,150 +1,73 @@ /* 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 */ -let $commentButtonTemplate; - -window.FilesCommentButton = (function() { - 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; - - COMMENT_BUTTON_CLASS = '.add-diff-note'; - - LINE_HOLDER_CLASS = '.line_holder'; - - LINE_NUMBER_CLASS = 'diff-line-num'; - - LINE_CONTENT_CLASS = 'line_content'; - - UNFOLDABLE_LINE_CLASS = 'js-unfold'; - - EMPTY_CELL_CLASS = 'empty-cell'; - - OLD_LINE_CLASS = 'old_line'; - - LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; - - TEXT_FILE_SELECTOR = '.text-file'; - - function FilesCommentButton(filesContainerElement) { - 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; +/* Developer beware! Do not add logic to showButton or hideButton + * that will force a reflow. Doing so will create a signficant performance + * bottleneck for pages with large diffs. For a comprehensive list of what + * causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a + */ + +const LINE_NUMBER_CLASS = 'diff-line-num'; +const UNFOLDABLE_LINE_CLASS = 'js-unfold'; +const NO_COMMENT_CLASS = 'no-comment-btn'; +const EMPTY_CELL_CLASS = 'empty-cell'; +const OLD_LINE_CLASS = 'old_line'; +const LINE_COLUMN_CLASSES = `.${LINE_NUMBER_CLASS}, .line_content`; +const DIFF_CONTAINER_SELECTOR = '.files'; +const DIFF_EXPANDED_CLASS = 'diff-expanded'; + +export default { + init($diffFile) { + /* 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 + * will be true in all cases */ + + if (!this.userCanCreateNote) { + // data-can-create-note is an empty string when true, otherwise undefined + this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === ''; } - textFileElement = this.getTextFileElement($currentTarget); - buttonParentElement.append(this.buildButton({ - 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); + if (typeof notes !== 'undefined' && !this.isParallelView) { + this.isParallelView = notes.isParallelView && notes.isParallelView(); } - }; - FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { - if (!this.isParallelView) { - if (hoveredElement.hasClass(OLD_LINE_CLASS)) { - return hoveredElement; - } - return hoveredElement.parent().find("." + OLD_LINE_CLASS); - } else { - if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) { - return hoveredElement; - } - return $(hoveredElement).prev("." + LINE_NUMBER_CLASS); + if (this.userCanCreateNote) { + $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e)) + .on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e)); } - }; + }, - FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { - return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS); - }; + showButton(isParallelView, e) { + const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView); - FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { - return lineContentElement.attr('data-note-type') && lineContentElement.attr('data-note-type') !== ''; - }; + if (!this.validateButtonParent(buttonParentElement)) return; - return FilesCommentButton; -})(); + buttonParentElement.classList.add('is-over'); + buttonParentElement.nextElementSibling.classList.add('is-over'); + }, -$.fn.filesCommentButton = function() { - $commentButtonTemplate = $(''); + hideButton(isParallelView, e) { + const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView); - if (!(this && (this.parent().data('can-create-note') != null))) { - return; - } - return this.each(function() { - if (!$.data(this, 'filesCommentButton')) { - return $.data(this, 'filesCommentButton', new FilesCommentButton($(this))); + buttonParentElement.classList.remove('is-over'); + buttonParentElement.nextElementSibling.classList.remove('is-over'); + }, + + getButtonParent(hoveredElement, isParallelView) { + 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); + }, }; diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 34476f3303f..46d77b31ffd 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -829,6 +829,8 @@ export default class Notes { */ setupDiscussionNoteForm(dataHolder, form) { // setup note target + const diffFileData = dataHolder.closest('.text-file'); + var discussionID = dataHolder.data('discussionId'); if (discussionID) { @@ -839,9 +841,10 @@ export default class Notes { form.attr('data-line-code', dataHolder.data('lineCode')); form.find('#line_type').val(dataHolder.data('lineType')); - form.find('#note_noteable_type').val(dataHolder.data('noteableType')); - form.find('#note_noteable_id').val(dataHolder.data('noteableId')); - form.find('#note_commit_id').val(dataHolder.data('commitId')); + form.find('#note_noteable_type').val(diffFileData.data('noteableType')); + form.find('#note_noteable_id').val(diffFileData.data('noteableId')); + form.find('#note_commit_id').val(diffFileData.data('commitId')); + form.find('#note_type').val(dataHolder.data('noteType')); // LegacyDiffNote diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index c44892dae3d..9316a2af0b7 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -1,5 +1,7 @@ /* 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() { window.SingleFileDiff = (function() { var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; @@ -78,6 +80,8 @@ gl.diffNotesCompileComponents(); } + FilesCommentButton.init($(_this.file)); + if (cb) cb(); }; })(this)); diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index b58922626fa..631649b363f 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -476,6 +476,7 @@ height: 19px; width: 19px; margin-left: -15px; + z-index: 100; &:hover { .diff-comment-avatar, @@ -491,7 +492,7 @@ transform: translateX((($i * $x-pos) - $x-pos)); &:hover { - transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2); + transform: translateX((($i * $x-pos) - $x-pos)); } } } @@ -542,6 +543,7 @@ height: 19px; padding: 0; transition: transform .1s ease-out; + z-index: 100; svg { position: absolute; @@ -555,10 +557,6 @@ fill: $white-light; } - &:hover { - transform: scale(1.2); - } - &:focus { outline: 0; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 53d5cf2f7bc..303425041df 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -628,8 +628,14 @@ ul.notes { * Line note button on the side of diffs */ +.line_holder .is-over:not(.no-comment-btn) { + .add-diff-note { + opacity: 1; + } +} + .add-diff-note { - display: none; + opacity: 0; margin-top: -2px; border-radius: 50%; background: $white-light; @@ -642,13 +648,11 @@ ul.notes { width: 23px; height: 23px; border: 1px solid $blue-500; - transition: transform .1s ease-in-out; &:hover { background: $blue-500; border-color: $blue-600; color: $white-light; - transform: scale(1.15); } &:active { diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 64ad7b280cb..ecc6cd6c6c5 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -47,6 +47,18 @@ module NotesHelper data 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) return unless current_user diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 43708d22a0c..cd0fb21f8a7 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -19,6 +19,7 @@ - if plain = link_text - else + = add_diff_note_button(line_code, diff_file.position(line), type) %a{ href: "##{line_code}", data: { linenumber: link_text } } - discussion = line_discussions.try(:first) - if discussion && discussion.resolvable? && !plain @@ -29,7 +30,7 @@ = link_text - else %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 %pre= line.text - else diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 8e5f4d2573d..56d63250714 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,4 +1,5 @@ / Side-by-side diff view + .text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data } %table - diff_file.parallel_diff_lines.each do |line| @@ -18,11 +19,12 @@ - left_line_code = diff_file.line_code(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 } } + = add_diff_note_button(left_line_code, left_position, 'old') %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } } - discussion_left = discussions_left.try(:first) - if discussion_left && discussion_left.resolvable? %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 %td.old_line.diff-line-num.empty-cell %td.line_content.parallel @@ -38,11 +40,12 @@ - right_line_code = diff_file.line_code(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 } } + = add_diff_note_button(right_line_code, right_position, 'new') %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } } - discussion_right = discussions_right.try(:first) - if discussion_right && discussion_right.resolvable? %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 %td.old_line.diff-line-num.empty-cell %td.line_content.parallel diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 36fc315599e..2c59ec5bb06 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -232,7 +232,7 @@ module SharedDiffNote end def click_parallel_diff_line(code, line_type) - find(".line_content.parallel.#{line_type}[data-line-code='#{code}']").trigger 'mouseover' + find(".line_holder.parallel .diff-line-num[id='#{code}']").trigger 'mouseover' find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click' end end diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index ea749528c11..d492a15ea17 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -129,7 +129,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do before do large_diff.find('.diff-line-num', match: :prefer_exact).hover - large_diff.find('.add-diff-note').click + large_diff.find('.add-diff-note', match: :prefer_exact).click large_diff.find('.note-textarea').send_keys comment_text large_diff.find_button('Comment').click wait_for_requests -- cgit v1.2.1 From 0664715d491311f02074110c639f60fbf80b0e1d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 29 Jun 2017 16:53:56 +0000 Subject: Cleanup codeclimate.json file generated by CI --- .gitlab-ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e52b656599c..8d1a9ce8014 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -474,9 +474,10 @@ codeclimate: services: - docker:dind script: + - docker pull stedolan/jq - 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 > codeclimate.json - - sed -i.bak 's/\({"body":"\)[^"]*\("}\)/\1\2/g' 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 > raw_codeclimate.json + - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name},{fingerprint},{location})' > codeclimate.json artifacts: paths: [codeclimate.json] -- cgit v1.2.1 From 6cdbb1e687bcb787f749b51c472ec8449d0ca0e4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 29 Jun 2017 10:19:54 -0500 Subject: Fix 'New merge request' button for users who don't have push access to canonical project --- app/views/projects/merge_requests/index.html.haml | 8 +++++--- changelogs/unreleased/dm-empty-state-new-merge-request.yml | 5 +++++ 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/dm-empty-state-new-merge-request.yml diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 86996e488a1..1e30cc09894 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -13,6 +13,9 @@ = render 'projects/last_push' +- 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 + - if @project.merge_requests.exists? %div{ class: container_class } .top-area @@ -20,9 +23,8 @@ .nav-controls - if @can_bulk_update = 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 + = link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do New merge request = render 'shared/issuable/search_bar', type: :merge_requests @@ -33,4 +35,4 @@ .merge-requests-holder = render 'merge_requests' - 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 diff --git a/changelogs/unreleased/dm-empty-state-new-merge-request.yml b/changelogs/unreleased/dm-empty-state-new-merge-request.yml new file mode 100644 index 00000000000..5fad7a0f883 --- /dev/null +++ b/changelogs/unreleased/dm-empty-state-new-merge-request.yml @@ -0,0 +1,5 @@ +--- +title: Fix 'New merge request' button for users who don't have push access to canonical + project +merge_request: +author: -- cgit v1.2.1 From a7335c1188d32edd58aebdb818dbf4c49899149b Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Wed, 28 Jun 2017 13:16:40 -0500 Subject: Refactored tests and added a breakpoint to the merge_request_tabs --- app/assets/javascripts/merge_request_tabs.js | 3 +++ spec/features/issuables/user_sees_sidebar_spec.rb | 30 ++++++++++++++++++++++ spec/features/issues/issue_sidebar_spec.rb | 17 ------------ .../features/issuable_sidebar_shared_examples.rb | 9 +++++++ 4 files changed, 42 insertions(+), 17 deletions(-) create mode 100644 spec/features/issuables/user_sees_sidebar_spec.rb create mode 100644 spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index c25d9a95d14..0cbac94e98c 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -144,6 +144,9 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; this.resetViewContainer(); this.mountPipelinesView(); } else { + if (Breakpoints.get().getBreakpointSize() !== 'xs') { + this.expandView(); + } this.resetViewContainer(); this.destroyPipelinesView(); } diff --git a/spec/features/issuables/user_sees_sidebar_spec.rb b/spec/features/issuables/user_sees_sidebar_spec.rb new file mode 100644 index 00000000000..4d7a7dc1806 --- /dev/null +++ b/spec/features/issuables/user_sees_sidebar_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +describe 'Issue Sidebar on Mobile' do + include MobileHelpers + + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:issue) { create(:issue, project: project) } + let!(:user) { create(:user)} + + before do + sign_in(user) + end + + context 'mobile sidebar on merge requests', js: true do + before do + visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) + end + + it_behaves_like "issue sidebar stays collapsed on mobile" + end + + context 'mobile sidebar on issues', js: true do + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it_behaves_like "issue sidebar stays collapsed on mobile" + end +end diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index cd06b5af675..09724781a27 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -6,7 +6,6 @@ feature 'Issue Sidebar', feature: true do let(:group) { create(:group, :nested) } let(:project) { create(:project, :public, namespace: group) } let(:issue) { create(:issue, project: project) } - let(:merge_request) { create(:merge_request, source_project: project) } let!(:user) { create(:user)} let!(:label) { create(:label, project: project, title: 'bug') } @@ -155,22 +154,6 @@ feature 'Issue Sidebar', feature: true do end end - context 'as a allowed mobile user', js: true do - before do - project.team << [user, :developer] - resize_screen_xs - end - - context 'mobile sidebar' do - it 'collapses the sidebar for small screens on an issue/merge_request' do - visit_issue(project, issue) - expect(page).not_to have_css('aside.right-sidebar.right-sidebar-collapsed') - visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) - expect(page).not_to have_css('aside.right-sidebar.right-sidebar-collapsed') - end - end - end - context 'as a guest' do before do project.team << [user, :guest] diff --git a/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb b/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb new file mode 100644 index 00000000000..96c821b26f7 --- /dev/null +++ b/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb @@ -0,0 +1,9 @@ +shared_examples 'issue sidebar stays collapsed on mobile' do + before do + resize_screen_xs + end + + it 'keeps the sidebar collapsed' do + expect(page).not_to have_css('.right-sidebar.right-sidebar-collapsed') + end +end -- cgit v1.2.1 From c051d47de83ed0446a05d3972a806f953a6d4bb9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 29 Jun 2017 18:42:45 +0000 Subject: Fix codeclimate job in .gitlab-ci.yml --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8d1a9ce8014..5723b836c76 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -477,7 +477,7 @@ codeclimate: - docker pull stedolan/jq - 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 - - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name},{fingerprint},{location})' > codeclimate.json + - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json artifacts: paths: [codeclimate.json] -- cgit v1.2.1 From def0f6281029659503f32c4b93e90ac6cbfd6884 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 26 Jun 2017 15:28:09 -0400 Subject: Remove initTimeagoTimeout and let timeago.js update timeagos internally. MR: !12468 --- .../javascripts/lib/utils/datetime_utility.js | 24 +++------------------- app/assets/javascripts/main.js | 2 +- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index bfcc50996cc..034a1ec2054 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -112,29 +112,11 @@ window.dateFormat = dateFormat; return timefor; }; - w.gl.utils.cachedTimeagoElements = []; w.gl.utils.renderTimeago = function($els) { - if (!$els && !w.gl.utils.cachedTimeagoElements.length) { - 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(); + const timeagoEls = $els || document.querySelectorAll('.js-timeago-render'); - 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); }; w.gl.utils.getDayDifference = function(a, b) { diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index d27b4ec78c6..de1b658f602 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -358,7 +358,7 @@ $(function () { gl.awardsHandler = new AwardsHandler(); new Aside(); - gl.utils.initTimeagoTimeout(); + gl.utils.renderTimeago(); $(document).trigger('init.scrolling-tabs'); }); -- cgit v1.2.1 From f4e6aba1bbeca043a29b4903cef2f5b99a1faac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Thu, 29 Jun 2017 15:22:40 -0400 Subject: Set the GL_REPOSITORY env variable on Gitlab::Git::Hook --- app/services/git_hooks_service.rb | 6 ++-- app/services/git_operation_service.rb | 2 +- lib/gitlab/git/hook.rb | 8 +++-- .../projects/import_export/import_file_spec.rb | 2 +- spec/lib/gitlab/git/hook_spec.rb | 38 ++++++++++++++++------ .../lib/gitlab/import_export/repo_restorer_spec.rb | 2 +- spec/models/repository_spec.rb | 15 +++------ spec/services/git_hooks_service_spec.rb | 7 ++-- 8 files changed, 47 insertions(+), 33 deletions(-) diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb index d222d1e63aa..eab65d09299 100644 --- a/app/services/git_hooks_service.rb +++ b/app/services/git_hooks_service.rb @@ -3,8 +3,8 @@ class GitHooksService attr_accessor :oldrev, :newrev, :ref - def execute(user, repo_path, oldrev, newrev, ref) - @repo_path = repo_path + def execute(user, project, oldrev, newrev, ref) + @project = project @user = Gitlab::GlId.gl_id(user) @oldrev = oldrev @newrev = newrev @@ -26,7 +26,7 @@ class GitHooksService private 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) end end diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index ed6ea638235..43636fde0be 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -120,7 +120,7 @@ class GitOperationService def with_hooks(ref, newrev, oldrev) GitHooksService.new.execute( user, - repository.path_to_repo, + repository.project, oldrev, newrev, ref) do |service| diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb index bd90d24a2ec..5042916343b 100644 --- a/lib/gitlab/git/hook.rb +++ b/lib/gitlab/git/hook.rb @@ -4,9 +4,10 @@ module Gitlab GL_PROTOCOL = 'web'.freeze attr_reader :name, :repo_path, :path - def initialize(name, repo_path) + def initialize(name, project) @name = name - @repo_path = repo_path + @project = project + @repo_path = project.repository.path @path = File.join(repo_path.strip, 'hooks', name) end @@ -38,7 +39,8 @@ module Gitlab vars = { 'GL_ID' => gl_id, 'PWD' => repo_path, - 'GL_PROTOCOL' => GL_PROTOCOL + 'GL_PROTOCOL' => GL_PROTOCOL, + 'GL_REPOSITORY' => Gitlab::GlRepository.gl_repository(@project, false) } options = { diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index a111aa87c52..3f8d2255298 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -98,6 +98,6 @@ feature 'Import/Export - project import integration test', feature: true, js: tr end def project_hook_exists?(project) - Gitlab::Git::Hook.new('post-receive', project.repository.path).exists? + Gitlab::Git::Hook.new('post-receive', project).exists? end end diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb index 3f279c21865..73518656bde 100644 --- a/spec/lib/gitlab/git/hook_spec.rb +++ b/spec/lib/gitlab/git/hook_spec.rb @@ -4,18 +4,20 @@ require 'fileutils' describe Gitlab::Git::Hook, lib: true do describe "#trigger" do let(:project) { create(:project, :repository) } + let(:repo_path) { project.repository.path } let(:user) { create(:user) } + let(:gl_id) { Gitlab::GlId.gl_id(user) } def create_hook(name) - FileUtils.mkdir_p(File.join(project.repository.path, 'hooks')) - File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f| + FileUtils.mkdir_p(File.join(repo_path, 'hooks')) + File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f| f.write('exit 0') end end def create_failing_hook(name) - FileUtils.mkdir_p(File.join(project.repository.path, 'hooks')) - File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f| + FileUtils.mkdir_p(File.join(repo_path, 'hooks')) + File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f| f.write(<<-HOOK) echo 'regular message from the hook' echo 'error message from the hook' 1>&2 @@ -27,13 +29,29 @@ describe Gitlab::Git::Hook, lib: true do ['pre-receive', 'post-receive', 'update'].each do |hook_name| context "when triggering a #{hook_name} hook" do context "when the hook is successful" do + let(:hook_path) { File.join(repo_path, 'hooks', hook_name) } + let(:gl_repository) { Gitlab::GlRepository.gl_repository(project, false) } + let(:env) do + { + 'GL_ID' => gl_id, + 'PWD' => repo_path, + 'GL_PROTOCOL' => 'web', + 'GL_REPOSITORY' => gl_repository + } + end + it "returns success with no errors" do create_hook(hook_name) - hook = Gitlab::Git::Hook.new(hook_name, project.repository.path) + hook = Gitlab::Git::Hook.new(hook_name, project) blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + if hook_name != 'update' + expect(Open3).to receive(:popen3) + .with(env, hook_path, chdir: repo_path).and_call_original + end + + status, errors = hook.trigger(gl_id, blank, blank, ref) expect(status).to be true expect(errors).to be_blank end @@ -42,11 +60,11 @@ describe Gitlab::Git::Hook, lib: true do context "when the hook is unsuccessful" do it "returns failure with errors" do create_failing_hook(hook_name) - hook = Gitlab::Git::Hook.new(hook_name, project.repository.path) + hook = Gitlab::Git::Hook.new(hook_name, project) blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + status, errors = hook.trigger(gl_id, blank, blank, ref) expect(status).to be false expect(errors).to eq("error message from the hook\n") end @@ -56,11 +74,11 @@ describe Gitlab::Git::Hook, lib: true do context "when the hook doesn't exist" do it "returns success with no errors" do - hook = Gitlab::Git::Hook.new('unknown_hook', project.repository.path) + hook = Gitlab::Git::Hook.new('unknown_hook', project) blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + status, errors = hook.trigger(gl_id, blank, blank, ref) expect(status).to be true expect(errors).to be_nil end diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb index 168a59e5139..30b6a0d8845 100644 --- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -34,7 +34,7 @@ describe Gitlab::ImportExport::RepoRestorer, services: true do it 'has the webhooks' do restorer.restore - expect(Gitlab::Git::Hook.new('post-receive', project.repository.path_to_repo)).to exist + expect(Gitlab::Git::Hook.new('post-receive', project)).to exist end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 3e984ec7588..c69f0a495db 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -780,7 +780,7 @@ describe Repository, models: true do context 'when pre hooks were successful' do it 'runs without errors' do expect_any_instance_of(GitHooksService).to receive(:execute) - .with(user, project.repository.path_to_repo, old_rev, blank_sha, 'refs/heads/feature') + .with(user, project, old_rev, blank_sha, 'refs/heads/feature') expect { repository.rm_branch(user, 'feature') }.not_to raise_error end @@ -823,12 +823,7 @@ describe Repository, models: true do service = GitHooksService.new expect(GitHooksService).to receive(:new).and_return(service) expect(service).to receive(:execute) - .with( - user, - repository.path_to_repo, - old_rev, - new_rev, - 'refs/heads/feature') + .with(user, project, old_rev, new_rev, 'refs/heads/feature') .and_yield(service).and_return(true) end @@ -1474,9 +1469,9 @@ describe Repository, models: true do it 'passes commit SHA to pre-receive and update hooks,\ and tag SHA to post-receive hook' do - pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', repository.path_to_repo) - update_hook = Gitlab::Git::Hook.new('update', repository.path_to_repo) - post_receive_hook = Gitlab::Git::Hook.new('post-receive', repository.path_to_repo) + pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', project) + update_hook = Gitlab::Git::Hook.new('update', project) + post_receive_hook = Gitlab::Git::Hook.new('post-receive', project) allow(Gitlab::Git::Hook).to receive(:new) .and_return(pre_receive_hook, update_hook, post_receive_hook) diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb index ac7ccfbaab0..213678c27f5 100644 --- a/spec/services/git_hooks_service_spec.rb +++ b/spec/services/git_hooks_service_spec.rb @@ -12,7 +12,6 @@ describe GitHooksService, services: true do @oldrev = sample_commit.parent_id @newrev = sample_commit.id @ref = 'refs/heads/feature' - @repo_path = project.repository.path_to_repo end describe '#execute' do @@ -21,7 +20,7 @@ describe GitHooksService, services: true do hook = double(trigger: [true, nil]) expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) - service.execute(user, @repo_path, @blankrev, @newrev, @ref) { } + service.execute(user, project, @blankrev, @newrev, @ref) { } end end @@ -31,7 +30,7 @@ describe GitHooksService, services: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(user, @repo_path, @blankrev, @newrev, @ref) + service.execute(user, project, @blankrev, @newrev, @ref) end.to raise_error(GitHooksService::PreReceiveError) end end @@ -43,7 +42,7 @@ describe GitHooksService, services: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(user, @repo_path, @blankrev, @newrev, @ref) + service.execute(user, project, @blankrev, @newrev, @ref) end.to raise_error(GitHooksService::PreReceiveError) end end -- cgit v1.2.1 From 553346a4f4ffff4ed8cfabc090529a02021f1ca4 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Thu, 29 Jun 2017 22:31:58 +0000 Subject: Remove empty afterEach() from issue_show app_spec.js --- spec/javascripts/issue_show/components/app_spec.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 9df92318864..bc13373a27e 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -42,9 +42,6 @@ describe('Issuable output', () => { }).$mount(); }); - afterEach(() => { - }); - it('should render a title/description/edited and update title/description/edited on update', (done) => { vm.poll.options.successCallback({ json() { -- cgit v1.2.1 From 42ccb5981a8425216f9d69372754c52510f0cade Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 20 Jun 2017 16:38:14 +0100 Subject: Only do complicated confidentiality checks when necessary When we are filtering by a single project, and the current user has access to see confidential issues on that project, we don't need to filter by confidentiality at all - just as if the user were an admin. The filter by confidentiality often picks a non-optimal query plan: for instance, AND-ing the results of all issues in the project (a relatively small set), and all issues in the states requested (a huge set), rather than just starting small and winnowing further. --- app/finders/issues_finder.rb | 42 +++++++---- app/views/layouts/nav/_project.html.haml | 4 +- spec/finders/issues_finder_spec.rb | 125 +++++++++++++++++++++++++++---- 3 files changed, 142 insertions(+), 29 deletions(-) diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 3da5508aefd..3455a75b8bc 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -16,14 +16,30 @@ # sort: string # class IssuesFinder < IssuableFinder + CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER + def klass Issue end + def not_restricted_by_confidentiality + return Issue.where('issues.confidential IS NOT TRUE') if user_cannot_see_confidential_issues? + return Issue.all if user_can_see_all_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 def init_collection - IssuesFinder.not_restricted_by_confidentiality(current_user) + not_restricted_by_confidentiality end def by_assignee(items) @@ -38,22 +54,20 @@ class IssuesFinder < IssuableFinder end end - def self.not_restricted_by_confidentiality(user) - return Issue.where('issues.confidential IS NOT TRUE') if user.blank? + def item_project_ids(items) + items&.reorder(nil)&.select(:project_id) + end - return Issue.all if user.full_private_access? + def user_can_see_all_confidential_issues? + return false unless current_user + return true if current_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)) + project? && + project && + project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL end - def item_project_ids(items) - items&.reorder(nil)&.select(:project_id) + def user_cannot_see_confidential_issues? + current_user.blank? end end diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 68024d782a6..a2c6e44425a 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -28,7 +28,7 @@ %span Issues - 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(IssuesFinder.new(current_user, project_id: @project.id, state: :opened).execute.count) - if project_nav_tab? :merge_requests - controllers = [:merge_requests, 'projects/merge_requests/conflicts'] @@ -37,7 +37,7 @@ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span 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(MergeRequestsFinder.new(current_user, project_id: @project.id, state: :opened).execute.count) - if project_nav_tab? :pipelines = nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 8ace1fb5751..dfa15e859a4 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -295,22 +295,121 @@ describe IssuesFinder do end end - describe '.not_restricted_by_confidentiality' do - let(:authorized_user) { create(:user) } - let(:project) { create(:empty_project, namespace: authorized_user.namespace) } - let!(:public_issue) { create(:issue, project: project) } - let!(:confidential_issue) { create(:issue, project: project, confidential: true) } - - it 'returns non confidential issues for nil user' do - expect(described_class.send(:not_restricted_by_confidentiality, nil)).to include(public_issue) - end + describe '#not_restricted_by_confidentiality' do + let(:guest) { create(:user) } + set(:authorized_user) { create(:user) } + set(:project) { create(:empty_project, namespace: authorized_user.namespace) } + set(:public_issue) { create(:issue, project: project) } + set(:confidential_issue) { create(:issue, project: project, confidential: true) } + + context 'when no project filter is given' do + let(:params) { {} } + + context 'for an anonymous user' do + subject { described_class.new(nil, params).not_restricted_by_confidentiality } + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + end + + context 'for a user without project membership' do + subject { described_class.new(user, params).not_restricted_by_confidentiality } + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + end + + context 'for a guest user' do + subject { described_class.new(guest, params).not_restricted_by_confidentiality } + + before do + project.add_guest(guest) + end + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + end + + context 'for a project member with access to view confidential issues' do + subject { described_class.new(authorized_user, params).not_restricted_by_confidentiality } - it 'returns non confidential issues for user not authorized for the issues projects' do - expect(described_class.send(:not_restricted_by_confidentiality, user)).to include(public_issue) + it 'returns all issues' do + expect(subject).to include(public_issue, confidential_issue) + end + end end - it 'returns all issues for user authorized for the issues projects' do - expect(described_class.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue) + context 'when searching within a specific project' do + let(:params) { { project_id: project.id } } + + context 'for an anonymous user' do + subject { described_class.new(nil, params).not_restricted_by_confidentiality } + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + + it 'does not filter by confidentiality' do + expect(Issue).not_to receive(:where).with(a_string_matching('confidential'), anything) + + subject + end + end + + context 'for a user without project membership' do + subject { described_class.new(user, params).not_restricted_by_confidentiality } + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + + it 'filters by confidentiality' do + expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything) + + subject + end + end + + context 'for a guest user' do + subject { described_class.new(guest, params).not_restricted_by_confidentiality } + + before do + project.add_guest(guest) + end + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + + it 'filters by confidentiality' do + expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything) + + subject + end + end + + context 'for a project member with access to view confidential issues' do + subject { described_class.new(authorized_user, params).not_restricted_by_confidentiality } + + it 'returns all issues' do + expect(subject).to include(public_issue, confidential_issue) + end + + it 'does not filter by confidentiality' do + expect(Issue).not_to receive(:where).with(a_string_matching('confidential'), anything) + + subject + end + end end end end -- cgit v1.2.1 From 20bb678d91715817f3da04c7a1b73db84295accd Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 22 Jun 2017 20:58:20 +0100 Subject: Cache total issue / MR counts for project by user type This runs a slightly slower query to get the issue and MR counts in the navigation, but caches by user type (can see all / none confidential issues) for two minutes. --- app/finders/issues_finder.rb | 26 ++++++++--------- app/helpers/issuables_helper.rb | 29 ++++++++++++------- app/views/layouts/nav/_project.html.haml | 4 +-- spec/helpers/issuables_helper_spec.rb | 49 ++++++++++++++++++++++++++++---- 4 files changed, 77 insertions(+), 31 deletions(-) diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 3455a75b8bc..328198c026a 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -36,6 +36,19 @@ class IssuesFinder < IssuableFinder project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id)) end + def user_can_see_all_confidential_issues? + return false unless current_user + return true if current_user.full_private_access? + + project? && + project && + project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL + end + + def user_cannot_see_confidential_issues? + current_user.blank? + end + private def init_collection @@ -57,17 +70,4 @@ class IssuesFinder < IssuableFinder def item_project_ids(items) items&.reorder(nil)&.select(:project_id) end - - def user_can_see_all_confidential_issues? - return false unless current_user - return true if current_user.full_private_access? - - project? && - project && - project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL - end - - def user_cannot_see_confidential_issues? - current_user.blank? - end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 3259a9c1933..d99a9bab12f 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -165,11 +165,7 @@ module IssuablesHelper } state_title = titles[state] || state.to_s.humanize - - 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 + count = issuables_count_for_state(issuable_type, state) html = content_tag(:span, state_title) html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge') @@ -255,22 +251,35 @@ module IssuablesHelper end end - def issuables_count_for_state(issuable_type, state) + def issuables_count_for_state(issuable_type, state, finder: nil) + finder ||= public_send("#{issuable_type}_finder") + cache_key = issuables_state_counter_cache_key(issuable_type, finder, state) + @counts ||= {} - @counts[issuable_type] ||= public_send("#{issuable_type}_finder").count_by_state - @counts[issuable_type][state] + @counts[cache_key] ||= Rails.cache.fetch(cache_key, expires_in: 2.minutes) do + finder.count_by_state + end + + @counts[cache_key][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) + def issuables_state_counter_cache_key(issuable_type, finder, 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('-')) + key_components = ['issuables_count', issuable_type, opts.sort] + + if issuable_type == :issues + key_components << finder.user_can_see_all_confidential_issues? + key_components << finder.user_cannot_see_confidential_issues? + end + + hexdigest(key_components.flatten.join('-')) end def issuable_templates(issuable) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index a2c6e44425a..b095adcfe7e 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -28,7 +28,7 @@ %span Issues - if @project.default_issues_tracker? - %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id, state: :opened).execute.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 - controllers = [:merge_requests, 'projects/merge_requests/conflicts'] @@ -37,7 +37,7 @@ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span Merge Requests - %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id, state: :opened).execute.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 = nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 15cb620199d..7dfda388de4 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -77,20 +77,58 @@ describe IssuablesHelper do }.with_indifferent_access end + let(:finder) { double(:finder, user_cannot_see_confidential_issues?: true, user_can_see_all_confidential_issues?: false) } + + before do + allow(helper).to receive(:issues_finder).and_return(finder) + allow(helper).to receive(:merge_requests_finder).and_return(finder) + end + it 'returns the cached value when called for the same issuable type & with the same params' do expect(helper).to receive(:params).twice.and_return(params) - expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42) + expect(finder).to receive(:count_by_state).and_return(opened: 42) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') - expect(helper).not_to receive(:issuables_count_for_state) + expect(finder).not_to receive(:count_by_state) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') end + it 'takes confidential status into account when searching for issues' do + allow(helper).to receive(:params).and_return(params) + expect(finder).to receive(:count_by_state).and_return(opened: 42) + + expect(helper.issuables_state_counter_text(:issues, :opened)) + .to include('42') + + expect(finder).to receive(:user_cannot_see_confidential_issues?).and_return(false) + expect(finder).to receive(:count_by_state).and_return(opened: 40) + + expect(helper.issuables_state_counter_text(:issues, :opened)) + .to include('40') + + expect(finder).to receive(:user_can_see_all_confidential_issues?).and_return(true) + expect(finder).to receive(:count_by_state).and_return(opened: 45) + + expect(helper.issuables_state_counter_text(:issues, :opened)) + .to include('45') + end + + it 'does not take confidential status into account when searching for merge requests' do + allow(helper).to receive(:params).and_return(params) + expect(finder).to receive(:count_by_state).and_return(opened: 42) + expect(finder).not_to receive(:user_cannot_see_confidential_issues?) + expect(finder).not_to receive(:user_can_see_all_confidential_issues?) + + expect(helper.issuables_state_counter_text(:merge_requests, :opened)) + .to include('42') + end + it 'does not take some keys into account in the cache key' do + expect(finder).to receive(:count_by_state).and_return(opened: 42) expect(helper).to receive(:params).and_return({ author_id: '11', state: 'foo', @@ -98,11 +136,11 @@ describe IssuablesHelper do utf8: 'foo', page: 'foo' }.with_indifferent_access) - expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') + expect(finder).not_to receive(:count_by_state) expect(helper).to receive(:params).and_return({ author_id: '11', state: 'bar', @@ -110,7 +148,6 @@ describe IssuablesHelper do utf8: 'bar', page: 'bar' }.with_indifferent_access) - expect(helper).not_to receive(:issuables_count_for_state) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') @@ -118,13 +155,13 @@ describe IssuablesHelper do it 'does not take params order into account in the cache key' do expect(helper).to receive(:params).and_return('author_id' => '11', 'state' => 'opened') - expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42) + expect(finder).to receive(:count_by_state).and_return(opened: 42) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') expect(helper).to receive(:params).and_return('state' => 'opened', 'author_id' => '11') - expect(helper).not_to receive(:issuables_count_for_state) + expect(finder).not_to receive(:count_by_state) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') -- cgit v1.2.1 From c400030d0f51c71f32990ab0ecfebfa245ed663e Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 23 Jun 2017 12:50:33 +0100 Subject: Don't count any confidential issues for non-project-members --- app/finders/issuable_finder.rb | 2 +- app/finders/issues_finder.rb | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 558f8b5e2e5..e8605f3d5b3 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -62,7 +62,7 @@ class IssuableFinder # grouping and counting within that query. # 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 finder = self.class.new(current_user, count_params) counts = Hash.new(0) diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 328198c026a..b213a7aebfd 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -23,8 +23,8 @@ class IssuesFinder < IssuableFinder end def not_restricted_by_confidentiality - return Issue.where('issues.confidential IS NOT TRUE') if user_cannot_see_confidential_issues? 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 @@ -37,16 +37,19 @@ class IssuesFinder < IssuableFinder end def user_can_see_all_confidential_issues? - return false unless current_user - return true if current_user.full_private_access? + 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? - project? && + @user_can_see_all_confidential_issues = + project? && project && project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL end def user_cannot_see_confidential_issues? - current_user.blank? + return false if user_can_see_all_confidential_issues? + + current_user.blank? || params[:for_counting] end private -- cgit v1.2.1 From 8deece32478aaa83354fcfff7b5d6f3250d55844 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 26 Jun 2017 15:48:27 +0100 Subject: Add changelog entry for issue / MR tab counting optimisations --- changelogs/unreleased/speed-up-issue-counting-for-a-project.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/speed-up-issue-counting-for-a-project.yml diff --git a/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml b/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml new file mode 100644 index 00000000000..493ecbcb77a --- /dev/null +++ b/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml @@ -0,0 +1,5 @@ +--- +title: Cache open issue and merge request counts for project tabs to speed up project + pages +merge_request: +author: -- cgit v1.2.1 From 0c6cdd07829668e04012219eb21cc60db8c1eabc Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 29 Jun 2017 12:43:56 +0100 Subject: Make finders responsible for counter cache keys --- app/finders/issuable_finder.rb | 14 +++++++++++ app/finders/issues_finder.rb | 23 +++++++++++++----- app/helpers/issuables_helper.rb | 21 +--------------- spec/finders/issues_finder_spec.rb | 18 +++++++------- spec/helpers/issuables_helper_spec.rb | 46 +++++++++++++++++------------------ 5 files changed, 63 insertions(+), 59 deletions(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index e8605f3d5b3..7bc2117f61e 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -20,6 +20,7 @@ # class IssuableFinder NONE = '0'.freeze + IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze attr_accessor :current_user, :params @@ -86,6 +87,10 @@ class IssuableFinder execute.find_by!(*params) end + def state_counter_cache_key(state) + Digest::SHA1.hexdigest(state_counter_cache_key_components(state).flatten.join('-')) + end + def group return @group if defined?(@group) @@ -418,4 +423,13 @@ class IssuableFinder def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' 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 diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index b213a7aebfd..d20f4475a03 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -22,7 +22,7 @@ class IssuesFinder < IssuableFinder Issue end - def not_restricted_by_confidentiality + 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? @@ -36,7 +36,15 @@ class IssuesFinder < IssuableFinder project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id)) end + private + + def init_collection + 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? @@ -46,16 +54,19 @@ class IssuesFinder < IssuableFinder project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL end - def user_cannot_see_confidential_issues? + def user_cannot_see_confidential_issues?(for_counting: false) return false if user_can_see_all_confidential_issues? - current_user.blank? || params[:for_counting] + current_user.blank? || for_counting || params[:for_counting] end - private + def state_counter_cache_key_components(state) + extra_components = [ + user_can_see_all_confidential_issues?, + user_cannot_see_confidential_issues?(for_counting: true) + ] - def init_collection - not_restricted_by_confidentiality + super + extra_components end def by_assignee(items) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index d99a9bab12f..5385ada6dc4 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -253,7 +253,7 @@ module IssuablesHelper def issuables_count_for_state(issuable_type, state, finder: nil) finder ||= public_send("#{issuable_type}_finder") - cache_key = issuables_state_counter_cache_key(issuable_type, finder, state) + cache_key = finder.state_counter_cache_key(state) @counts ||= {} @counts[cache_key] ||= Rails.cache.fetch(cache_key, expires_in: 2.minutes) do @@ -263,25 +263,6 @@ module IssuablesHelper @counts[cache_key][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, finder, state) - opts = params.with_indifferent_access - opts[:state] = state - opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY) - opts.delete_if { |_, value| value.blank? } - - key_components = ['issuables_count', issuable_type, opts.sort] - - if issuable_type == :issues - key_components << finder.user_can_see_all_confidential_issues? - key_components << finder.user_cannot_see_confidential_issues? - end - - hexdigest(key_components.flatten.join('-')) - end - def issuable_templates(issuable) @issuable_templates ||= case issuable diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index dfa15e859a4..4a52f0d5c58 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -295,7 +295,7 @@ describe IssuesFinder do end end - describe '#not_restricted_by_confidentiality' do + describe '#with_confidentiality_access_check' do let(:guest) { create(:user) } set(:authorized_user) { create(:user) } set(:project) { create(:empty_project, namespace: authorized_user.namespace) } @@ -306,7 +306,7 @@ describe IssuesFinder do let(:params) { {} } context 'for an anonymous user' do - subject { described_class.new(nil, params).not_restricted_by_confidentiality } + subject { described_class.new(nil, params).with_confidentiality_access_check } it 'returns only public issues' do expect(subject).to include(public_issue) @@ -315,7 +315,7 @@ describe IssuesFinder do end context 'for a user without project membership' do - subject { described_class.new(user, params).not_restricted_by_confidentiality } + subject { described_class.new(user, params).with_confidentiality_access_check } it 'returns only public issues' do expect(subject).to include(public_issue) @@ -324,7 +324,7 @@ describe IssuesFinder do end context 'for a guest user' do - subject { described_class.new(guest, params).not_restricted_by_confidentiality } + subject { described_class.new(guest, params).with_confidentiality_access_check } before do project.add_guest(guest) @@ -337,7 +337,7 @@ describe IssuesFinder do end context 'for a project member with access to view confidential issues' do - subject { described_class.new(authorized_user, params).not_restricted_by_confidentiality } + subject { described_class.new(authorized_user, params).with_confidentiality_access_check } it 'returns all issues' do expect(subject).to include(public_issue, confidential_issue) @@ -349,7 +349,7 @@ describe IssuesFinder do let(:params) { { project_id: project.id } } context 'for an anonymous user' do - subject { described_class.new(nil, params).not_restricted_by_confidentiality } + subject { described_class.new(nil, params).with_confidentiality_access_check } it 'returns only public issues' do expect(subject).to include(public_issue) @@ -364,7 +364,7 @@ describe IssuesFinder do end context 'for a user without project membership' do - subject { described_class.new(user, params).not_restricted_by_confidentiality } + subject { described_class.new(user, params).with_confidentiality_access_check } it 'returns only public issues' do expect(subject).to include(public_issue) @@ -379,7 +379,7 @@ describe IssuesFinder do end context 'for a guest user' do - subject { described_class.new(guest, params).not_restricted_by_confidentiality } + subject { described_class.new(guest, params).with_confidentiality_access_check } before do project.add_guest(guest) @@ -398,7 +398,7 @@ describe IssuesFinder do end context 'for a project member with access to view confidential issues' do - subject { described_class.new(authorized_user, params).not_restricted_by_confidentiality } + subject { described_class.new(authorized_user, params).with_confidentiality_access_check } it 'returns all issues' do expect(subject).to include(public_issue, confidential_issue) diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 7dfda388de4..d2e918ef014 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -77,59 +77,57 @@ describe IssuablesHelper do }.with_indifferent_access end - let(:finder) { double(:finder, user_cannot_see_confidential_issues?: true, user_can_see_all_confidential_issues?: false) } + let(:issues_finder) { IssuesFinder.new(nil, params) } + let(:merge_requests_finder) { MergeRequestsFinder.new(nil, params) } before do - allow(helper).to receive(:issues_finder).and_return(finder) - allow(helper).to receive(:merge_requests_finder).and_return(finder) + allow(helper).to receive(:issues_finder).and_return(issues_finder) + allow(helper).to receive(:merge_requests_finder).and_return(merge_requests_finder) end it 'returns the cached value when called for the same issuable type & with the same params' do - expect(helper).to receive(:params).twice.and_return(params) - expect(finder).to receive(:count_by_state).and_return(opened: 42) + expect(issues_finder).to receive(:count_by_state).and_return(opened: 42) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') - expect(finder).not_to receive(:count_by_state) + expect(issues_finder).not_to receive(:count_by_state) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') end it 'takes confidential status into account when searching for issues' do - allow(helper).to receive(:params).and_return(params) - expect(finder).to receive(:count_by_state).and_return(opened: 42) + expect(issues_finder).to receive(:count_by_state).and_return(opened: 42) expect(helper.issuables_state_counter_text(:issues, :opened)) .to include('42') - expect(finder).to receive(:user_cannot_see_confidential_issues?).and_return(false) - expect(finder).to receive(:count_by_state).and_return(opened: 40) + expect(issues_finder).to receive(:user_cannot_see_confidential_issues?).twice.and_return(false) + expect(issues_finder).to receive(:count_by_state).and_return(opened: 40) expect(helper.issuables_state_counter_text(:issues, :opened)) .to include('40') - expect(finder).to receive(:user_can_see_all_confidential_issues?).and_return(true) - expect(finder).to receive(:count_by_state).and_return(opened: 45) + expect(issues_finder).to receive(:user_can_see_all_confidential_issues?).and_return(true) + expect(issues_finder).to receive(:count_by_state).and_return(opened: 45) expect(helper.issuables_state_counter_text(:issues, :opened)) .to include('45') end it 'does not take confidential status into account when searching for merge requests' do - allow(helper).to receive(:params).and_return(params) - expect(finder).to receive(:count_by_state).and_return(opened: 42) - expect(finder).not_to receive(:user_cannot_see_confidential_issues?) - expect(finder).not_to receive(:user_can_see_all_confidential_issues?) + expect(merge_requests_finder).to receive(:count_by_state).and_return(opened: 42) + expect(merge_requests_finder).not_to receive(:user_cannot_see_confidential_issues?) + expect(merge_requests_finder).not_to receive(:user_can_see_all_confidential_issues?) expect(helper.issuables_state_counter_text(:merge_requests, :opened)) .to include('42') end it 'does not take some keys into account in the cache key' do - expect(finder).to receive(:count_by_state).and_return(opened: 42) - expect(helper).to receive(:params).and_return({ + expect(issues_finder).to receive(:count_by_state).and_return(opened: 42) + expect(issues_finder).to receive(:params).and_return({ author_id: '11', state: 'foo', sort: 'foo', @@ -140,8 +138,8 @@ describe IssuablesHelper do expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') - expect(finder).not_to receive(:count_by_state) - expect(helper).to receive(:params).and_return({ + expect(issues_finder).not_to receive(:count_by_state) + expect(issues_finder).to receive(:params).and_return({ author_id: '11', state: 'bar', sort: 'bar', @@ -154,14 +152,14 @@ describe IssuablesHelper do end it 'does not take params order into account in the cache key' do - expect(helper).to receive(:params).and_return('author_id' => '11', 'state' => 'opened') - expect(finder).to receive(:count_by_state).and_return(opened: 42) + expect(issues_finder).to receive(:params).and_return('author_id' => '11', 'state' => 'opened') + expect(issues_finder).to receive(:count_by_state).and_return(opened: 42) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') - expect(helper).to receive(:params).and_return('state' => 'opened', 'author_id' => '11') - expect(finder).not_to receive(:count_by_state) + expect(issues_finder).to receive(:params).and_return('state' => 'opened', 'author_id' => '11') + expect(issues_finder).not_to receive(:count_by_state) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') -- cgit v1.2.1 From cb30edfae5c3557686463ca22eca7ef572c3ac33 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 29 Jun 2017 17:15:49 +0100 Subject: Clarify counter caching for users without project access --- app/finders/issues_finder.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index d20f4475a03..18f60f9a2b6 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -54,6 +54,21 @@ class IssuesFinder < IssuableFinder 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. The 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? -- cgit v1.2.1 From 49b747725ef5932d34ed83e028f77b2d370fd76a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 30 Jun 2017 09:42:17 +0000 Subject: Only verifies top position after the request has finished to account for errors --- app/assets/javascripts/build.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 9974e135022..60103155ce0 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -85,9 +85,8 @@ window.Build = (function () { if (!this.hasBeenScrolled) { this.scrollToBottom(); } - }); - - this.verifyTopPosition(); + }) + .then(() => this.verifyTopPosition()); } Build.prototype.canScroll = function () { @@ -176,7 +175,7 @@ window.Build = (function () { } if ($flashError.length) { - topPostion += $flashError.outerHeight(); + topPostion += $flashError.outerHeight() + prependTopDefault; } this.$buildTrace.css({ @@ -234,7 +233,8 @@ window.Build = (function () { if (!this.hasBeenScrolled) { this.scrollToBottom(); } - }); + }) + .then(() => this.verifyTopPosition()); }, 4000); } else { this.$buildRefreshAnimation.remove(); -- cgit v1.2.1 From 4bd17d4f7427b2f5f950e601d1a21042b30f69b1 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 30 Jun 2017 11:55:27 +0100 Subject: Make issuables_count_for_state public This doesn't need to be public in CE, but EE uses it as such. --- app/helpers/issuables_helper.rb | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 5385ada6dc4..05177e58c5a 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -233,6 +233,18 @@ module IssuablesHelper } 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 def sidebar_gutter_collapsed? @@ -251,18 +263,6 @@ 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 - def issuable_templates(issuable) @issuable_templates ||= case issuable -- cgit v1.2.1 From 9da3076944146444cb864d5db066a766c76b1935 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Fri, 30 Jun 2017 14:47:53 +0200 Subject: Improve support for external issue references --- .../concerns/mentionable/reference_regexes.rb | 2 +- app/models/external_issue.rb | 5 ---- app/models/project.rb | 4 +-- .../project_services/issue_tracker_service.rb | 5 +++- app/models/project_services/jira_service.rb | 2 +- .../adam-external-issue-references-spike.yml | 4 +++ doc/integration/external-issue-tracker.md | 3 ++ doc/user/project/integrations/bugzilla.md | 11 ++++++++ doc/user/project/integrations/redmine.md | 11 ++++++++ lib/banzai/filter/abstract_reference_filter.rb | 15 +--------- .../filter/external_issue_reference_filter.rb | 4 ++- lib/banzai/filter/issue_reference_filter.rb | 32 +-------------------- lib/banzai/reference_parser/issue_parser.rb | 3 -- spec/factories/projects.rb | 2 +- .../filter/external_issue_reference_filter_spec.rb | 4 +-- .../banzai/filter/issue_reference_filter_spec.rb | 25 ---------------- spec/lib/banzai/pipeline/gfm_pipeline_spec.rb | 33 ++++++++++++++++++++++ .../banzai/reference_parser/issue_parser_spec.rb | 10 ------- spec/models/project_services/jira_service_spec.rb | 6 ++-- .../project_services/redmine_service_spec.rb | 4 +-- spec/services/git_push_service_spec.rb | 12 -------- .../issue_tracker_service_shared_example.rb | 8 +++--- 22 files changed, 87 insertions(+), 118 deletions(-) create mode 100644 changelogs/unreleased/adam-external-issue-references-spike.yml create mode 100644 spec/lib/banzai/pipeline/gfm_pipeline_spec.rb diff --git a/app/models/concerns/mentionable/reference_regexes.rb b/app/models/concerns/mentionable/reference_regexes.rb index 1848230ec7e..2d86a70c395 100644 --- a/app/models/concerns/mentionable/reference_regexes.rb +++ b/app/models/concerns/mentionable/reference_regexes.rb @@ -14,7 +14,7 @@ module Mentionable end EXTERNAL_PATTERN = begin - issue_pattern = ExternalIssue.reference_pattern + issue_pattern = IssueTrackerService.reference_pattern link_patterns = URI.regexp(%w(http https)) reference_pattern(link_patterns, issue_pattern) end diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index e63f89a9f85..0bf18e529f0 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -38,11 +38,6 @@ class ExternalIssue @project.id end - # Pattern used to extract `JIRA-123` issue references from text - def self.reference_pattern - @reference_pattern ||= %r{(?\b([A-Z][A-Z0-9_]+-)\d+)} - end - def to_reference(_from_project = nil, full: nil) id end diff --git a/app/models/project.rb b/app/models/project.rb index a75c5209955..8140ed3763f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -727,8 +727,8 @@ class Project < ActiveRecord::Base end end - def issue_reference_pattern - issues_tracker.reference_pattern + def external_issue_reference_pattern + external_issue_tracker.class.reference_pattern end def default_issues_tracker? diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index ff138b9066d..fcc7c4bec06 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -5,7 +5,10 @@ class IssueTrackerService < Service # Pattern used to extract links from comments # 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})(?\d+)} end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 2450fb43212..00328892b4a 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -18,7 +18,7 @@ class JiraService < IssueTrackerService end # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 - def reference_pattern + def self.reference_pattern @reference_pattern ||= %r{(?\b([A-Z][A-Z0-9_]+-)\d+)} end diff --git a/changelogs/unreleased/adam-external-issue-references-spike.yml b/changelogs/unreleased/adam-external-issue-references-spike.yml new file mode 100644 index 00000000000..aeec6688425 --- /dev/null +++ b/changelogs/unreleased/adam-external-issue-references-spike.yml @@ -0,0 +1,4 @@ +--- +title: Improve support for external issue references +merge_request: 12485 +author: diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 265c891cf83..2dd9b33273c 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -8,6 +8,9 @@ you to do the following: issue index of the external tracker - clicking **New issue** on the project dashboard creates a new issue on the external tracker +- you can reference these external issues inside GitLab interface + (merge requests, commits, comments) and they will be automatically converted + into links ## Configuration diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md index 0b219e84478..6a040516231 100644 --- a/doc/user/project/integrations/bugzilla.md +++ b/doc/user/project/integrations/bugzilla.md @@ -16,3 +16,14 @@ Once you have configured and enabled Bugzilla: - the **Issues** link on the GitLab project pages takes you to the appropriate Bugzilla product page - clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue + +## Referencing issues in Bugzilla + +Issues in Bugzilla can be referenced in two alternative ways: +1. `#` where `` is a number (example `#143`) +2. `-` where `` starts with a capital letter which is + then followed by capital letters, numbers or underscores, and `` is + a number (example `API_32-143`). + +Please note that `` part is ignored and links always point to the +address specified in `issues_url`. diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md index 89c0312d3c2..8026f1f57bc 100644 --- a/doc/user/project/integrations/redmine.md +++ b/doc/user/project/integrations/redmine.md @@ -21,3 +21,14 @@ Once you have configured and enabled Redmine: As an example, below is a configuration for a project named gitlab-ci. ![Redmine configuration](img/redmine_configuration.png) + +## Referencing issues in Redmine + +Issues in Redmine can be referenced in two alternative ways: +1. `#` where `` is a number (example `#143`) +2. `-` where `` starts with a capital letter which is + then followed by capital letters, numbers or underscores, and `` is + a number (example `API_32-143`). + +Please note that `` part is ignored and links always point to the +address specified in `issues_url`. diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 8bc2dd18bda..7a262dd025c 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -216,12 +216,7 @@ module Banzai @references_per_project ||= begin refs = Hash.new { |hash, key| hash[key] = Set.new } - regex = - if uses_reference_pattern? - Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern) - else - object_class.link_reference_pattern - end + regex = Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern) nodes.each do |node| node.to_html.scan(regex) do @@ -323,14 +318,6 @@ module Banzai value end end - - # There might be special cases like filters - # that should ignore reference pattern - # eg: IssueReferenceFilter when using a external issues tracker - # In those cases this method should be overridden on the filter subclass - def uses_reference_pattern? - true - end end end end diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb index dce4de3ceaf..53a229256a5 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -3,6 +3,8 @@ module Banzai # HTML filter that replaces external issue tracker references with links. # References are ignored if the project doesn't use an external issue # tracker. + # + # This filter does not support cross-project references. class ExternalIssueReferenceFilter < ReferenceFilter self.reference_type = :external_issue @@ -87,7 +89,7 @@ module Banzai end def issue_reference_pattern - external_issues_cached(:issue_reference_pattern) + external_issues_cached(:external_issue_reference_pattern) end private diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb index 044d18ff824..ba1a5ac84b3 100644 --- a/lib/banzai/filter/issue_reference_filter.rb +++ b/lib/banzai/filter/issue_reference_filter.rb @@ -15,10 +15,6 @@ module Banzai Issue end - def uses_reference_pattern? - context[:project].default_issues_tracker? - end - def find_object(project, iid) issues_per_project[project][iid] end @@ -38,13 +34,7 @@ module Banzai projects_per_reference.each do |path, project| issue_ids = references_per_project[path] - - issues = - if project.default_issues_tracker? - project.issues.where(iid: issue_ids.to_a) - else - issue_ids.map { |id| ExternalIssue.new(id, project) } - end + issues = project.issues.where(iid: issue_ids.to_a) issues.each do |issue| hash[project][issue.iid.to_i] = issue @@ -55,26 +45,6 @@ module Banzai end end - def object_link_title(object) - if object.is_a?(ExternalIssue) - "Issue in #{object.project.external_issue_tracker.title}" - else - super - end - end - - def data_attributes_for(text, project, object, link: false) - if object.is_a?(ExternalIssue) - data_attribute( - project: project.id, - external_issue: object.id, - reference_type: ExternalIssueReferenceFilter.reference_type - ) - else - super - end - end - def projects_relation_for_paths(paths) super(paths).includes(:gitlab_issue_tracker_service) end diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb index 9fd4bd68d43..a65bbe23958 100644 --- a/lib/banzai/reference_parser/issue_parser.rb +++ b/lib/banzai/reference_parser/issue_parser.rb @@ -4,9 +4,6 @@ module Banzai self.reference_type = :issue def nodes_visible_to_user(user, nodes) - # It is not possible to check access rights for external issue trackers - return nodes if project && project.external_issue_tracker - issues = issues_for_nodes(nodes) readable_issues = Ability diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index aef1c17a239..1bb2db11e7f 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -220,7 +220,7 @@ FactoryGirl.define do active: true, properties: { 'project_url' => 'http://redmine/projects/project_name_in_redmine', - 'issues_url' => "http://redmine/#{project.id}/project_name_in_redmine/:id", + 'issues_url' => 'http://redmine/projects/project_name_in_redmine/issues/:id', 'new_issue_url' => 'http://redmine/projects/project_name_in_redmine/issues/new' } ) diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index a4bb043f8f1..b7d82c36ddd 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -88,12 +88,12 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do it 'queries the collection on the first call' do expect_any_instance_of(Project).to receive(:default_issues_tracker?).once.and_call_original - expect_any_instance_of(Project).to receive(:issue_reference_pattern).once.and_call_original + expect_any_instance_of(Project).to receive(:external_issue_reference_pattern).once.and_call_original not_cached = reference_filter.call("look for #{reference}", { project: project }) expect_any_instance_of(Project).not_to receive(:default_issues_tracker?) - expect_any_instance_of(Project).not_to receive(:issue_reference_pattern) + expect_any_instance_of(Project).not_to receive(:external_issue_reference_pattern) cached = reference_filter.call("look for #{reference}", { project: project }) diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index e5c1deb338b..a79d365d6c5 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -39,13 +39,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do let(:reference) { "##{issue.iid}" } - it 'ignores valid references when using non-default tracker' do - allow(project).to receive(:default_issues_tracker?).and_return(false) - - exp = act = "Issue #{reference}" - expect(reference_filter(act).to_html).to eq exp - end - it 'links to a valid reference' do doc = reference_filter("Fixed #{reference}") @@ -340,24 +333,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do .to eq({ project => { issue.iid => issue } }) end end - - context 'using an external issue tracker' do - it 'returns a Hash containing the issues per project' do - doc = Nokogiri::HTML.fragment('') - filter = described_class.new(doc, project: project) - - expect(project).to receive(:default_issues_tracker?).and_return(false) - - expect(filter).to receive(:projects_per_reference) - .and_return({ project.path_with_namespace => project }) - - expect(filter).to receive(:references_per_project) - .and_return({ project.path_with_namespace => Set.new([1]) }) - - expect(filter.issues_per_project[project][1]) - .to be_an_instance_of(ExternalIssue) - end - end end describe '.references_in' do diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb new file mode 100644 index 00000000000..2b8c76f2bb8 --- /dev/null +++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' + +describe Banzai::Pipeline::GfmPipeline do + describe 'integration between parsing regular and external issue references' do + let(:project) { create(:redmine_project, :public) } + + it 'allows to use shorthand external reference syntax for Redmine' do + markdown = '#12' + + result = described_class.call(markdown, project: project)[:output] + link = result.css('a').first + + expect(link['href']).to eq 'http://redmine/projects/project_name_in_redmine/issues/12' + end + + it 'parses cross-project references to regular issues' do + other_project = create(:empty_project, :public) + issue = create(:issue, project: other_project) + markdown = issue.to_reference(project, full: true) + + result = described_class.call(markdown, project: project)[:output] + link = result.css('a').first + + expect(link['href']).to eq( + Gitlab::Routing.url_helpers.namespace_project_issue_path( + other_project.namespace, + other_project, + issue + ) + ) + end + end +end diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb index 58e1a0c1bc1..acdd23f81f3 100644 --- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -39,16 +39,6 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do expect(subject.nodes_visible_to_user(user, [link])).to eq([]) end end - - context 'when the project uses an external issue tracker' do - it 'returns all nodes' do - link = double(:link) - - expect(project).to receive(:external_issue_tracker).and_return(true) - - expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) - end - end end describe '#referenced_by' do diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index c86f56c55eb..4a1de76f099 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -64,12 +64,12 @@ describe JiraService, models: true do end end - describe '#reference_pattern' do + describe '.reference_pattern' do it_behaves_like 'allows project key on reference pattern' it 'does not allow # on the code' do - expect(subject.reference_pattern.match('#123')).to be_nil - expect(subject.reference_pattern.match('1#23#12')).to be_nil + expect(described_class.reference_pattern.match('#123')).to be_nil + expect(described_class.reference_pattern.match('1#23#12')).to be_nil end end diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb index 6631d9040b1..441b3f896ca 100644 --- a/spec/models/project_services/redmine_service_spec.rb +++ b/spec/models/project_services/redmine_service_spec.rb @@ -31,11 +31,11 @@ describe RedmineService, models: true do end end - describe '#reference_pattern' do + describe '.reference_pattern' do it_behaves_like 'allows project key on reference pattern' it 'does allow # on the reference' do - expect(subject.reference_pattern.match('#123')[:issue]).to eq('123') + expect(described_class.reference_pattern.match('#123')[:issue]).to eq('123') end end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index ca827fc0f39..8e8816870e1 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -401,18 +401,6 @@ describe GitPushService, services: true do expect(SystemNoteService).not_to receive(:cross_reference) execute_service(project, commit_author, @oldrev, @newrev, @ref ) end - - it "doesn't close issues when external issue tracker is in use" do - allow_any_instance_of(Project).to receive(:default_issues_tracker?) - .and_return(false) - external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid, reference_pattern: project.issue_reference_pattern) - allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(external_issue_tracker) - - # The push still shouldn't create cross-reference notes. - expect do - execute_service(project, commit_author, @oldrev, @newrev, 'refs/heads/hurf' ) - end.not_to change { Note.where(project_id: project.id, system: true).count } - end end context "to non-default branches" do diff --git a/spec/support/issue_tracker_service_shared_example.rb b/spec/support/issue_tracker_service_shared_example.rb index e70b3963d9d..a6ab03cb808 100644 --- a/spec/support/issue_tracker_service_shared_example.rb +++ b/spec/support/issue_tracker_service_shared_example.rb @@ -8,15 +8,15 @@ end RSpec.shared_examples 'allows project key on reference pattern' do |url_attr| it 'allows underscores in the project name' do - expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' end it 'allows numbers in the project name' do - expect(subject.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' + expect(described_class.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' end it 'requires the project name to begin with A-Z' do - expect(subject.reference_pattern.match('3EXT_EXT-1234')).to eq nil - expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + expect(described_class.reference_pattern.match('3EXT_EXT-1234')).to eq nil + expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' end end -- cgit v1.2.1 From b3498d88c1dd8e1c0d6880383148a3d818fd1145 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 30 Jun 2017 15:03:35 +0200 Subject: Use Gitaly 0.14.0 --- GITALY_SERVER_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 54d1a4f2a4a..a803cc227fe 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.13.0 +0.14.0 -- cgit v1.2.1 From 2d52a628867c4c718413d0b93835f89b084d1693 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Fri, 30 Jun 2017 13:20:09 +0000 Subject: Resolve "More actions dropdown hidden by end of diff" --- app/assets/stylesheets/pages/diff.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 631649b363f..55011e8a21b 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -20,8 +20,6 @@ } .diff-content { - overflow: auto; - overflow-y: hidden; background: $white-light; color: $gl-text-color; border-radius: 0 0 3px 3px; -- cgit v1.2.1 From 1a449b24a22283aeffa08603d2cff00db10d7fe5 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Fri, 30 Jun 2017 14:35:31 +0100 Subject: Update CHANGELOG.md for 9.3.3 [ci skip] --- CHANGELOG.md | 9 +++++++++ changelogs/unreleased/dm-dependency-linker-newlines.yml | 5 ----- changelogs/unreleased/fix-34417.yml | 4 ---- changelogs/unreleased/fix-head-pipeline-for-commit-status.yml | 4 ---- changelogs/unreleased/highest-return-on-diff-investment.yml | 4 ---- changelogs/unreleased/issue-boards-closed-list-all.yml | 4 ---- changelogs/unreleased/issue-form-multiple-line-markdown.yml | 4 ---- 7 files changed, 9 insertions(+), 25 deletions(-) delete mode 100644 changelogs/unreleased/dm-dependency-linker-newlines.yml delete mode 100644 changelogs/unreleased/fix-34417.yml delete mode 100644 changelogs/unreleased/fix-head-pipeline-for-commit-status.yml delete mode 100644 changelogs/unreleased/highest-return-on-diff-investment.yml delete mode 100644 changelogs/unreleased/issue-boards-closed-list-all.yml delete mode 100644 changelogs/unreleased/issue-form-multiple-line-markdown.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index f372cbf91e8..7591559da22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ documentation](doc/development/changelog.md) for instructions on adding your own 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) - API: Fix optional arugments for POST :id/variables. !12474 diff --git a/changelogs/unreleased/dm-dependency-linker-newlines.yml b/changelogs/unreleased/dm-dependency-linker-newlines.yml deleted file mode 100644 index 5631095fcb7..00000000000 --- a/changelogs/unreleased/dm-dependency-linker-newlines.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix diff of requirements.txt file by not matching newlines as part of package - names -merge_request: -author: diff --git a/changelogs/unreleased/fix-34417.yml b/changelogs/unreleased/fix-34417.yml deleted file mode 100644 index 5f012ad0c81..00000000000 --- a/changelogs/unreleased/fix-34417.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Perform housekeeping only when an import of a fresh project is completed -merge_request: -author: diff --git a/changelogs/unreleased/fix-head-pipeline-for-commit-status.yml b/changelogs/unreleased/fix-head-pipeline-for-commit-status.yml deleted file mode 100644 index f12e7b53790..00000000000 --- a/changelogs/unreleased/fix-head-pipeline-for-commit-status.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix head pipeline stored in merge request for external pipelines -merge_request: 12478 -author: diff --git a/changelogs/unreleased/highest-return-on-diff-investment.yml b/changelogs/unreleased/highest-return-on-diff-investment.yml deleted file mode 100644 index c8be1e0ff8f..00000000000 --- a/changelogs/unreleased/highest-return-on-diff-investment.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Bring back branches badge to main project page -merge_request: 12548 -author: diff --git a/changelogs/unreleased/issue-boards-closed-list-all.yml b/changelogs/unreleased/issue-boards-closed-list-all.yml deleted file mode 100644 index 7643864150d..00000000000 --- a/changelogs/unreleased/issue-boards-closed-list-all.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed issue boards closed list not showing all closed issues -merge_request: -author: diff --git a/changelogs/unreleased/issue-form-multiple-line-markdown.yml b/changelogs/unreleased/issue-form-multiple-line-markdown.yml deleted file mode 100644 index 23128f346bc..00000000000 --- a/changelogs/unreleased/issue-form-multiple-line-markdown.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed multi-line markdown tooltip buttons in issue edit form -merge_request: -author: -- cgit v1.2.1 From 728be6af7c46bf57f2d4a3dd2f1d5dd9ec23bd4b Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Sun, 18 Jun 2017 22:30:58 -0400 Subject: Make setSidebarHeight more efficient with SidebarHeightManager. --- .../javascripts/issuable_bulk_update_sidebar.js | 26 ++--------------- app/assets/javascripts/right_sidebar.js | 25 ++-------------- app/assets/javascripts/sidebar_height_manager.js | 33 ++++++++++++++++++++++ 3 files changed, 38 insertions(+), 46 deletions(-) create mode 100644 app/assets/javascripts/sidebar_height_manager.js diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index a8856120c5e..4f376599ba9 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -5,6 +5,7 @@ /* global SubscriptionSelect */ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; +import SidebarHeightManager from './sidebar_height_manager'; const HIDDEN_CLASS = 'hidden'; const DISABLED_CONTENT_CLASS = 'disabled-content'; @@ -56,18 +57,6 @@ export default class IssuableBulkUpdateSidebar { 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() { IssuableBulkUpdateActions.setOriginalDropdownData(); } @@ -97,7 +86,7 @@ export default class IssuableBulkUpdateSidebar { this.toggleCheckboxDisplay(enable); if (enable) { - this.initSidebar(); + SidebarHeightManager.init(); } } @@ -143,17 +132,6 @@ export default class IssuableBulkUpdateSidebar { 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() { const $checkedIssues = $('.selected_issue:checked'); diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index b5cd01044a3..d8f1fe10b26 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -1,6 +1,7 @@ /* 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 SidebarHeightManager from './sidebar_height_manager'; (function() { this.Sidebar = (function() { @@ -8,12 +9,6 @@ import Cookies from 'js-cookie'; this.toggleTodo = this.toggleTodo.bind(this); this.sidebar = $('aside'); - this.$sidebarInner = this.sidebar.find('.issuable-sidebar'); - this.$navGitlab = $('.navbar-gitlab'); - this.$layoutNav = $('.layout-nav'); - this.$subScroll = $('.sub-nav-scroll'); - this.$rightSidebar = $('.js-right-sidebar'); - this.removeListeners(); this.addEventListeners(); } @@ -27,16 +22,14 @@ import Cookies from 'js-cookie'; }; Sidebar.prototype.addEventListeners = function() { + SidebarHeightManager.init(); const $document = $(document); - const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20); - const slowerThrottledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 200); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); - $(window).on('resize', () => throttledSetSidebarHeight()); - $document.on('scroll', () => slowerThrottledSetSidebarHeight()); + $document.on('click', '.js-sidebar-toggle', function(e, triggered) { var $allGutterToggleIcons, $this, $thisIcon; e.preventDefault(); @@ -214,18 +207,6 @@ import Cookies from 'js-cookie'; } }; - Sidebar.prototype.setSidebarHeight = function() { - const $navHeight = this.$navGitlab.outerHeight() + this.$layoutNav.outerHeight() + (this.$subScroll ? this.$subScroll.outerHeight() : 0); - 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() { return this.sidebar.is('.right-sidebar-expanded'); }; diff --git a/app/assets/javascripts/sidebar_height_manager.js b/app/assets/javascripts/sidebar_height_manager.js new file mode 100644 index 00000000000..022415f22b2 --- /dev/null +++ b/app/assets/javascripts/sidebar_height_manager.js @@ -0,0 +1,33 @@ +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; + } + }, +}; + -- cgit v1.2.1 From 9e3ef082be2595921319279ec095c2765a66e9e9 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 30 Jun 2017 14:10:09 +0000 Subject: Remove placeholder note when award emoji slash command is applied --- app/assets/javascripts/notes.js | 4 ++++ spec/javascripts/notes_spec.js | 45 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 46d77b31ffd..194d1730f3d 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -337,6 +337,10 @@ export default class Notes { if (!noteEntity.valid) { 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.refresh(); } diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 5ece4ed080b..2c096ed08a8 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -523,6 +523,51 @@ import '~/notes'; }); }); + describe('postComment with Slash commands', () => { + const sampleComment = '/assign @root\n/award :100:'; + const note = { + commands_changes: { + assignee_id: 1, + emoji_award: '100' + }, + errors: { + commands_only: ['Commands applied'] + }, + valid: false + }; + let $form; + let $notesContainer; + + beforeEach(() => { + this.notes = new Notes('', []); + window.gon.current_username = 'root'; + window.gon.current_user_fullname = 'Administrator'; + gl.awardsHandler = { + addAwardToEmojiBar: () => {}, + scrollToAwards: () => {} + }; + gl.GfmAutoComplete = { + dataSources: { + commands: '/root/test-project/autocomplete_sources/commands' + } + }; + $form = $('form.js-main-target-form'); + $notesContainer = $('ul.main-notes-list'); + $form.find('textarea.js-note-text').val(sampleComment); + }); + + it('should remove slash command placeholder when comment with slash commands is done posting', () => { + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough(); + $('.js-comment-button').click(); + + expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown + deferred.resolve(note); + expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed + }); + }); + describe('update comment with script tags', () => { const sampleComment = ''; const updatedComment = ''; -- cgit v1.2.1 From 4e38985b1cc159c4e1582ab34c29d7ec151aef35 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 30 Jun 2017 15:44:21 +0100 Subject: Fix typo in IssuesFinder comment [ci skip] --- app/finders/issues_finder.rb | 2 +- changelogs/unreleased/speed-up-issue-counting-for-a-project.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 18f60f9a2b6..85230ff1293 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -60,7 +60,7 @@ class IssuesFinder < IssuableFinder # `user_can_see_all_confidential_issues?`) are more complicated, because they # can see confidential issues where: # 1. They are an assignee. - # 2. The are an author. + # 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 diff --git a/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml b/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml index 493ecbcb77a..6bf03d9a382 100644 --- a/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml +++ b/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml @@ -1,5 +1,5 @@ --- title: Cache open issue and merge request counts for project tabs to speed up project pages -merge_request: +merge_request: 12457 author: -- cgit v1.2.1 From 7541362fe7b8983f1c6ce511dc9b7c727857dfdd Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Fri, 30 Jun 2017 15:48:42 +0000 Subject: Automatically hide sidebar on smaller screens --- app/assets/stylesheets/new_sidebar.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index be4cc02b3ea..17f23f7fce3 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -49,6 +49,7 @@ $new-sidebar-width: 220px; position: fixed; z-index: 400; width: $new-sidebar-width; + transition: width $sidebar-transition-duration; top: 50px; bottom: 0; left: 0; @@ -62,6 +63,8 @@ $new-sidebar-width: 220px; } li { + white-space: nowrap; + a { display: block; padding: 12px 14px; @@ -72,6 +75,10 @@ $new-sidebar-width: 220px; color: $gl-text-color; text-decoration: none; } + + @media (max-width: $screen-xs-max) { + width: 0; + } } .sidebar-sub-level-items { -- cgit v1.2.1 From 4c20ee71a979121d88f90e475825fc03a01e3468 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Fri, 30 Jun 2017 12:20:21 -0400 Subject: Restore timeago translations in renderTimeago. --- app/assets/javascripts/lib/utils/datetime_utility.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 034a1ec2054..1d1763c3963 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -116,7 +116,7 @@ window.dateFormat = dateFormat; const timeagoEls = $els || document.querySelectorAll('.js-timeago-render'); // timeago.js sets timeouts internally for each timeago value to be updated in real time - gl.utils.getTimeago().render(timeagoEls); + gl.utils.getTimeago().render(timeagoEls, lang); }; w.gl.utils.getDayDifference = function(a, b) { -- cgit v1.2.1 From ec396fd93ab800e8b962f7b8bc6095f8ab754d93 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 30 Jun 2017 16:52:11 +0000 Subject: New navigation breadcrumbs --- app/assets/javascripts/group_name.js | 23 ++-- app/assets/stylesheets/new_nav.scss | 124 +++++++++++++++++++++ app/helpers/groups_helper.rb | 21 +++- app/helpers/projects_helper.rb | 12 +- app/views/dashboard/activity.html.haml | 1 + app/views/dashboard/groups/index.html.haml | 1 + app/views/dashboard/milestones/index.html.haml | 1 + app/views/dashboard/projects/index.html.haml | 2 + app/views/dashboard/snippets/index.html.haml | 1 + app/views/layouts/_page.html.haml | 2 + app/views/layouts/header/_default.html.haml | 2 +- app/views/layouts/header/_new.html.haml | 2 - app/views/layouts/nav/_breadcrumbs.html.haml | 19 ++++ app/views/projects/boards/_show.html.haml | 4 + app/views/projects/issues/_nav_btns.html.haml | 11 ++ app/views/projects/issues/index.html.haml | 19 +--- .../projects/merge_requests/_nav_btns.html.haml | 5 + app/views/projects/merge_requests/index.html.haml | 16 ++- spec/helpers/groups_helper_spec.rb | 2 +- 19 files changed, 232 insertions(+), 36 deletions(-) create mode 100644 app/views/layouts/nav/_breadcrumbs.html.haml create mode 100644 app/views/projects/issues/_nav_btns.html.haml create mode 100644 app/views/projects/merge_requests/_nav_btns.html.haml diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js index 462d792b8d5..37c6765d942 100644 --- a/app/assets/javascripts/group_name.js +++ b/app/assets/javascripts/group_name.js @@ -1,13 +1,13 @@ - +import Cookies from 'js-cookie'; import _ from 'underscore'; export default class GroupName { constructor() { - this.titleContainer = document.querySelector('.title-container'); - this.title = document.querySelector('.title'); + this.titleContainer = document.querySelector('.js-title-container'); + this.title = this.titleContainer.querySelector('.title'); this.titleWidth = this.title.offsetWidth; - this.groupTitle = document.querySelector('.group-title'); - this.groups = document.querySelectorAll('.group-path'); + this.groupTitle = this.titleContainer.querySelector('.group-title'); + this.groups = this.titleContainer.querySelectorAll('.group-path'); this.toggle = null; this.isHidden = false; this.init(); @@ -33,11 +33,20 @@ export default class GroupName { createToggle() { this.toggle = document.createElement('button'); + this.toggle.setAttribute('type', 'button'); this.toggle.className = 'text-expander group-name-toggle'; this.toggle.setAttribute('aria-label', 'Toggle full path'); - this.toggle.innerHTML = '...'; + if (Cookies.get('new_nav') === 'true') { + this.toggle.innerHTML = ''; + } else { + this.toggle.innerHTML = '...'; + } 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(); } diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index 3ce5b4fd073..bfb7a0c7e25 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -264,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; + } +} diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index eb45241615f..af0b3e9c5bc 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -16,11 +16,12 @@ module GroupsHelper full_title = '' 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 += ' / '.html_safe end - full_title += link_to(simple_sanitize(group.name), group_path(group), class: 'group-path') + full_title += group_title_link(group) full_title += ' · '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name content_tag :span, class: 'group-title' do @@ -56,4 +57,20 @@ module GroupsHelper def group_issues(group) IssuesFinder.new(current_user, group_id: group.id).execute 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 diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c04b1419a19..53d95c2de94 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -58,7 +58,17 @@ module ProjectsHelper link_to(simple_sanitize(owner.name), user_path(owner)) 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 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 diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml index f893c3e1675..ad35d05c29a 100644 --- a/app/views/dashboard/activity.html.haml +++ b/app/views/dashboard/activity.html.haml @@ -1,3 +1,4 @@ +- @hide_top_links = true - @no_container = true = content_for :meta_tags do diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index f9b45a539a1..1cea8182733 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -1,3 +1,4 @@ +- @hide_top_links = true - page_title "Groups" - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index 664ec618b79..ef1467c4d78 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -1,3 +1,4 @@ +- @hide_top_links = true - page_title 'Milestones' - header_title 'Milestones', dashboard_milestones_path diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index 5e63a61e21b..7ac6cf06fb9 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -1,4 +1,6 @@ - @no_container = true +- @hide_top_links = true +- @breadcrumb_title = "Projects" = content_for :meta_tags do = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity") diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 85cbe0bf0e6..e86b1ab3116 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -1,3 +1,4 @@ +- @hide_top_links = true - page_title "Snippets" - header_title "Snippets", dashboard_snippets_path diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 62a76a1b00e..1a9f5401a78 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -14,6 +14,8 @@ = render "layouts/broadcast" = render "layouts/flash" = yield :flash_message + - if show_new_nav? + = render "layouts/nav/breadcrumbs" %div{ class: "#{(container_class unless @no_container)} #{@content_class}" } .content{ id: "content-body" } = yield diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index f056c0af968..8cbc3f6105f 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -17,7 +17,7 @@ = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do = brand_header_logo - .title-container + .title-container.js-title-container %h1.title{ class: ('initializing' if @has_group_title) }= title .navbar-collapse.collapse diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml index c0833c64911..5859f689dd1 100644 --- a/app/views/layouts/header/_new.html.haml +++ b/app/views/layouts/header/_new.html.haml @@ -83,8 +83,6 @@ = icon('ellipsis-v', class: 'js-navbar-toggle-right') = icon('times', class: 'js-navbar-toggle-left', style: 'display: none;') - = yield :header_content - = render 'shared/outdated_browser' - if @project && !@project.empty_repo? diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml new file mode 100644 index 00000000000..5f1641f4300 --- /dev/null +++ b/app/views/layouts/nav/_breadcrumbs.html.haml @@ -0,0 +1,19 @@ +- 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 diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index 6684ecfce81..3622720a8b7 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -2,6 +2,10 @@ - @content_class = "issue-boards-content" - 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 = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'filtered_search' diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml new file mode 100644 index 00000000000..698959ec74f --- /dev/null +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -0,0 +1,11 @@ += 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" diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 7183794ce72..89ac5ff7128 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -13,23 +13,16 @@ = content_for :meta_tags do = 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? %div{ class: (container_class) } .top-area = render 'shared/issuable/nav', type: :issues - .nav-controls - = link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do - = 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 + .nav-controls{ class: ("visible-xs" if show_new_nav?) } + = render "projects/issues/nav_btns" = render 'shared/issuable/search_bar', type: :issues - if @can_bulk_update diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml new file mode 100644 index 00000000000..e92f2712347 --- /dev/null +++ b/app/views/projects/merge_requests/_nav_btns.html.haml @@ -0,0 +1,5 @@ +- 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 diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 1e30cc09894..6fe44ba3c3d 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,5 +1,7 @@ - @no_container = true - @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" - unless @project.default_issues_tracker? @@ -10,22 +12,18 @@ = webpack_bundle_tag 'common_vue' = 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' -- 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 - - if @project.merge_requests.exists? %div{ class: container_class } .top-area = render 'shared/issuable/nav', type: :merge_requests - .nav-controls - - 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 + .nav-controls{ class: ("visible-xs" if show_new_nav?) } + = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path = render 'shared/issuable/search_bar', type: :merge_requests diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 8da22dc78fa..e3f9d9db9eb 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -91,7 +91,7 @@ describe GroupsHelper do let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } it 'outputs the groups in the correct order' do - expect(group_title(very_deep_nested_group)).to match(/>#{group.name}<\/a>.*>#{nested_group.name}<\/a>.*>#{deep_nested_group.name}<\/a>/) + expect(helper.group_title(very_deep_nested_group)).to match(/>#{group.name}<\/a>.*>#{nested_group.name}<\/a>.*>#{deep_nested_group.name}<\/a>/) end end end -- cgit v1.2.1 From 8fd0887fe4ab1c8ccee2fe023af2b6b2afd22ecc Mon Sep 17 00:00:00 2001 From: tauriedavis Date: Fri, 30 Jun 2017 09:52:59 -0700 Subject: Add issuable-list class to shared mr/issue lists to fix new responsive layout --- app/views/shared/_issues.html.haml | 2 +- app/views/shared/_merge_requests.html.haml | 2 +- changelogs/unreleased/fix-assigned-issuable-lists.yml | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/fix-assigned-issuable-lists.yml diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml index 3a49227961f..49555b6ff4e 100644 --- a/app/views/shared/_issues.html.haml +++ b/app/views/shared/_issues.html.haml @@ -1,6 +1,6 @@ - if @issues.to_a.any? .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 = paginate @issues, theme: "gitlab" - else diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml index eecbb32e90e..0517896cfbd 100644 --- a/app/views/shared/_merge_requests.html.haml +++ b/app/views/shared/_merge_requests.html.haml @@ -1,6 +1,6 @@ - if @merge_requests.to_a.any? .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 = paginate @merge_requests, theme: "gitlab" diff --git a/changelogs/unreleased/fix-assigned-issuable-lists.yml b/changelogs/unreleased/fix-assigned-issuable-lists.yml new file mode 100644 index 00000000000..fc2cd18ddb6 --- /dev/null +++ b/changelogs/unreleased/fix-assigned-issuable-lists.yml @@ -0,0 +1,5 @@ +--- +title: Add issuable-list class to shared mr/issue lists to fix new responsive layout + design +merge_request: +author: -- cgit v1.2.1 From dcdf2a8bc58e5a9750f3a4dfdb4b1795863b3853 Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Wed, 28 Jun 2017 21:30:38 +0200 Subject: Make entrypoint and command keys to be array of strings --- lib/gitlab/ci/config/entry/image.rb | 2 +- lib/gitlab/ci/config/entry/service.rb | 4 ++-- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 25 +++++++++++++++++-------- spec/lib/gitlab/ci/config/entry/image_spec.rb | 4 ++-- spec/lib/gitlab/ci/config/entry/service_spec.rb | 6 +++--- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb index 897dcff8012..6555c589173 100644 --- a/lib/gitlab/ci/config/entry/image.rb +++ b/lib/gitlab/ci/config/entry/image.rb @@ -15,7 +15,7 @@ module Gitlab validates :config, allowed_keys: ALLOWED_KEYS validates :name, type: String, presence: true - validates :entrypoint, type: String, allow_nil: true + validates :entrypoint, array_of_strings: true, allow_nil: true end def hash? diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb index b52faf48b58..3e2ebcff31a 100644 --- a/lib/gitlab/ci/config/entry/service.rb +++ b/lib/gitlab/ci/config/entry/service.rb @@ -15,8 +15,8 @@ module Gitlab validates :config, allowed_keys: ALLOWED_KEYS validates :name, type: String, presence: true - validates :entrypoint, type: String, allow_nil: true - validates :command, type: String, allow_nil: true + validates :entrypoint, array_of_strings: true, allow_nil: true + validates :command, array_of_strings: true, allow_nil: true validates :alias, type: String, allow_nil: true end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index af0e7855a9b..e02317adbad 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -598,8 +598,10 @@ module Ci describe "Image and service handling" do context "when extended docker configuration is used" do it "returns image and service when defined" do - config = YAML.dump({ image: { name: "ruby:2.1" }, - services: ["mysql", { name: "docker:dind", alias: "docker" }], + config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, + services: ["mysql", { name: "docker:dind", alias: "docker", + entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }], before_script: ["pwd"], rspec: { script: "rspec" } }) @@ -614,8 +616,10 @@ module Ci coverage_regex: nil, tag_list: [], options: { - image: { name: "ruby:2.1" }, - services: [{ name: "mysql" }, { name: "docker:dind", alias: "docker" }] + image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "mysql" }, + { name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }] }, allow_failure: false, when: "on_success", @@ -628,8 +632,11 @@ module Ci config = YAML.dump({ image: "ruby:2.1", services: ["mysql"], before_script: ["pwd"], - rspec: { image: { name: "ruby:2.5" }, - services: [{ name: "postgresql", alias: "db-pg" }, "docker:dind"], script: "rspec" } }) + rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "postgresql", alias: "db-pg", + entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }, "docker:dind"], + script: "rspec" } }) config_processor = GitlabCiYamlProcessor.new(config, path) @@ -642,8 +649,10 @@ module Ci coverage_regex: nil, tag_list: [], options: { - image: { name: "ruby:2.5" }, - services: [{ name: "postgresql", alias: "db-pg" }, { name: "docker:dind" }] + image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"]}, + { name: "docker:dind" }] }, allow_failure: false, when: "on_success", diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index bca22e39500..d8800fb675b 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -38,7 +38,7 @@ describe Gitlab::Ci::Config::Entry::Image do end context 'when configuration is a hash' do - let(:config) { { name: 'ruby:2.2', entrypoint: '/bin/sh' } } + let(:config) { { name: 'ruby:2.2', entrypoint: ['/bin/sh', 'run'] } } describe '#value' do it 'returns image hash' do @@ -66,7 +66,7 @@ describe Gitlab::Ci::Config::Entry::Image do describe '#entrypoint' do it "returns image's entrypoint" do - expect(entry.entrypoint).to eq '/bin/sh' + expect(entry.entrypoint).to eq ['/bin/sh', 'run'] end end end diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb index 7202fe525e4..24b7086c34c 100644 --- a/spec/lib/gitlab/ci/config/entry/service_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::Entry::Service do context 'when configuration is a hash' do let(:config) do - { name: 'postgresql:9.5', alias: 'db', command: 'cmd', entrypoint: '/bin/sh' } + { name: 'postgresql:9.5', alias: 'db', command: ['cmd', 'run'], entrypoint: ['/bin/sh', 'run'] } end describe '#valid?' do @@ -72,13 +72,13 @@ describe Gitlab::Ci::Config::Entry::Service do describe '#command' do it "returns service's command" do - expect(entry.command).to eq 'cmd' + expect(entry.command).to eq ['cmd', 'run'] end end describe '#entrypoint' do it "returns service's entrypoint" do - expect(entry.entrypoint).to eq '/bin/sh' + expect(entry.entrypoint).to eq ['/bin/sh', 'run'] end end end -- cgit v1.2.1 From 16ff3229cbb71e05f9cea3a16b481a8120dfcb7e Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Fri, 30 Jun 2017 13:30:19 +0200 Subject: Fix rubocop offenses --- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 2 +- spec/lib/gitlab/ci/config/entry/image_spec.rb | 4 ++-- spec/lib/gitlab/ci/config/entry/service_spec.rb | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index e02317adbad..482f03aa0cc 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -651,7 +651,7 @@ module Ci options: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"], - command: ["/usr/local/bin/init", "run"]}, + command: ["/usr/local/bin/init", "run"] }, { name: "docker:dind" }] }, allow_failure: false, diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index d8800fb675b..1a4d9ed5517 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -38,7 +38,7 @@ describe Gitlab::Ci::Config::Entry::Image do end context 'when configuration is a hash' do - let(:config) { { name: 'ruby:2.2', entrypoint: ['/bin/sh', 'run'] } } + let(:config) { { name: 'ruby:2.2', entrypoint: %w(/bin/sh run) } } describe '#value' do it 'returns image hash' do @@ -66,7 +66,7 @@ describe Gitlab::Ci::Config::Entry::Image do describe '#entrypoint' do it "returns image's entrypoint" do - expect(entry.entrypoint).to eq ['/bin/sh', 'run'] + expect(entry.entrypoint).to eq %w(/bin/sh run) end end end diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb index 24b7086c34c..9ebf947a751 100644 --- a/spec/lib/gitlab/ci/config/entry/service_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::Entry::Service do context 'when configuration is a hash' do let(:config) do - { name: 'postgresql:9.5', alias: 'db', command: ['cmd', 'run'], entrypoint: ['/bin/sh', 'run'] } + { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run) } end describe '#valid?' do @@ -72,13 +72,13 @@ describe Gitlab::Ci::Config::Entry::Service do describe '#command' do it "returns service's command" do - expect(entry.command).to eq ['cmd', 'run'] + expect(entry.command).to eq %w(cmd run) end end describe '#entrypoint' do it "returns service's entrypoint" do - expect(entry.entrypoint).to eq ['/bin/sh', 'run'] + expect(entry.entrypoint).to eq %w(/bin/sh run) end end end -- cgit v1.2.1