Commit f7837988 authored by Mike Greiling's avatar Mike Greiling

Merge branch 'master' into sh-headless-chrome-support

* master: (113 commits)
  Introduce new hook data builders for Issue and MergeRequest
  Don't create todos for old issue assignees
  Start adding Gitlab::HookData::IssuableBuilder
  Include the changes in issuable webhook payloads
  Rename the `codeclimate` job to `codequality`
  Don't show an "Unsubscribe" link in snippet comment notifications
  Add QA::Scenario::Gitlab::Group::Create
  Removes CommitsList from global namespace
  Fix wiki empty page translation namespace not being removed
  Fixes mini graph in commit view
  Fix link to new i18n index page
  Update i18n docs
  Move i18n/introduction to i18n/index
  Resolve "Simple documentation update - backup to restore in restore section"
  Remove AjaxLoadingSpinner and CreateLabelDropdown from global namespace
  Move cycle analytics banner into a vue file
  Updated Icons + Fix for Collapsed Groups Angle
  Don't create fork networks for root projects that are deleted
  Remove executable permissions on images to make docs lint happy
  Sync up hard coded DN class in migration
  ...
parents 02838d5b d6170ce4
...@@ -270,6 +270,7 @@ flaky-examples-check: ...@@ -270,6 +270,7 @@ flaky-examples-check:
NEW_FLAKY_SPECS_REPORT: rspec_flaky/report-new.json NEW_FLAKY_SPECS_REPORT: rspec_flaky/report-new.json
stage: post-test stage: post-test
allow_failure: yes allow_failure: yes
retry: 0
only: only:
- branches - branches
except: except:
...@@ -429,6 +430,7 @@ ee_compat_check: ...@@ -429,6 +430,7 @@ ee_compat_check:
- branches@gitlab-org/gitlab-ee - branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee - branches@gitlab/gitlab-ee
allow_failure: yes allow_failure: yes
retry: 0
cache: cache:
key: "ee_compat_check_repo" key: "ee_compat_check_repo"
paths: paths:
...@@ -578,7 +580,7 @@ karma: ...@@ -578,7 +580,7 @@ karma:
- chrome_debug.log - chrome_debug.log
- coverage-javascript/ - coverage-javascript/
codeclimate: codequality:
<<: *except-docs <<: *except-docs
<<: *pull-cache <<: *pull-cache
before_script: [] before_script: []
......
<svg width="24" height="30" viewBox="0 0 24 30" xmlns="http://www.w3.org/2000/svg"><title>cursor</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#1F78D1" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#FFF"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787c-.91 0-1.763.156-2.558.469-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068 0-.009.01-.031.033-.067a.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26.094-.126.168-.24.221-.342.054-.103.114-.235.181-.395.067-.161.125-.33.174-.51-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#1F78D1" fill-rule="nonzero"/></g></svg>
<svg width="48" height="60" viewBox="0 0 48 60" xmlns="http://www.w3.org/2000/svg"><title>cursor_2x</title><g fill="none" fill-rule="evenodd"><path d="M48 24.21C48 37.583 36.522 47.369 24 60 11.478 47.368 0 37.582 0 24.21 0 10.84 10.745 0 24 0s24 10.84 24 24.21z" fill="#1F78D1" fill-rule="nonzero"/><path d="M30.56 50.497c2.915-2.95 5.078-5.268 6.947-7.493 5.703-6.788 8.406-12.53 8.406-18.793 0-12.223-9.815-22.124-21.913-22.124S2.087 11.988 2.087 24.211c0 6.263 2.703 12.005 8.406 18.793 1.87 2.225 4.032 4.544 6.947 7.493 1.022 1.035 4.432 4.426 6.56 6.55 2.128-2.124 5.538-5.515 6.56-6.55z" fill="#FFF"/><path d="M29.103 16.512c-1.58-.625-3.282-.938-5.103-.938-1.821 0-3.527.313-5.116.938-1.58.616-2.84 1.45-3.777 2.504-.928 1.054-1.393 2.192-1.393 3.415 0 1 .317 1.956.951 2.866.643.902 1.545 1.684 2.706 2.344l1.165.67-.362 1.286a9.603 9.603 0 0 1-.937 2.303 13.208 13.208 0 0 0 3.683-2.29l.576-.509.763.08c.616.072 1.196.108 1.741.108 1.821 0 3.522-.308 5.103-.925 1.589-.625 2.848-1.464 3.776-2.517.938-1.054 1.407-2.192 1.407-3.416 0-1.223-.469-2.361-1.407-3.415-.928-1.053-2.187-1.888-3.776-2.504zm5.29 1.62c1.071 1.313 1.607 2.746 1.607 4.3 0 1.553-.536 2.99-1.607 4.312-1.072 1.312-2.527 2.353-4.366 3.12-1.84.76-3.848 1.139-6.027 1.139a18.32 18.32 0 0 1-1.942-.107c-1.768 1.562-3.821 2.643-6.16 3.24-.438.126-.947.224-1.527.295h-.067a.521.521 0 0 1-.362-.147.649.649 0 0 1-.214-.362v-.013c-.027-.036-.032-.09-.014-.16.027-.072.036-.117.027-.135 0-.017.022-.062.067-.133a1.29 1.29 0 0 0 .08-.121c.01-.009.04-.045.094-.107a106.068 106.068 0 0 1 .522-.59c.215-.232.367-.401.456-.508.098-.099.236-.273.415-.523.188-.25.335-.477.442-.683.107-.205.228-.468.362-.79.134-.321.25-.66.348-1.018-1.402-.794-2.51-1.777-3.322-2.946C12.402 25.025 12 23.77 12 22.43c0-1.553.536-2.986 1.607-4.299 1.072-1.321 2.527-2.361 4.366-3.12 1.84-.768 3.848-1.152 6.027-1.152 2.179 0 4.188.384 6.027 1.152 1.84.759 3.294 1.799 4.366 3.12z" fill="#1F78D1" fill-rule="nonzero"/></g></svg>
{"iconCount":135,"spriteSize":58718,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","calendar","cancel","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","comment-dots","comment-next","comment","comments","commit","credit-card","disk","doc_code","doc_image","doc_text","download","duplicate","earth","eye-slash","eye","file-additions","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","star-o","star","stop","talic","task-done","template","thump-down","thump-up","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]} {"iconCount":164,"spriteSize":72823,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","calendar","cancel","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","dashboard","disk","doc_code","doc_image","doc_text","download","duplicate","earth","eye-slash","eye","file-additions","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","talic","task-done","template","thump-down","thump-up","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]}
\ No newline at end of file \ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
const MAX_MESSAGE_LENGTH = 500; const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
class AbuseReports { export default class AbuseReports {
constructor() { constructor() {
$(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage); $(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
$(document) $(document)
...@@ -32,6 +32,3 @@ class AbuseReports { ...@@ -32,6 +32,3 @@ class AbuseReports {
} }
} }
} }
window.gl = window.gl || {};
window.gl.AbuseReports = AbuseReports;
class AjaxLoadingSpinner { export default class AjaxLoadingSpinner {
static init() { static init() {
const $elements = $('.js-ajax-loading-spinner'); const $elements = $('.js-ajax-loading-spinner');
...@@ -30,6 +30,3 @@ class AjaxLoadingSpinner { ...@@ -30,6 +30,3 @@ class AjaxLoadingSpinner {
classList.toggle('fa-spin'); classList.toggle('fa-spin');
} }
} }
window.gl = window.gl || {};
gl.AjaxLoadingSpinner = AjaxLoadingSpinner;
/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var, /* eslint-disable func-names, no-new, space-before-function-paren, one-var,
promise/catch-or-return */ promise/catch-or-return */
import _ from 'underscore'; import _ from 'underscore';
import CreateLabelDropdown from '../../create_label';
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {}; window.gl.issueBoards = window.gl.issueBoards || {};
...@@ -15,15 +16,15 @@ $(document).off('created.label').on('created.label', (e, label) => { ...@@ -15,15 +16,15 @@ $(document).off('created.label').on('created.label', (e, label) => {
label: { label: {
id: label.id, id: label.id,
title: label.title, title: label.title,
color: label.color color: label.color,
} },
}); });
}); });
gl.issueBoards.newListDropdownInit = () => { gl.issueBoards.newListDropdownInit = () => {
$('.js-new-board-list').each(function () { $('.js-new-board-list').each(function () {
const $this = $(this); const $this = $(this);
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path')); new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
$this.glDropdown({ $this.glDropdown({
data(term, callback) { data(term, callback) {
...@@ -38,17 +39,17 @@ gl.issueBoards.newListDropdownInit = () => { ...@@ -38,17 +39,17 @@ gl.issueBoards.newListDropdownInit = () => {
const $a = $('<a />', { const $a = $('<a />', {
class: (active ? `is-active js-board-list-${active.id}` : ''), class: (active ? `is-active js-board-list-${active.id}` : ''),
text: label.title, text: label.title,
href: '#' href: '#',
}); });
const $labelColor = $('<span />', { const $labelColor = $('<span />', {
class: 'dropdown-label-box', class: 'dropdown-label-box',
style: `background-color: ${label.color}` style: `background-color: ${label.color}`,
}); });
return $li.append($a.prepend($labelColor)); return $li.append($a.prepend($labelColor));
}, },
search: { search: {
fields: ['title'] fields: ['title'],
}, },
filterable: true, filterable: true,
selectable: true, selectable: true,
...@@ -66,13 +67,13 @@ gl.issueBoards.newListDropdownInit = () => { ...@@ -66,13 +67,13 @@ gl.issueBoards.newListDropdownInit = () => {
label: { label: {
id: label.id, id: label.id,
title: label.title, title: label.title,
color: label.color color: label.color,
} },
}); });
Store.state.lists = _.sortBy(Store.state.lists, 'position'); Store.state.lists = _.sortBy(Store.state.lists, 'position');
} }
} },
}); });
}); });
}; };
/* eslint-disable func-names, space-before-function-paren, wrap-iife */
/* global CommitFile */
window.Commit = (function() {
function Commit() {
$('.files .diff-file').each(function() {
return new CommitFile(this);
});
}
return Commit;
})();
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new */
/* global ImageFile */
(function() {
this.CommitFile = (function() {
function CommitFile(file) {
if ($('.image', file).length) {
new gl.ImageFile(file);
}
}
return CommitFile;
})();
}).call(window);
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
import 'vendor/jquery.waitforimages';
(function() { (function() {
gl.ImageFile = (function() { gl.ImageFile = (function() {
var prepareFrames; var prepareFrames;
...@@ -17,15 +19,10 @@ ...@@ -17,15 +19,10 @@
// Load two-up view after images are loaded // Load two-up view after images are loaded
// so that we can display the correct width and height information // so that we can display the correct width and height information
const images = $('.two-up.view img', _this.file); const $images = $('.two-up.view img', _this.file);
let loadedCount = 0;
images.on('load', () => {
loadedCount += 1;
if (loadedCount === images.length) { $images.waitForImages(function() {
_this.initView('two-up'); _this.initView('two-up');
}
}); });
}); });
}; };
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, max-len, prefer-arrow-callback */ /* eslint-disable func-names, wrap-iife, consistent-return,
no-return-assign, no-param-reassign, one-var-declaration-per-line, no-unused-vars,
prefer-template, object-shorthand, prefer-arrow-callback */
/* global Pager */ /* global Pager */
window.CommitsList = (function() { export default (function () {
var CommitsList = {}; const CommitsList = {};
CommitsList.timer = null; CommitsList.timer = null;
CommitsList.init = function(limit) { CommitsList.init = function (limit) {
this.$contentList = $('.content_list'); this.$contentList = $('.content_list');
$("body").on("click", ".day-commits-table li.commit", function(e) { $('body').on('click', '.day-commits-table li.commit', function (e) {
if (e.target.nodeName !== "A") { if (e.target.nodeName !== 'A') {
location.href = $(this).attr("url"); location.href = $(this).attr('url');
e.stopPropagation(); e.stopPropagation();
return false; return false;
} }
...@@ -19,48 +21,47 @@ window.CommitsList = (function() { ...@@ -19,48 +21,47 @@ window.CommitsList = (function() {
Pager.init(parseInt(limit, 10), false, false, this.processCommits); Pager.init(parseInt(limit, 10), false, false, this.processCommits);
this.content = $("#commits-list"); this.content = $('#commits-list');
this.searchField = $("#commits-search"); this.searchField = $('#commits-search');
this.lastSearch = this.searchField.val(); this.lastSearch = this.searchField.val();
return this.initSearch(); return this.initSearch();
}; };
CommitsList.initSearch = function() { CommitsList.initSearch = function () {
this.timer = null; this.timer = null;
return this.searchField.keyup((function(_this) { return this.searchField.keyup((function (_this) {
return function() { return function () {
clearTimeout(_this.timer); clearTimeout(_this.timer);
return _this.timer = setTimeout(_this.filterResults, 500); return _this.timer = setTimeout(_this.filterResults, 500);
}; };
})(this)); })(this));
}; };
CommitsList.filterResults = function() { CommitsList.filterResults = function () {
var commitsUrl, form, search; const form = $('.commits-search-form');
form = $(".commits-search-form"); const search = CommitsList.searchField.val();
search = CommitsList.searchField.val();
if (search === CommitsList.lastSearch) return; if (search === CommitsList.lastSearch) return;
commitsUrl = form.attr("action") + '?' + form.serialize(); const commitsUrl = form.attr('action') + '?' + form.serialize();
CommitsList.content.fadeTo('fast', 0.5); CommitsList.content.fadeTo('fast', 0.5);
return $.ajax({ return $.ajax({
type: "GET", type: 'GET',
url: form.attr("action"), url: form.attr('action'),
data: form.serialize(), data: form.serialize(),
complete: function() { complete: function () {
return CommitsList.content.fadeTo('fast', 1.0); return CommitsList.content.fadeTo('fast', 1.0);
}, },
success: function(data) { success: function (data) {
CommitsList.lastSearch = search; CommitsList.lastSearch = search;
CommitsList.content.html(data.html); CommitsList.content.html(data.html);
return history.replaceState({ return history.replaceState({
page: commitsUrl page: commitsUrl,
// Change url so if user reload a page - search results are saved // Change url so if user reload a page - search results are saved
}, document.title, commitsUrl); }, document.title, commitsUrl);
}, },
error: function() { error: function () {
CommitsList.lastSearch = null; CommitsList.lastSearch = null;
}, },
dataType: "json" dataType: 'json',
}); });
}; };
...@@ -81,7 +82,7 @@ window.CommitsList = (function() { ...@@ -81,7 +82,7 @@ window.CommitsList = (function() {
commitsCount = $commitsHeadersLast.nextUntil('li.js-commit-header').find('li.commit').length; commitsCount = $commitsHeadersLast.nextUntil('li.js-commit-header').find('li.commit').length;
// Remove duplicate of commits header. // Remove duplicate of commits header.
processedData = $processedData.not(`li.js-commit-header[data-day="${loadedShownDayFirst}"]`); processedData = $processedData.not(`li.js-commit-header[data-day='${loadedShownDayFirst}']`);
// Update commits count in the previous commits header. // Update commits count in the previous commits header.
commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length); commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length);
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */ /* eslint-disable func-names, prefer-arrow-callback */
import Api from './api'; import Api from './api';
class CreateLabelDropdown { export default class CreateLabelDropdown {
constructor ($el, namespacePath, projectPath) { constructor($el, namespacePath, projectPath) {
this.$el = $el; this.$el = $el;
this.namespacePath = namespacePath; this.namespacePath = namespacePath;
this.projectPath = projectPath; this.projectPath = projectPath;
...@@ -22,7 +22,7 @@ class CreateLabelDropdown { ...@@ -22,7 +22,7 @@ class CreateLabelDropdown {
this.addBinding(); this.addBinding();
} }
cleanBinding () { cleanBinding() {
this.$colorSuggestions.off('click'); this.$colorSuggestions.off('click');
this.$newLabelField.off('keyup change'); this.$newLabelField.off('keyup change');
this.$newColorField.off('keyup change'); this.$newColorField.off('keyup change');
...@@ -31,7 +31,7 @@ class CreateLabelDropdown { ...@@ -31,7 +31,7 @@ class CreateLabelDropdown {
this.$newLabelCreateButton.off('click'); this.$newLabelCreateButton.off('click');
} }
addBinding () { addBinding() {
const self = this; const self = this;
this.$colorSuggestions.on('click', function (e) { this.$colorSuggestions.on('click', function (e) {
...@@ -44,7 +44,7 @@ class CreateLabelDropdown { ...@@ -44,7 +44,7 @@ class CreateLabelDropdown {
this.$dropdownBack.on('click', this.resetForm.bind(this)); this.$dropdownBack.on('click', this.resetForm.bind(this));
this.$cancelButton.on('click', function(e) { this.$cancelButton.on('click', function (e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
...@@ -55,7 +55,7 @@ class CreateLabelDropdown { ...@@ -55,7 +55,7 @@ class CreateLabelDropdown {
this.$newLabelCreateButton.on('click', this.saveLabel.bind(this)); this.$newLabelCreateButton.on('click', this.saveLabel.bind(this));
} }
addColorValue (e, $this) { addColorValue(e, $this) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
...@@ -66,7 +66,7 @@ class CreateLabelDropdown { ...@@ -66,7 +66,7 @@ class CreateLabelDropdown {
.addClass('is-active'); .addClass('is-active');
} }
enableLabelCreateButton () { enableLabelCreateButton() {
if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') { if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') {
this.$newLabelError.hide(); this.$newLabelError.hide();
this.$newLabelCreateButton.enable(); this.$newLabelCreateButton.enable();
...@@ -75,7 +75,7 @@ class CreateLabelDropdown { ...@@ -75,7 +75,7 @@ class CreateLabelDropdown {
} }
} }
resetForm () { resetForm() {
this.$newLabelField this.$newLabelField
.val('') .val('')
.trigger('change'); .trigger('change');
...@@ -90,13 +90,13 @@ class CreateLabelDropdown { ...@@ -90,13 +90,13 @@ class CreateLabelDropdown {
.removeClass('is-active'); .removeClass('is-active');
} }
saveLabel (e) { saveLabel(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
Api.newLabel(this.namespacePath, this.projectPath, { Api.newLabel(this.namespacePath, this.projectPath, {
title: this.$newLabelField.val(), title: this.$newLabelField.val(),
color: this.$newColorField.val() color: this.$newColorField.val(),
}, (label) => { }, (label) => {
this.$newLabelCreateButton.enable(); this.$newLabelCreateButton.enable();
...@@ -107,8 +107,8 @@ class CreateLabelDropdown { ...@@ -107,8 +107,8 @@ class CreateLabelDropdown {
errors = label.message; errors = label.message;
} else { } else {
errors = Object.keys(label.message).map(key => errors = Object.keys(label.message).map(key =>
`${gl.text.humanize(key)} ${label.message[key].join(', ')}` `${gl.text.humanize(key)} ${label.message[key].join(', ')}`,
).join("<br/>"); ).join('<br/>');
} }
this.$newLabelError this.$newLabelError
...@@ -122,6 +122,3 @@ class CreateLabelDropdown { ...@@ -122,6 +122,3 @@ class CreateLabelDropdown {
}); });
} }
} }
window.gl = window.gl || {};
gl.CreateLabelDropdown = CreateLabelDropdown;
<script>
import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg';
export default {
props: {
documentationLink: {
type: String,
required: true,
},
},
computed: {
iconCycleAnalyticsSplash() {
return iconCycleAnalyticsSplash;
},
},
methods: {
dismissOverviewDialog() {
this.$emit('dismiss-overview-dialog');
},
},
};
</script>
<template>
<div class="landing content-block">
<button
class="js-ca-dismiss-button dismiss-button"
type="button"
:aria-label="__('Dismiss Cycle Analytics introduction box')"
@click="dismissOverviewDialog">
<i
class="fa fa-times"
aria-hidden="true">
</i>
</button>
<div class="svg-container" v-html="iconCycleAnalyticsSplash">
</div>
<div class="inner-content">
<h4>
{{__('Introducing Cycle Analytics')}}
</h4>
<p>
{{ __('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.') }}
</p>
<p>
<a
:href="documentationLink"
target="_blank"
rel="nofollow"
class="btn">
{{__('Read more')}}
</a>
</p>
</div>
</div>
</template>
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import Vue from 'vue'; import Vue from 'vue';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
import banner from './components/banner.vue';
import stageCodeComponent from './components/stage_code_component.vue'; import stageCodeComponent from './components/stage_code_component.vue';
import stagePlanComponent from './components/stage_plan_component.vue'; import stagePlanComponent from './components/stage_plan_component.vue';
import stageComponent from './components/stage_component.vue'; import stageComponent from './components/stage_component.vue';
...@@ -44,6 +45,7 @@ $(() => { ...@@ -44,6 +45,7 @@ $(() => {
}, },
}, },
components: { components: {
banner,
'stage-issue-component': stageComponent, 'stage-issue-component': stageComponent,
'stage-plan-component': stagePlanComponent, 'stage-plan-component': stagePlanComponent,
'stage-code-component': stageCodeComponent, 'stage-code-component': stageCodeComponent,
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import './lib/utils/url_utility'; import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button'; import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff'; import SingleFileDiff from './single_file_diff';
import imageDiffHelper from './image_diff/helpers/index';
const UNFOLD_COUNT = 20; const UNFOLD_COUNT = 20;
let isBound = false; let isBound = false;
...@@ -20,7 +21,9 @@ class Diff { ...@@ -20,7 +21,9 @@ class Diff {
const tab = document.getElementById('diffs'); const tab = document.getElementById('diffs');
if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== '')) FilesCommentButton.init($diffFile); if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== '')) FilesCommentButton.init($diffFile);
$diffFile.each((index, file) => new gl.ImageFile(file)); const firstFile = $('.files').first().get(0);
const canCreateNote = firstFile && firstFile.hasAttribute('data-can-create-note');
$diffFile.each((index, file) => imageDiffHelper.initImageDiff(file, canCreateNote));
if (!isBound) { if (!isBound) {
$(document) $(document)
......
...@@ -171,7 +171,14 @@ const JumpToDiscussion = Vue.extend({ ...@@ -171,7 +171,14 @@ const JumpToDiscussion = Vue.extend({
// When jumping between unresolved discussions on the diffs tab, we show them. // When jumping between unresolved discussions on the diffs tab, we show them.
$target.closest(".content").show(); $target.closest(".content").show();
$target = $target.closest("tr.notes_holder"); const $notesHolder = $target.closest("tr.notes_holder");
// Image diff discussions does not use notes_holder
// so we should keep original $target value in those cases
if ($notesHolder.length > 0) {
$target = $notesHolder;
}
$target.show(); $target.show();
// If we are on the diffs tab, we don't scroll to the discussion itself, but to // If we are on the diffs tab, we don't scroll to the discussion itself, but to
......
...@@ -7,8 +7,6 @@ ...@@ -7,8 +7,6 @@
/* global IssuableForm */ /* global IssuableForm */
/* global LabelsSelect */ /* global LabelsSelect */
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global Commit */
/* global CommitsList */
/* global NewBranchForm */ /* global NewBranchForm */
/* global NotificationsForm */ /* global NotificationsForm */
/* global NotificationsDropdown */ /* global NotificationsDropdown */
...@@ -36,6 +34,7 @@ ...@@ -36,6 +34,7 @@
/* global Sidebar */ /* global Sidebar */
/* global ShortcutsWiki */ /* global ShortcutsWiki */
import CommitsList from './commits';
import Issue from './issue'; import Issue from './issue';
import BindInOut from './behaviors/bind_in_out'; import BindInOut from './behaviors/bind_in_out';
import DeleteModal from './branches/branches_delete_modal'; import DeleteModal from './branches/branches_delete_modal';
...@@ -76,7 +75,9 @@ import initProjectVisibilitySelector from './project_visibility'; ...@@ -76,7 +75,9 @@ import initProjectVisibilitySelector from './project_visibility';
import GpgBadges from './gpg_badges'; import GpgBadges from './gpg_badges';
import UserFeatureHelper from './helpers/user_feature_helper'; import UserFeatureHelper from './helpers/user_feature_helper';
import initChangesDropdown from './init_changes_dropdown'; import initChangesDropdown from './init_changes_dropdown';
import AbuseReports from './abuse_reports';
import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
import AjaxLoadingSpinner from './ajax_loading_spinner';
(function() { (function() {
var Dispatcher; var Dispatcher;
...@@ -238,7 +239,7 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; ...@@ -238,7 +239,7 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)); new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML));
break; break;
case 'projects:branches:index': case 'projects:branches:index':
gl.AjaxLoadingSpinner.init(); AjaxLoadingSpinner.init();
new DeleteModal(); new DeleteModal();
break; break;
case 'projects:issues:new': case 'projects:issues:new':
...@@ -316,7 +317,6 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; ...@@ -316,7 +317,6 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
new gl.Activities(); new gl.Activities();
break; break;
case 'projects:commit:show': case 'projects:commit:show':
new Commit();
new gl.Diff(); new gl.Diff();
new ZenMode(); new ZenMode();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
...@@ -562,7 +562,7 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; ...@@ -562,7 +562,7 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
new Labels(); new Labels();
} }
case 'abuse_reports': case 'abuse_reports':
new gl.AbuseReports(); new AbuseReports();
break; break;
} }
break; break;
......
export function createImageBadge(noteId, { x, y }, classNames = []) {
const buttonEl = document.createElement('button');
const classList = classNames.concat(['js-image-badge']);
classList.forEach(className => buttonEl.classList.add(className));
buttonEl.setAttribute('type', 'button');
buttonEl.setAttribute('disabled', true);
buttonEl.dataset.noteId = noteId;
buttonEl.style.left = `${x}px`;
buttonEl.style.top = `${y}px`;
return buttonEl;
}
export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) {
const buttonEl = createImageBadge(noteId, coordinate, ['badge']);
buttonEl.innerText = badgeText;
containerEl.appendChild(buttonEl);
}
export function addImageCommentBadge(containerEl, { coordinate, noteId }) {
const buttonEl = createImageBadge(noteId, coordinate, ['image-comment-badge', 'inverted']);
const iconEl = document.createElement('i');
iconEl.className = 'fa fa-comment-o';
iconEl.setAttribute('aria-label', 'comment');
buttonEl.appendChild(iconEl);
containerEl.appendChild(buttonEl);
}
export function addAvatarBadge(el, event) {
const { noteId, badgeNumber } = event.detail;
// Add badge to new comment
const avatarBadgeEl = el.querySelector(`#${noteId} .badge`);
avatarBadgeEl.innerText = badgeNumber;
avatarBadgeEl.classList.remove('hidden');
}
export function addCommentIndicator(containerEl, { x, y }) {
const buttonEl = document.createElement('button');
buttonEl.classList.add('btn-transparent');
buttonEl.classList.add('comment-indicator');
buttonEl.setAttribute('type', 'button');
buttonEl.style.left = `${x}px`;
buttonEl.style.top = `${y}px`;
buttonEl.innerHTML = gl.utils.spriteIcon('image-comment-dark');
containerEl.appendChild(buttonEl);
}
export function removeCommentIndicator(imageFrameEl) {
const commentIndicatorEl = imageFrameEl.querySelector('.comment-indicator');
const imageEl = imageFrameEl.querySelector('img');
const willRemove = !!commentIndicatorEl;
let meta = {};
if (willRemove) {
meta = {
x: parseInt(commentIndicatorEl.style.left, 10),
y: parseInt(commentIndicatorEl.style.top, 10),
image: {
width: imageEl.width,
height: imageEl.height,
},
};
commentIndicatorEl.remove();
}
return Object.assign({}, meta, {
removed: willRemove,
});
}
export function showCommentIndicator(imageFrameEl, coordinate) {
const { x, y } = coordinate;
const commentIndicatorEl = imageFrameEl.querySelector('.comment-indicator');
if (commentIndicatorEl) {
commentIndicatorEl.style.left = `${x}px`;
commentIndicatorEl.style.top = `${y}px`;
} else {
addCommentIndicator(imageFrameEl, coordinate);
}
}
export function commentIndicatorOnClick(event) {
// Prevent from triggering onAddImageDiffNote in notes.js
event.stopPropagation();
const buttonEl = event.currentTarget;
const diffViewerEl = buttonEl.closest('.diff-viewer');
const textareaEl = diffViewerEl.querySelector('.note-container .note-textarea');
textareaEl.focus();
}
export function setPositionDataAttribute(el, options) {
// Update position data attribute so that the
// new comment form can use this data for ajax request
const { x, y, width, height } = options;
const position = el.dataset.position;
const positionObject = Object.assign({}, JSON.parse(position), {
x,
y,
width,
height,
});
el.setAttribute('data-position', JSON.stringify(positionObject));
}
export function updateDiscussionAvatarBadgeNumber(discussionEl, newBadgeNumber) {
const avatarBadgeEl = discussionEl.querySelector('.image-diff-avatar-link .badge');
avatarBadgeEl.innerText = newBadgeNumber;
}
export function updateDiscussionBadgeNumber(discussionEl, newBadgeNumber) {
const discussionBadgeEl = discussionEl.querySelector('.badge');
discussionBadgeEl.innerText = newBadgeNumber;
}
export function toggleCollapsed(event) {
const toggleButtonEl = event.currentTarget;
const discussionNotesEl = toggleButtonEl.closest('.discussion-notes');
const formEl = discussionNotesEl.querySelector('.discussion-form');
const isCollapsed = discussionNotesEl.classList.contains('collapsed');
if (isCollapsed) {
discussionNotesEl.classList.remove('collapsed');
} else {
discussionNotesEl.classList.add('collapsed');
}
// Override the inline display style set in notes.js
if (formEl && !isCollapsed) {
formEl.style.display = 'none';
} else if (formEl && isCollapsed) {
formEl.style.display = 'block';
}
}
import * as badgeHelper from './badge_helper';
import * as commentIndicatorHelper from './comment_indicator_helper';
import * as domHelper from './dom_helper';
import * as utilsHelper from './utils_helper';
export default {
addCommentIndicator: commentIndicatorHelper.addCommentIndicator,
removeCommentIndicator: commentIndicatorHelper.removeCommentIndicator,
showCommentIndicator: commentIndicatorHelper.showCommentIndicator,
commentIndicatorOnClick: commentIndicatorHelper.commentIndicatorOnClick,
addImageBadge: badgeHelper.addImageBadge,
addImageCommentBadge: badgeHelper.addImageCommentBadge,
addAvatarBadge: badgeHelper.addAvatarBadge,
setPositionDataAttribute: domHelper.setPositionDataAttribute,
updateDiscussionAvatarBadgeNumber: domHelper.updateDiscussionAvatarBadgeNumber,
updateDiscussionBadgeNumber: domHelper.updateDiscussionBadgeNumber,
toggleCollapsed: domHelper.toggleCollapsed,
resizeCoordinatesToImageElement: utilsHelper.resizeCoordinatesToImageElement,
generateBadgeFromDiscussionDOM: utilsHelper.generateBadgeFromDiscussionDOM,
getTargetSelection: utilsHelper.getTargetSelection,
initImageDiff: utilsHelper.initImageDiff,
};
import ImageBadge from '../image_badge';
import ImageDiff from '../image_diff';
import ReplacedImageDiff from '../replaced_image_diff';
import '../../commit/image_file';
export function resizeCoordinatesToImageElement(imageEl, meta) {
const { x, y, width, height } = meta;
const imageWidth = imageEl.width;
const imageHeight = imageEl.height;
const widthRatio = imageWidth / width;
const heightRatio = imageHeight / height;
return {
x: Math.round(x * widthRatio),
y: Math.round(y * heightRatio),
width: imageWidth,
height: imageHeight,
};
}
export function generateBadgeFromDiscussionDOM(imageFrameEl, discussionEl) {
const position = JSON.parse(discussionEl.dataset.position);
const firstNoteEl = discussionEl.querySelector('.note');
const badge = new ImageBadge({
actual: position,
imageEl: imageFrameEl.querySelector('img'),
noteId: firstNoteEl.id,
discussionId: discussionEl.dataset.discussionId,
});
return badge;
}
export function getTargetSelection(event) {
const containerEl = event.currentTarget;
const imageEl = containerEl.querySelector('img');
const x = event.offsetX;
const y = event.offsetY;
const width = imageEl.width;
const height = imageEl.height;
const actualWidth = imageEl.naturalWidth;
const actualHeight = imageEl.naturalHeight;
const widthRatio = actualWidth / width;
const heightRatio = actualHeight / height;
// Browser will include the frame as a clickable target,
// which would result in potential 1px out of bounds value
// This bound the coordinates to inside the frame
const normalizedX = Math.max(0, x) && Math.min(x, width);
const normalizedY = Math.max(0, y) && Math.min(y, height);
return {
browser: {
x: normalizedX,
y: normalizedY,
width,
height,
},
actual: {
// Round x, y so that we don't need to deal with decimals
x: Math.round(normalizedX * widthRatio),
y: Math.round(normalizedY * heightRatio),
width: actualWidth,
height: actualHeight,
},
};
}
export function initImageDiff(fileEl, canCreateNote, renderCommentBadge) {
const options = {
canCreateNote,
renderCommentBadge,
};
let diff;
// ImageFile needs to be invoked before initImageDiff so that badges
// can mount to the correct location
new gl.ImageFile(fileEl); // eslint-disable-line no-new
if (fileEl.querySelector('.diff-file .js-single-image')) {
diff = new ImageDiff(fileEl, options);
diff.init();
} else if (fileEl.querySelector('.diff-file .js-replaced-image')) {
diff = new ReplacedImageDiff(fileEl, options);
diff.init();
}
return diff;
}
import imageDiffHelper from './helpers/index';
const defaultMeta = {
x: 0,
y: 0,
width: 0,
height: 0,
};
export default class ImageBadge {
constructor(options) {
const { noteId, discussionId } = options;
this.actual = options.actual || defaultMeta;
this.browser = options.browser || defaultMeta;
this.noteId = noteId;
this.discussionId = discussionId;
if (options.imageEl && !options.browser) {
this.browser = imageDiffHelper.resizeCoordinatesToImageElement(options.imageEl, this.actual);
}
}
}
import imageDiffHelper from './helpers/index';
import ImageBadge from './image_badge';
import { isImageLoaded } from '../lib/utils/image_utility';
export default class ImageDiff {
constructor(el, options) {
this.el = el;
this.canCreateNote = !!(options && options.canCreateNote);
this.renderCommentBadge = !!(options && options.renderCommentBadge);
this.$noteContainer = $('.note-container', this.el);
this.imageBadges = [];
}
init() {
this.imageFrameEl = this.el.querySelector('.diff-file .js-image-frame');
this.imageEl = this.imageFrameEl.querySelector('img');
this.bindEvents();
}
bindEvents() {
this.imageClickedWrapper = this.imageClicked.bind(this);
this.imageBlurredWrapper = imageDiffHelper.removeCommentIndicator.bind(null, this.imageFrameEl);
this.addBadgeWrapper = this.addBadge.bind(this);
this.removeBadgeWrapper = this.removeBadge.bind(this);
this.renderBadgesWrapper = this.renderBadges.bind(this);
// Render badges
if (isImageLoaded(this.imageEl)) {
this.renderBadges();
} else {
this.imageEl.addEventListener('load', this.renderBadgesWrapper);
}
// jquery makes the event delegation here much simpler
this.$noteContainer.on('click', '.js-diff-notes-toggle', imageDiffHelper.toggleCollapsed);
$(this.el).on('click', '.comment-indicator', imageDiffHelper.commentIndicatorOnClick);
if (this.canCreateNote) {
this.el.addEventListener('click.imageDiff', this.imageClickedWrapper);
this.el.addEventListener('blur.imageDiff', this.imageBlurredWrapper);
this.el.addEventListener('addBadge.imageDiff', this.addBadgeWrapper);
this.el.addEventListener('removeBadge.imageDiff', this.removeBadgeWrapper);
}
}
imageClicked(event) {
const customEvent = event.detail;
const selection = imageDiffHelper.getTargetSelection(customEvent);
const el = customEvent.currentTarget;
imageDiffHelper.setPositionDataAttribute(el, selection.actual);
imageDiffHelper.showCommentIndicator(this.imageFrameEl, selection.browser);
}
renderBadges() {
const discussionsEls = this.el.querySelectorAll('.note-container .discussion-notes .notes');
[...discussionsEls].forEach(this.renderBadge.bind(this));
}
renderBadge(discussionEl, index) {
const imageBadge = imageDiffHelper
.generateBadgeFromDiscussionDOM(this.imageFrameEl, discussionEl);
this.imageBadges.push(imageBadge);
const options = {
coordinate: imageBadge.browser,
noteId: imageBadge.noteId,
};
if (this.renderCommentBadge) {
imageDiffHelper.addImageCommentBadge(this.imageFrameEl, options);
} else {
const numberBadgeOptions = Object.assign({}, options, {
badgeText: index + 1,
});
imageDiffHelper.addImageBadge(this.imageFrameEl, numberBadgeOptions);
}
}
addBadge(event) {
const { x, y, width, height, noteId, discussionId } = event.detail;
const badgeText = this.imageBadges.length + 1;
const imageBadge = new ImageBadge({
actual: {
x,
y,
width,
height,
},
imageEl: this.imageFrameEl.querySelector('img'),
noteId,
discussionId,
});
this.imageBadges.push(imageBadge);
imageDiffHelper.addImageBadge(this.imageFrameEl, {
coordinate: imageBadge.browser,
badgeText,
noteId,
});
imageDiffHelper.addAvatarBadge(this.el, {
detail: {
noteId,
badgeNumber: badgeText,
},
});
const discussionEl = this.el.querySelector(`#discussion_${discussionId}`);
imageDiffHelper.updateDiscussionBadgeNumber(discussionEl, badgeText);
}
removeBadge(event) {
const { badgeNumber } = event.detail;
const indexToRemove = badgeNumber - 1;
const imageBadgeEls = this.imageFrameEl.querySelectorAll('.badge');
if (this.imageBadges.length !== badgeNumber) {
// Cascade badges count numbers for (avatar badges + image badges)
this.imageBadges.forEach((badge, index) => {
if (index > indexToRemove) {
const { discussionId } = badge;
const updatedBadgeNumber = index;
const discussionEl = this.el.querySelector(`#discussion_${discussionId}`);
imageBadgeEls[index].innerText = updatedBadgeNumber;
imageDiffHelper.updateDiscussionBadgeNumber(discussionEl, updatedBadgeNumber);
imageDiffHelper.updateDiscussionAvatarBadgeNumber(discussionEl, updatedBadgeNumber);
}
});
}
this.imageBadges.splice(indexToRemove, 1);
const imageBadgeEl = imageBadgeEls[indexToRemove];
imageBadgeEl.remove();
}
}
import imageDiffHelper from './helpers/index';
export default () => {
// Always pass can-create-note as false because a user
// cannot place new badge markers on discussion tab
const canCreateNote = false;
const renderCommentBadge = true;
const diffFileEls = document.querySelectorAll('.timeline-content .diff-file.js-image-file');
[...diffFileEls].forEach(diffFileEl =>
imageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge));
};
import imageDiffHelper from './helpers/index';
import { viewTypes, isValidViewType } from './view_types';
import ImageDiff from './image_diff';
export default class ReplacedImageDiff extends ImageDiff {
init(defaultViewType = viewTypes.TWO_UP) {
this.imageFrameEls = {
[viewTypes.TWO_UP]: this.el.querySelector('.two-up .js-image-frame'),
[viewTypes.SWIPE]: this.el.querySelector('.swipe .js-image-frame'),
[viewTypes.ONION_SKIN]: this.el.querySelector('.onion-skin .js-image-frame'),
};
const viewModesEl = this.el.querySelector('.view-modes-menu');
this.viewModesEls = {
[viewTypes.TWO_UP]: viewModesEl.querySelector('.two-up'),
[viewTypes.SWIPE]: viewModesEl.querySelector('.swipe'),
[viewTypes.ONION_SKIN]: viewModesEl.querySelector('.onion-skin'),
};
this.currentView = defaultViewType;
this.generateImageEls();
this.bindEvents();
}
generateImageEls() {
this.imageEls = {};
const viewTypeNames = Object.getOwnPropertyNames(viewTypes);
viewTypeNames.forEach((viewType) => {
this.imageEls[viewType] = this.imageFrameEls[viewType].querySelector('img');
});
}
bindEvents() {
super.bindEvents();
this.changeToViewTwoUp = this.changeView.bind(this, viewTypes.TWO_UP);
this.changeToViewSwipe = this.changeView.bind(this, viewTypes.SWIPE);
this.changeToViewOnionSkin = this.changeView.bind(this, viewTypes.ONION_SKIN);
this.viewModesEls[viewTypes.TWO_UP].addEventListener('click', this.changeToViewTwoUp);
this.viewModesEls[viewTypes.SWIPE].addEventListener('click', this.changeToViewSwipe);
this.viewModesEls[viewTypes.ONION_SKIN].addEventListener('click', this.changeToViewOnionSkin);
}
get imageEl() {
return this.imageEls[this.currentView];
}
get imageFrameEl() {
return this.imageFrameEls[this.currentView];
}
changeView(newView) {
if (!isValidViewType(newView)) {
return;
}
const indicator = imageDiffHelper.removeCommentIndicator(this.imageFrameEl);
this.currentView = newView;
// Clear existing badges on new view
const existingBadges = this.imageFrameEl.querySelectorAll('.badge');
[...existingBadges].map(badge => badge.remove());
// Remove existing references to old view image badges
this.imageBadges = [];
// Image_file.js has a fade animation of 200ms for loading the view
// Need to wait an additional 250ms for the images to be displayed
// on window in order to re-normalize their dimensions
setTimeout(this.renderNewView.bind(this, indicator), 250);
}
renderNewView(indicator) {
// Generate badge coordinates on new view
this.renderBadges();
// Re-render indicator in new view
if (indicator.removed) {
const normalizedIndicator = imageDiffHelper
.resizeCoordinatesToImageElement(this.imageEl, {
x: indicator.x,
y: indicator.y,
width: indicator.image.width,
height: indicator.image.height,
});
imageDiffHelper.showCommentIndicator(this.imageFrameEl, normalizedIndicator);
}
}
}
export const viewTypes = {
TWO_UP: 'TWO_UP',
SWIPE: 'SWIPE',
ONION_SKIN: 'ONION_SKIN',
};
export function isValidViewType(validate) {
return !!Object.getOwnPropertyNames(viewTypes).find(viewType => viewType === validate);
}
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import _ from 'underscore'; import _ from 'underscore';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils'; import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label';
(function() { (function() {
this.LabelsSelect = (function() { this.LabelsSelect = (function() {
...@@ -61,7 +62,7 @@ import DropdownUtils from './filtered_search/dropdown_utils'; ...@@ -61,7 +62,7 @@ import DropdownUtils from './filtered_search/dropdown_utils';
$sidebarLabelTooltip.tooltip(); $sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) { if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath); new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
} }
saveLabelData = function() { saveLabelData = function() {
......
/* eslint-disable import/prefer-default-export */
export function isImageLoaded(element) {
return element.complete && element.naturalHeight !== 0;
}
...@@ -35,12 +35,9 @@ import './shortcuts_network'; ...@@ -35,12 +35,9 @@ import './shortcuts_network';
import './templates/issuable_template_selector'; import './templates/issuable_template_selector';
import './templates/issuable_template_selectors'; import './templates/issuable_template_selectors';
// commit
import './commit/file';
import './commit/image_file'; import './commit/image_file';
// lib/utils // lib/utils
import './lib/utils/bootstrap_linked_tabs';
import { handleLocationHash } from './lib/utils/common_utils'; import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility'; import './lib/utils/datetime_utility';
import './lib/utils/pretty_time'; import './lib/utils/pretty_time';
...@@ -57,10 +54,8 @@ import './u2f/register'; ...@@ -57,10 +54,8 @@ import './u2f/register';
import './u2f/util'; import './u2f/util';
// everything else // everything else
import './abuse_reports';
import './activities'; import './activities';
import './admin'; import './admin';
import './ajax_loading_spinner';
import './api'; import './api';
import './aside'; import './aside';
import './autosave'; import './autosave';
...@@ -71,14 +66,12 @@ import './build'; ...@@ -71,14 +66,12 @@ import './build';
import './build_artifacts'; import './build_artifacts';
import './build_variables'; import './build_variables';
import './ci_lint_editor'; import './ci_lint_editor';
import './commit';
import './commits'; import './commits';
import './compare'; import './compare';
import './compare_autocomplete'; import './compare_autocomplete';
import './confirm_danger_modal'; import './confirm_danger_modal';
import './copy_as_gfm'; import './copy_as_gfm';
import './copy_to_clipboard'; import './copy_to_clipboard';
import './create_label';
import './diff'; import './diff';
import './dropzone_input'; import './dropzone_input';
import './due_date_select'; import './due_date_select';
...@@ -111,7 +104,6 @@ import './merge_request'; ...@@ -111,7 +104,6 @@ import './merge_request';
import './merge_request_tabs'; import './merge_request_tabs';
import './milestone'; import './milestone';
import './milestone_select'; import './milestone_select';
import './mini_pipeline_graph_dropdown';
import './namespace_select'; import './namespace_select';
import './new_branch_form'; import './new_branch_form';
import './new_commit_form'; import './new_commit_form';
...@@ -119,7 +111,6 @@ import './notes'; ...@@ -119,7 +111,6 @@ import './notes';
import './notifications_dropdown'; import './notifications_dropdown';
import './notifications_form'; import './notifications_form';
import './pager'; import './pager';
import './pipelines';
import './preview_markdown'; import './preview_markdown';
import './project'; import './project';
import './project_avatar'; import './project_avatar';
......
...@@ -13,6 +13,8 @@ import { ...@@ -13,6 +13,8 @@ import {
isMetaClick, isMetaClick,
} from './lib/utils/common_utils'; } from './lib/utils/common_utils';
import initDiscussionTab from './image_diff/init_discussion_tab';
/* eslint-disable max-len */ /* eslint-disable max-len */
// MergeRequestTabs // MergeRequestTabs
// //
...@@ -154,6 +156,8 @@ import { ...@@ -154,6 +156,8 @@ import {
} }
this.resetViewContainer(); this.resetViewContainer();
this.destroyPipelinesView(); this.destroyPipelinesView();
initDiscussionTab();
} }
if (this.setUrl) { if (this.setUrl) {
this.setCurrentAction(action); this.setCurrentAction(action);
......
...@@ -79,7 +79,11 @@ ...@@ -79,7 +79,11 @@
}, },
formatMetricUsage(series) { formatMetricUsage(series) {
return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`; const value = series.values[this.currentDataIndex].value;
if (isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`;
}, },
createSeriesString(index, series) { createSeriesString(index, series) {
......
...@@ -56,12 +56,16 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra ...@@ -56,12 +56,16 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
timeSeriesScaleX.ticks(d3.time.minute, 60); timeSeriesScaleX.ticks(d3.time.minute, 60);
timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]); timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]);
const defined = d => !isNaN(d.value) && d.value != null;
const lineFunction = d3.svg.line() const lineFunction = d3.svg.line()
.defined(defined)
.interpolate('linear') .interpolate('linear')
.x(d => timeSeriesScaleX(d.time)) .x(d => timeSeriesScaleX(d.time))
.y(d => timeSeriesScaleY(d.value)); .y(d => timeSeriesScaleY(d.value));
const areaFunction = d3.svg.area() const areaFunction = d3.svg.area()
.defined(defined)
.interpolate('linear') .interpolate('linear')
.x(d => timeSeriesScaleX(d.time)) .x(d => timeSeriesScaleX(d.time))
.y0(graphHeight - graphHeightOffset) .y0(graphHeight - graphHeightOffset)
......
...@@ -24,6 +24,7 @@ import './autosave'; ...@@ -24,6 +24,7 @@ import './autosave';
import './dropzone_input'; import './dropzone_input';
import TaskList from './task_list'; import TaskList from './task_list';
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils'; import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
window.autosize = autosize; window.autosize = autosize;
window.Dropzone = Dropzone; window.Dropzone = Dropzone;
...@@ -42,6 +43,7 @@ export default class Notes { ...@@ -42,6 +43,7 @@ export default class Notes {
this.visibilityChange = this.visibilityChange.bind(this); this.visibilityChange = this.visibilityChange.bind(this);
this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this); this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this);
this.onAddDiffNote = this.onAddDiffNote.bind(this); this.onAddDiffNote = this.onAddDiffNote.bind(this);
this.onAddImageDiffNote = this.onAddImageDiffNote.bind(this);
this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this); this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this);
this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this); this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this);
this.removeNote = this.removeNote.bind(this); this.removeNote = this.removeNote.bind(this);
...@@ -114,6 +116,8 @@ export default class Notes { ...@@ -114,6 +116,8 @@ export default class Notes {
$(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote); $(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
// add diff note // add diff note
$(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote); $(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// add diff note for images
$(document).on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote);
// hide diff note form // hide diff note form
$(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm); $(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
// toggle commit list // toggle commit list
...@@ -140,6 +144,7 @@ export default class Notes { ...@@ -140,6 +144,7 @@ export default class Notes {
$(document).off('click', '.js-note-attachment-delete'); $(document).off('click', '.js-note-attachment-delete');
$(document).off('click', '.js-discussion-reply-button'); $(document).off('click', '.js-discussion-reply-button');
$(document).off('click', '.js-add-diff-note-button'); $(document).off('click', '.js-add-diff-note-button');
$(document).off('click', '.js-add-image-diff-note-button');
$(document).off('visibilitychange'); $(document).off('visibilitychange');
$(document).off('keyup input', '.js-note-text'); $(document).off('keyup input', '.js-note-text');
$(document).off('click', '.js-note-target-reopen'); $(document).off('click', '.js-note-target-reopen');
...@@ -412,6 +417,11 @@ export default class Notes { ...@@ -412,6 +417,11 @@ export default class Notes {
this.note_ids.push(noteEntity.id); this.note_ids.push(noteEntity.id);
form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`); form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
row = form.closest('tr'); row = form.closest('tr');
if (noteEntity.on_image) {
row = form;
}
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion? // is this the first note of discussion?
...@@ -423,7 +433,7 @@ export default class Notes { ...@@ -423,7 +433,7 @@ export default class Notes {
if (noteEntity.diff_discussion_html) { if (noteEntity.diff_discussion_html) {
var $discussion = $(noteEntity.diff_discussion_html).renderGFM(); var $discussion = $(noteEntity.diff_discussion_html).renderGFM();
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) { if (!this.isParallelView() || row.hasClass('js-temp-notes-holder') || noteEntity.on_image) {
// insert the note and the reply button after the temp row // insert the note and the reply button after the temp row
row.after($discussion); row.after($discussion);
} else { } else {
...@@ -449,6 +459,7 @@ export default class Notes { ...@@ -449,6 +459,7 @@ export default class Notes {
if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) { if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity); this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
} }
...@@ -561,7 +572,7 @@ export default class Notes { ...@@ -561,7 +572,7 @@ export default class Notes {
form.find('#note_line_code').val(), form.find('#note_line_code').val(),
// DiffNote // DiffNote
form.find('#note_position').val() form.find('#note_position').val(),
]; ];
return new Autosave(textarea, key); return new Autosave(textarea, key);
} }
...@@ -783,9 +794,22 @@ export default class Notes { ...@@ -783,9 +794,22 @@ export default class Notes {
$(`.js-diff-avatars-${discussionId}`).trigger('remove.vue'); $(`.js-diff-avatars-${discussionId}`).trigger('remove.vue');
// The notes tr can contain multiple lists of notes, like on the parallel diff // The notes tr can contain multiple lists of notes, like on the parallel diff
if (notesTr.find('.discussion-notes').length > 1) { // notesTr does not exist for image diffs
if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) {
const $diffFile = $notes.closest('.diff-file');
if ($diffFile.length > 0) {
const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', {
detail: {
// badgeNumber's start with 1 and index starts with 0
badgeNumber: $notes.index() + 1,
},
});
$diffFile[0].dispatchEvent(removeBadgeEvent);
}
$notes.remove(); $notes.remove();
} else { } else if (notesTr.length > 0) {
notesTr.remove(); notesTr.remove();
} }
} }
...@@ -841,7 +865,11 @@ export default class Notes { ...@@ -841,7 +865,11 @@ export default class Notes {
*/ */
setupDiscussionNoteForm(dataHolder, form) { setupDiscussionNoteForm(dataHolder, form) {
// setup note target // setup note target
const diffFileData = dataHolder.closest('.text-file'); let diffFileData = dataHolder.closest('.text-file');
if (diffFileData.length === 0) {
diffFileData = dataHolder.closest('.image');
}
var discussionID = dataHolder.data('discussionId'); var discussionID = dataHolder.data('discussionId');
...@@ -907,6 +935,31 @@ export default class Notes { ...@@ -907,6 +935,31 @@ export default class Notes {
}); });
} }
onAddImageDiffNote(e) {
const $link = $(e.currentTarget || e.target);
const $diffFile = $link.closest('.diff-file');
const clickEvent = new CustomEvent('click.imageDiff', {
detail: e,
});
$diffFile[0].dispatchEvent(clickEvent);
// Setup comment form
let newForm;
const $noteContainer = $link.closest('.diff-viewer').find('.note-container');
const $form = $noteContainer.find('> .discussion-form');
if ($form.length === 0) {
newForm = this.cleanForm(this.formClone.clone());
newForm.appendTo($noteContainer);
} else {
newForm = $form;
}
this.setupDiscussionNoteForm($link, newForm);
}
toggleDiffNote({ toggleDiffNote({
target, target,
lineType, lineType,
...@@ -999,10 +1052,25 @@ export default class Notes { ...@@ -999,10 +1052,25 @@ export default class Notes {
} }
cancelDiscussionForm(e) { cancelDiscussionForm(e) {
var form;
e.preventDefault(); e.preventDefault();
form = $(e.target).closest('.js-discussion-note-form'); const $form = $(e.target).closest('.js-discussion-note-form');
return this.removeDiscussionNoteForm(form); const $discussionNote = $(e.target).closest('.discussion-notes');
if ($discussionNote.length === 0) {
// Only send blur event when the discussion form
// is not part of a discussion note
const $diffFile = $form.closest('.diff-file');
if ($diffFile.length > 0) {
const blurEvent = new CustomEvent('blur.imageDiff', {
detail: e,
});
$diffFile[0].dispatchEvent(blurEvent);
}
}
return this.removeDiscussionNoteForm($form);
} }
/** /**
...@@ -1414,6 +1482,15 @@ export default class Notes { ...@@ -1414,6 +1482,15 @@ export default class Notes {
// Submission successful! remove placeholder // Submission successful! remove placeholder
$notesContainer.find(`#${noteUniqueId}`).remove(); $notesContainer.find(`#${noteUniqueId}`).remove();
const $diffFile = $form.closest('.diff-file');
if ($diffFile.length > 0) {
const blurEvent = new CustomEvent('blur.imageDiff', {
detail: e,
});
$diffFile[0].dispatchEvent(blurEvent);
}
// Reset cached commands list when command is applied // Reset cached commands list when command is applied
if (hasQuickActions) { if (hasQuickActions) {
$form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho'); $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho');
...@@ -1436,7 +1513,28 @@ export default class Notes { ...@@ -1436,7 +1513,28 @@ export default class Notes {
} }
// Show final note element on UI // Show final note element on UI
this.addDiscussionNote($form, note, $notesContainer.length === 0); const isNewDiffComment = $notesContainer.length === 0;
this.addDiscussionNote($form, note, isNewDiffComment);
if (isNewDiffComment) {
// Add image badge, avatar badge and toggle discussion badge for new image diffs
const notePosition = $form.find('#note_position').val();
if ($diffFile.length > 0 && notePosition.length > 0) {
const { x, y, width, height } = JSON.parse(notePosition);
const addBadgeEvent = new CustomEvent('addBadge.imageDiff', {
detail: {
x,
y,
width,
height,
noteId: `note_${note.id}`,
discussionId: note.discussion_id,
},
});
$diffFile[0].dispatchEvent(addBadgeEvent);
}
}
// append flash-container to the Notes list // append flash-container to the Notes list
if ($notesContainer.length) { if ($notesContainer.length) {
...@@ -1457,6 +1555,16 @@ export default class Notes { ...@@ -1457,6 +1555,16 @@ export default class Notes {
// Submission failed, remove placeholder note and show Flash error message // Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${noteUniqueId}`).remove(); $notesContainer.find(`#${noteUniqueId}`).remove();
const blurEvent = new CustomEvent('blur.imageDiff', {
detail: e,
});
const closestDiffFile = $form.closest('.diff-file');
if (closestDiffFile.length) {
closestDiffFile[0].dispatchEvent(blurEvent);
}
if (hasQuickActions) { if (hasQuickActions) {
$notesContainer.find(`#${systemNoteUniqueId}`).remove(); $notesContainer.find(`#${systemNoteUniqueId}`).remove();
} }
...@@ -1500,6 +1608,8 @@ export default class Notes { ...@@ -1500,6 +1608,8 @@ export default class Notes {
const $noteBody = $editingNote.find('.js-task-list-container'); const $noteBody = $editingNote.find('.js-task-list-container');
const $noteBodyText = $noteBody.find('.note-text'); const $noteBodyText = $noteBody.find('.note-text');
const { formData, formContent, formAction } = this.getFormData($form); const { formData, formContent, formAction } = this.getFormData($form);
const $diffFile = $form.closest('.diff-file');
const $notesContainer = $form.closest('.notes');
// Cache original comment content // Cache original comment content
const cachedNoteBodyText = $noteBodyText.html(); const cachedNoteBodyText = $noteBodyText.html();
......
...@@ -18,23 +18,8 @@ import findAndFollowLink from './shortcuts_dashboard_navigation'; ...@@ -18,23 +18,8 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
Mousetrap.bind('f', (e => this.focusFilter(e))); Mousetrap.bind('f', (e => this.focusFilter(e)));
Mousetrap.bind('p b', this.onTogglePerfBar); Mousetrap.bind('p b', this.onTogglePerfBar);
const $globalDropdownMenu = $('.global-dropdown-menu');
const $globalDropdownToggle = $('.global-dropdown-toggle');
const findFileURL = document.body.dataset.findFile; const findFileURL = document.body.dataset.findFile;
$('.global-dropdown').on('hide.bs.dropdown', () => {
$globalDropdownMenu.removeClass('shortcuts');
});
Mousetrap.bind('n', () => {
$globalDropdownMenu.toggleClass('shortcuts');
$globalDropdownToggle.trigger('click');
if (!$globalDropdownMenu.is(':visible')) {
$globalDropdownToggle.blur();
}
});
Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos')); Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity')); Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity'));
Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues')); Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues'));
......
/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */ /* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
import FilesCommentButton from './files_comment_button'; import FilesCommentButton from './files_comment_button';
import imageDiffHelper from './image_diff/helpers/index';
const WRAPPER = '<div class="diff-content"></div>'; const WRAPPER = '<div class="diff-content"></div>';
const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'; const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
...@@ -74,7 +75,11 @@ export default class SingleFileDiff { ...@@ -74,7 +75,11 @@ export default class SingleFileDiff {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
} }
FilesCommentButton.init($(_this.file)); const $file = $(_this.file);
FilesCommentButton.init($file);
const canCreateNote = $file.closest('.files').is('[data-can-create-note]');
imageDiffHelper.initImageDiff($file[0], canCreateNote);
if (cb) cb(); if (cb) cb();
}; };
......
...@@ -30,10 +30,9 @@ ...@@ -30,10 +30,9 @@
@import "framework/media_object"; @import "framework/media_object";
@import "framework/mobile"; @import "framework/mobile";
@import "framework/modal"; @import "framework/modal";
@import "framework/nav";
@import "framework/new-nav";
@import "framework/pagination"; @import "framework/pagination";
@import "framework/panels"; @import "framework/panels";
@import "framework/secondary-navigation-elements";
@import "framework/selects"; @import "framework/selects";
@import "framework/sidebar"; @import "framework/sidebar";
@import "framework/new-sidebar"; @import "framework/new-sidebar";
......
...@@ -115,8 +115,7 @@ ...@@ -115,8 +115,7 @@
@return $unfoldedTransition; @return $unfoldedTransition;
} }
.btn, .btn {
.global-dropdown-toggle {
@include transition(background-color, border-color, color, box-shadow); @include transition(background-color, border-color, color, box-shadow);
} }
......
...@@ -207,6 +207,16 @@ ...@@ -207,6 +207,16 @@
&.user-cover-block { &.user-cover-block {
padding: 24px 0 0; padding: 24px 0 0;
.nav-links {
justify-content: center;
width: 100%;
float: none;
&.scrolling-tabs {
float: none;
}
}
} }
.group-info { .group-info {
......
@mixin btn-comment-icon {
border-radius: 50%;
background: $white-light;
padding: 1px 5px;
font-size: 12px;
color: $blue-500;
width: 23px;
height: 23px;
border: 1px solid $blue-500;
&:hover,
&.inverted {
background: $blue-500;
border-color: $blue-600;
color: $white-light;
}
&:active {
outline: 0;
}
}
@mixin btn-default { @mixin btn-default {
border-radius: 3px; border-radius: 3px;
font-size: $gl-font-size; font-size: $gl-font-size;
......
...@@ -749,7 +749,7 @@ ...@@ -749,7 +749,7 @@
margin-bottom: $dropdown-vertical-offset; margin-bottom: $dropdown-vertical-offset;
} }
li { li:not(.dropdown-bold-header) {
display: block; display: block;
padding: 0 1px; padding: 0 1px;
...@@ -889,7 +889,7 @@ ...@@ -889,7 +889,7 @@
@include new-style-dropdown('.breadcrumbs-list .dropdown '); @include new-style-dropdown('.breadcrumbs-list .dropdown ');
@include new-style-dropdown('.js-namespace-select + '); @include new-style-dropdown('.js-namespace-select + ');
header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu { header.header-content .dropdown-menu.projects-dropdown-menu {
padding: 0; padding: 0;
} }
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) { @mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) {
// Header // Header
header.navbar-gitlab-new { .navbar-gitlab {
background-color: $color-900; background-color: $color-900;
.navbar-collapse { .navbar-collapse {
...@@ -200,9 +200,9 @@ body { ...@@ -200,9 +200,9 @@ body {
&.ui_light { &.ui_light {
@include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700); @include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700);
header.navbar-gitlab-new { .navbar-gitlab {
background-color: $theme-gray-100; background-color: $theme-gray-100;
box-shadow: 0 2px 0 0 $border-color; box-shadow: 0 1px 0 0 $border-color;
.logo-text svg { .logo-text svg {
fill: $theme-gray-900; fill: $theme-gray-900;
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
svg { svg {
&.s8 { @include svg-size(8px); } &.s8 { @include svg-size(8px); }
&.s12 { @include svg-size(12px); }
&.s16 { @include svg-size(16px); } &.s16 { @include svg-size(16px); }
&.s18 { @include svg-size(18px); } &.s18 { @include svg-size(18px); }
&.s24 { @include svg-size(24px); } &.s24 { @include svg-size(24px); }
......
...@@ -25,10 +25,6 @@ body { ...@@ -25,10 +25,6 @@ body {
.content-wrapper { .content-wrapper {
padding-bottom: 100px; padding-bottom: 100px;
&:not(.page-with-layout-nav) {
margin-top: $header-height;
}
} }
.container { .container {
......
@import "framework/variables";
@import 'framework/tw_bootstrap_variables';
@import "bootstrap/variables";
@import "framework/mixins";
.content-wrapper.page-with-new-nav {
margin-top: $new-navbar-height;
}
header.navbar-gitlab-new {
color: $white-light;
border-bottom: 0;
min-height: $new-navbar-height;
.logo-text {
line-height: initial;
svg {
width: 55px;
height: 14px;
margin: 0;
fill: $white-light;
}
}
.header-content {
display: -webkit-flex;
display: flex;
padding-left: 0;
min-height: $new-navbar-height;
.title-container {
display: -webkit-flex;
display: flex;
-webkit-align-items: stretch;
align-items: stretch;
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
padding-top: 0;
overflow: visible;
}
.title {
display: -webkit-flex;
display: flex;
padding-right: 0;
color: currentColor;
img {
height: 28px;
margin-right: 8px;
}
a {
display: -webkit-flex;
display: flex;
align-items: center;
padding: 2px 8px;
margin: 5px 2px 5px -8px;
border-radius: $border-radius-default;
svg {
@media (min-width: $screen-sm-min) {
margin-right: 8px;
}
}
}
}
.dropdown.open {
> a {
border-bottom-color: $white-light;
}
}
.dropdown-menu {
margin-top: 4px;
min-width: 130px;
@media (max-width: $screen-xs-max) {
left: auto;
right: 0;
}
}
&.menu-expanded {
@media (max-width: $screen-xs-max) {
.title-container,
.header-logo, {
display: none;
}
}
}
}
.dropdown-bold-header {
color: $gl-text-color-secondary;
font-size: 12px;
}
.navbar-collapse {
padding-left: 0;
box-shadow: 0;
@media (max-width: $screen-xs-max) {
margin-left: -8px;
margin-right: -10px;
}
.nav {
> li:not(.hidden-xs) a {
@media (max-width: $screen-xs-max) {
margin-left: 0;
min-width: 100%;
}
}
}
}
.container-fluid {
.navbar-toggle {
min-width: 45px;
padding: 0 $gl-padding;
margin-right: -7px;
text-align: center;
color: currentColor;
svg {
fill: currentColor;
}
&:hover,
&:focus,
&.active {
color: currentColor;
background-color: transparent;
svg {
fill: currentColor;
}
}
}
.navbar-nav {
@media (max-width: $screen-xs-max) {
display: flex;
padding-right: 10px;
}
li {
.badge {
box-shadow: none;
font-weight: $gl-font-weight-bold;
}
}
}
.nav > li {
&.header-user {
@media (max-width: $screen-xs-max) {
padding-left: 10px;
}
}
> a {
will-change: color;
margin: 4px 2px;
padding: 6px 8px;
height: 32px;
@media (max-width: $screen-xs-max) {
padding: 0;
}
&.header-user-dropdown-toggle {
margin-left: 2px;
.header-user-avatar {
margin-right: 0;
}
}
&:hover,
&:focus {
text-decoration: none;
outline: 0;
opacity: 1;
color: $white-light;
svg {
fill: currentColor;
}
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $white-light;
}
}
}
}
.header-new-dropdown-toggle {
margin-right: 0;
}
.impersonated-user,
.impersonated-user:hover {
margin-right: 1px;
background-color: $white-light;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.impersonation-btn,
.impersonation-btn:hover {
background-color: $white-light;
margin-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
i {
color: $orange-500;
font-size: 20px;
}
}
&.active > a,
&.dropdown.open > a {
svg {
fill: currentColor;
}
}
}
}
}
.navbar-sub-nav {
display: -webkit-flex;
display: flex;
margin: 0 0 0 6px;
.dropdown-chevron {
position: relative;
top: -1px;
font-size: 10px;
}
}
.navbar-gitlab-new {
.navbar-sub-nav,
.navbar-nav {
> li {
> a:hover,
> a:focus {
text-decoration: none;
outline: 0;
color: $white-light;
svg {
fill: currentColor;
}
}
> a {
display: flex;
align-items: center;
justify-content: center;
padding: 6px 8px;
margin: 4px 2px;
font-size: 12px;
color: currentColor;
border-radius: $border-radius-default;
height: 32px;
font-weight: $gl-font-weight-bold;
svg {
fill: currentColor;
}
}
&.line-separator {
margin: 8px;
}
}
}
}
.caret-down {
height: 11px;
width: 11px;
margin-left: 4px;
fill: currentColor;
}
.header-user .dropdown-menu-nav,
.header-new .dropdown-menu-nav {
margin-top: $dropdown-vertical-offset;
}
.breadcrumbs {
display: flex;
min-height: 48px;
color: $gl-text-color;
}
.breadcrumbs-container {
display: -webkit-flex;
display: flex;
width: 100%;
position: relative;
padding-top: $gl-padding / 2;
padding-bottom: $gl-padding / 2;
align-items: center;
border-bottom: 1px solid $border-color;
}
.breadcrumbs-links {
-webkit-flex: 1;
flex: 1;
min-width: 0;
align-self: center;
color: $gl-text-color-secondary;
.avatar-tile {
margin-right: 4px;
border: 1px solid $border-color;
border-radius: 50%;
vertical-align: sub;
}
.text-expander {
margin-left: 0;
margin-right: 2px;
> i {
position: relative;
top: 1px;
}
}
}
.breadcrumbs-list {
display: -webkit-flex;
display: flex;
flex-wrap: wrap;
margin-bottom: 0;
line-height: 16px;
> li {
display: flex;
align-items: center;
position: relative;
padding: 2px 0;
&:not(:last-child) {
margin-right: 20px;
}
> a {
font-size: 12px;
color: currentColor;
}
}
}
.breadcrumb-item-text {
@include str-truncated(128px);
text-decoration: inherit;
}
.breadcrumbs-list-angle {
position: absolute;
right: -12px;
top: 50%;
color: $gl-text-color-tertiary;
transform: translateY(-50%);
}
.breadcrumbs-extra {
display: flex;
flex: 0 0 auto;
margin-left: auto;
}
.breadcrumbs-sub-title {
margin: 0;
font-size: 12px;
font-weight: 600;
line-height: 16px;
a {
color: $gl-text-color;
}
}
.btn-sign-in {
margin-top: 3px;
font-weight: $gl-font-weight-bold;
&:hover {
background-color: $white-light;
}
}
...@@ -24,7 +24,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -24,7 +24,7 @@ $new-sidebar-collapsed-width: 50px;
// Override position: absolute // Override position: absolute
.right-sidebar { .right-sidebar {
position: fixed; position: fixed;
height: calc(100% - #{$new-navbar-height}); height: calc(100% - #{$header-height});
} }
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header { .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
...@@ -87,7 +87,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -87,7 +87,7 @@ $new-sidebar-collapsed-width: 50px;
z-index: 400; z-index: 400;
width: $new-sidebar-width; width: $new-sidebar-width;
transition: left $sidebar-transition-duration; transition: left $sidebar-transition-duration;
top: $new-navbar-height; top: $header-height;
bottom: 0; bottom: 0;
left: 0; left: 0;
background-color: $gray-normal; background-color: $gray-normal;
...@@ -197,7 +197,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -197,7 +197,7 @@ $new-sidebar-collapsed-width: 50px;
} }
.with-performance-bar .nav-sidebar { .with-performance-bar .nav-sidebar {
top: $new-navbar-height + $performance-bar-height; top: $header-height + $performance-bar-height;
} }
.sidebar-sub-level-items { .sidebar-sub-level-items {
...@@ -495,7 +495,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -495,7 +495,7 @@ $new-sidebar-collapsed-width: 50px;
// Make issue boards full-height now that sub-nav is gone // Make issue boards full-height now that sub-nav is gone
.boards-list { .boards-list {
height: calc(100vh - #{$new-navbar-height}); height: calc(100vh - #{$header-height});
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS height: 475px; // Needed for PhantomJS
...@@ -506,5 +506,5 @@ $new-sidebar-collapsed-width: 50px; ...@@ -506,5 +506,5 @@ $new-sidebar-collapsed-width: 50px;
} }
.with-performance-bar .boards-list { .with-performance-bar .boards-list {
height: calc(100vh - #{$new-navbar-height} - #{$performance-bar-height}); height: calc(100vh - #{$header-height} - #{$performance-bar-height});
} }
// For tabbed navigation links, scrolling tabs, etc. For all top/main navigation,
// please check nav.scss
.nav-links { .nav-links {
display: flex; display: flex;
padding: 0; padding: 0;
margin: 0; margin: 0;
list-style: none; list-style: none;
height: auto; height: auto;
border-bottom: 1px solid $border-color;
li { li {
display: flex; display: flex;
...@@ -24,7 +23,6 @@ ...@@ -24,7 +23,6 @@
&:active, &:active,
&:focus { &:focus {
text-decoration: none; text-decoration: none;
border-bottom: 2px solid $gray-darkest;
color: $black; color: $black;
.badge { .badge {
...@@ -34,7 +32,6 @@ ...@@ -34,7 +32,6 @@
} }
&.active a { &.active a {
border-bottom: 2px solid $link-underline-blue;
color: $black; color: $black;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
...@@ -43,35 +40,6 @@ ...@@ -43,35 +40,6 @@
} }
} }
} }
&.sub-nav {
text-align: center;
background-color: $gray-normal;
.container-fluid {
background-color: $gray-normal;
margin-bottom: 0;
display: flex;
}
li {
&.active a {
border-bottom: none;
color: $link-underline-blue;
}
a {
margin: 0;
padding: 11px 10px 9px;
&:hover,
&:active,
&:focus {
border-color: transparent;
}
}
}
}
} }
.top-area { .top-area {
...@@ -91,17 +59,6 @@ ...@@ -91,17 +59,6 @@
} }
} }
.nav-search {
display: inline-block;
width: 100%;
padding: 11px 0;
/* Small devices (phones, tablets, 768px and lower) */
@media (min-width: $screen-sm-min) {
width: 50%;
}
}
.nav-links { .nav-links {
margin-bottom: 0; margin-bottom: 0;
border-bottom: none; border-bottom: none;
...@@ -150,12 +107,6 @@ ...@@ -150,12 +107,6 @@
} }
} }
&.nav-controls-new-nav {
> .dropdown {
margin-right: 0;
}
}
> .btn-grouped { > .btn-grouped {
float: none; float: none;
} }
...@@ -248,114 +199,43 @@ ...@@ -248,114 +199,43 @@
pre { pre {
width: 100%; width: 100%;
} }
}
.project-item-select-holder.btn-group {
display: flex;
max-width: 350px;
overflow: hidden;
float: right;
.new-project-item-link {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.new-project-item-select-button {
width: 32px;
}
}
.empty-state .project-item-select-holder.btn-group {
float: none;
display: inline-block;
.btn {
// overrides styles applied to plain `.empty-state .btn`
margin: 10px 0;
max-width: 300px;
width: auto;
@media(max-width: $screen-xs-max) {
max-width: 250px;
}
}
}
.new-project-item-select-button .fa-caret-down { @media (max-width: $screen-xs-max) {
margin-left: 2px; flex-flow: row wrap;
}
.layout-nav {
width: 100%;
background: $gray-light;
border-bottom: 1px solid $border-color;
transition: padding $sidebar-transition-duration;
text-align: center;
margin-top: $new-navbar-height;
.container-fluid { .nav-controls {
position: relative; $controls-margin: $btn-xs-side-margin - 2px;
flex: 0 0 100%;
.nav-control { &.controls-flex {
@media (max-width: $screen-sm-max) { display: flex;
margin-right: 2px; flex-flow: row wrap;
align-items: center;
justify-content: center;
padding: 0 0 $gl-padding-top;
} }
}
}
.controls {
float: right;
padding: 7px 0 0;
i {
color: $layout-link-gray;
}
.fa-rss,
.fa-cog {
font-size: 16px;
}
.fa-caret-down { .controls-item,
margin-left: 5px; .controls-item-full,
color: $gl-text-color-secondary; .controls-item:last-child {
} flex: 1 1 35%;
display: block;
.dropdown { width: 100%;
position: absolute; margin: $controls-margin;
top: 7px;
right: 15px;
z-index: 300;
li.active { .btn,
font-weight: $gl-font-weight-bold; .dropdown {
margin: 0;
}
} }
}
}
.nav-links { .controls-item-full {
border-bottom: none; flex: 1 1 100%;
height: 51px;
@media (min-width: $screen-sm-min) {
justify-content: center;
}
li {
a {
padding-top: 10px;
} }
} }
} }
} }
.with-performance-bar .layout-nav {
margin-top: $header-height + $performance-bar-height;
}
.scrolling-tabs-container { .scrolling-tabs-container {
position: relative; position: relative;
...@@ -385,25 +265,41 @@ ...@@ -385,25 +265,41 @@
left: -7px; left: -7px;
} }
} }
}
&.sub-nav-scroll { .inner-page-scroll-tabs {
position: relative;
.fade-right { .fade-right {
@include fade(left, $gray-normal); @include fade(left, $white-light);
right: 0; right: 0;
text-align: right;
.fa { .fa {
right: -23px; right: 5px;
}
} }
}
.fade-left { .fade-left {
@include fade(right, $gray-normal); @include fade(right, $white-light);
left: 0; left: 0;
text-align: left;
.fa { .fa {
left: 10px; left: 5px;
} }
}
.fade-right,
.fade-left {
top: 16px;
bottom: auto;
}
&.is-smaller {
.fade-right,
.fade-left {
top: 11px;
} }
} }
} }
...@@ -432,41 +328,7 @@ ...@@ -432,41 +328,7 @@
} }
} }
} }
}
.page-with-layout-nav {
.right-sidebar {
top: ($header-height + 1) * 2;
}
&.page-with-sub-nav {
.right-sidebar {
top: ($header-height + 1) * 3;
&.affix {
top: $header-height;
}
}
}
}
.with-performance-bar .page-with-layout-nav {
.right-sidebar {
top: ($header-height + 1) * 2 + $performance-bar-height;
}
&.page-with-sub-nav {
.right-sidebar {
top: ($header-height + 1) * 3 + $performance-bar-height;
&.affix {
top: $header-height + $performance-bar-height;
}
}
}
}
.nav-block {
&.activities { &.activities {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
...@@ -476,76 +338,39 @@ ...@@ -476,76 +338,39 @@
} }
} }
@media (max-width: $screen-xs-max) { .project-item-select-holder.btn-group {
.top-area { display: flex;
flex-flow: row wrap; max-width: 350px;
overflow: hidden;
.nav-controls { float: right;
$controls-margin: $btn-xs-side-margin - 2px;
flex: 0 0 100%;
&.controls-flex {
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: center;
padding: 0 0 $gl-padding-top;
}
.controls-item,
.controls-item-full,
.controls-item:last-child {
flex: 1 1 35%;
display: block;
width: 100%;
margin: $controls-margin;
.btn, .new-project-item-link {
.dropdown { white-space: nowrap;
margin: 0; overflow: hidden;
} text-overflow: ellipsis;
} }
.controls-item-full { .new-project-item-select-button {
flex: 1 1 100%; width: 32px;
}
}
} }
} }
.inner-page-scroll-tabs { .empty-state .project-item-select-holder.btn-group {
position: relative; float: none;
display: inline-block;
.fade-right {
@include fade(left, $white-light);
right: 0;
text-align: right;
.fa {
right: 5px;
}
}
.fade-left { .btn {
@include fade(right, $white-light); // overrides styles applied to plain `.empty-state .btn`
left: 0; margin: 10px 0;
text-align: left; max-width: 300px;
width: auto;
.fa { @media(max-width: $screen-xs-max) {
left: 5px; max-width: 250px;
} }
} }
}
.fade-right, .new-project-item-select-button .fa-caret-down {
.fade-left { margin-left: 2px;
top: 16px;
bottom: auto;
}
&.is-smaller {
.fade-right,
.fade-left {
top: 11px;
}
}
} }
...@@ -78,16 +78,16 @@ ...@@ -78,16 +78,16 @@
.right-sidebar { .right-sidebar {
border-left: 1px solid $border-color; border-left: 1px solid $border-color;
height: calc(100% - #{$new-navbar-height}); height: calc(100% - #{$header-height});
&.affix { &.affix {
position: fixed; position: fixed;
top: $new-navbar-height; top: $header-height;
} }
} }
.with-performance-bar .right-sidebar.affix { .with-performance-bar .right-sidebar.affix {
top: $new-navbar-height + $performance-bar-height; top: $header-height + $performance-bar-height;
} }
@mixin maintain-sidebar-dimensions { @mixin maintain-sidebar-dimensions {
......
...@@ -17,15 +17,19 @@ ...@@ -17,15 +17,19 @@
.diff-file { .diff-file {
border: 1px solid $border-color; border: 1px solid $border-color;
border-bottom: none;
margin: 0; margin: 0;
} }
&.text-file .diff-file {
border-bottom: none;
}
} }
.timeline-entry { .timeline-entry {
border-color: $white-normal; border-color: $white-normal;
color: $gl-text-color; color: $gl-text-color;
border-bottom: 1px solid $border-white-light; border-bottom: 1px solid $border-white-light;
background: $white-light;
.timeline-entry-inner { .timeline-entry-inner {
position: relative; position: relative;
......
...@@ -225,8 +225,7 @@ $gl-sidebar-padding: 22px; ...@@ -225,8 +225,7 @@ $gl-sidebar-padding: 22px;
$row-hover: $blue-50; $row-hover: $blue-50;
$row-hover-border: $blue-200; $row-hover-border: $blue-200;
$progress-color: #c0392b; $progress-color: #c0392b;
$header-height: 50px; $header-height: 40px;
$new-navbar-height: 40px;
$fixed-layout-width: 1280px; $fixed-layout-width: 1280px;
$limited-layout-width: 990px; $limited-layout-width: 990px;
$limited-layout-width-sm: 790px; $limited-layout-width-sm: 790px;
...@@ -323,6 +322,7 @@ $diff-image-info-color: grey; ...@@ -323,6 +322,7 @@ $diff-image-info-color: grey;
$diff-swipe-border: #999; $diff-swipe-border: #999;
$diff-view-modes-color: grey; $diff-view-modes-color: grey;
$diff-view-modes-border: #c1c1c1; $diff-view-modes-border: #c1c1c1;
$diff-jagged-border-gradient-color: darken($white-normal, 8%);
/* /*
* Fonts * Fonts
...@@ -712,3 +712,9 @@ Issuable warning ...@@ -712,3 +712,9 @@ Issuable warning
*/ */
$issuable-warning-size: 24px; $issuable-warning-size: 24px;
$issuable-warning-icon-margin: 4px; $issuable-warning-icon-margin: 4px;
/*
Image Commenting cursor
*/
$image-comment-cursor-left-offset: 12;
$image-comment-cursor-top-offset: 30;
...@@ -414,7 +414,6 @@ ...@@ -414,7 +414,6 @@
margin: 5px; margin: 5px;
} }
.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar,
.page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar { .page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar {
.issuable-sidebar-header { .issuable-sidebar-header {
position: relative; position: relative;
......
...@@ -64,10 +64,10 @@ ...@@ -64,10 +64,10 @@
color: $gl-text-color; color: $gl-text-color;
position: sticky; position: sticky;
position: -webkit-sticky; position: -webkit-sticky;
top: $new-navbar-height; top: $header-height;
&.affix { &.affix {
top: $new-navbar-height; top: $header-height;
} }
// with sidebar // with sidebar
...@@ -174,10 +174,10 @@ ...@@ -174,10 +174,10 @@
.with-performance-bar .build-page { .with-performance-bar .build-page {
.top-bar { .top-bar {
top: $new-navbar-height + $performance-bar-height; top: $header-height + $performance-bar-height;
&.affix { &.affix {
top: $new-navbar-height + $performance-bar-height; top: $header-height + $performance-bar-height;
} }
} }
} }
......
...@@ -54,12 +54,15 @@ ...@@ -54,12 +54,15 @@
.mr-widget-pipeline-graph { .mr-widget-pipeline-graph {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
margin-right: 4px;
.stage-cell .stage-container { .stage-cell .stage-container {
margin: 3px 3px 3px 0; margin: 3px 3px 3px 0;
} }
.stage-container:last-child {
margin-right: 0;
}
.dropdown-menu { .dropdown-menu {
margin-top: 11px; margin-top: 11px;
} }
......
...@@ -297,6 +297,7 @@ ...@@ -297,6 +297,7 @@
.drag-track { .drag-track {
display: block; display: block;
position: absolute; position: absolute;
top: 0;
left: 12px; left: 12px;
height: 10px; height: 10px;
width: 276px; width: 276px;
...@@ -547,16 +548,23 @@ ...@@ -547,16 +548,23 @@
} }
.diff-notes-collapse { .diff-notes-collapse {
width: 19px; width: 24px;
height: 19px; height: 24px;
border-radius: 50%;
padding: 0; padding: 0;
transition: transform .1s ease-out; transition: transform .1s ease-out;
z-index: 100; z-index: 100;
.collapse-icon {
height: 50%;
width: 100%;
}
svg { svg {
vertical-align: text-top; vertical-align: middle;
} }
.collapse-icon,
path { path {
fill: $white-light; fill: $white-light;
} }
...@@ -644,3 +652,157 @@ ...@@ -644,3 +652,157 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.note-container {
background-color: $gray-light;
border-top: 1px solid $white-normal;
// double jagged line divider
.discussion-notes + .discussion-notes::before,
.discussion-notes + .discussion-form::before {
content: '';
position: relative;
display: block;
width: 100%;
height: 10px;
background-color: $white-light;
background-image: linear-gradient(45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
linear-gradient(225deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
linear-gradient(135deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
linear-gradient(-45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%);
background-position: 5px 5px,0 5px,0 5px,5px 5px;
background-size: 10px 10px;
background-repeat: repeat;
}
.notes {
position: relative;
}
.diff-notes-collapse {
position: absolute;
left: -12px;
}
}
.diff-file .note-container > .new-note,
.note-container .discussion-notes {
margin-left: 100px;
border-left: 1px solid $white-normal;
}
.notes.active {
.diff-file .note-container > .new-note,
.note-container .discussion-notes {
// Override our margin and border (set for diff tab)
// when user is on the discussion tab for MR
margin-left: inherit;
border-left: inherit;
}
}
.files:not([data-can-create-note]) .frame {
cursor: auto;
}
.frame.click-to-comment {
position: relative;
cursor: url(icon_image_comment.svg)
$image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
// Retina cursor
cursor: -webkit-image-set(url(icon_image_comment.svg) 1x, url(icon_image_comment@2x.svg) 2x)
$image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
.comment-indicator {
position: absolute;
padding: 0;
width: (2px * $image-comment-cursor-left-offset);
height: (1px * $image-comment-cursor-top-offset);
// center the indicator to match the top left click region
margin-top: (-1px * $image-comment-cursor-top-offset) + 2;
margin-left: (-1px * $image-comment-cursor-left-offset) + 1;
svg {
width: 100%;
height: 100%;
}
&:focus {
outline: none;
}
}
}
.frame .badge,
.image-diff-avatar-link .badge,
.notes > .badge {
position: absolute;
background-color: $blue-400;
color: $white-light;
border: $white-light 1px solid;
min-height: $gl-padding;
padding: 5px 8px;
border-radius: 12px;
&:focus {
outline: none;
}
}
.frame .badge,
.frame .image-comment-badge {
// Center align badges on the frame
transform: translate3d(-50%, -50%, 0);
}
.image-comment-badge {
@include btn-comment-icon;
position: absolute;
&.inverted {
border-color: $white-light;
}
}
.image-diff-avatar-link {
position: relative;
.badge,
.image-comment-badge {
top: 25px;
right: 8px;
}
}
.notes > .badge {
display: none;
left: -13px;
}
.discussion-notes {
min-height: 35px;
&:first-child {
// First child does not have the jagged borders
min-height: 25px;
}
&.collapsed {
background-color: $white-light;
.diff-notes-collapse,
.note,
.discussion-reply-holder, {
display: none;
}
.notes > .badge {
display: block;
}
}
}
.discussion-body .image .frame {
position: relative;
}
...@@ -222,7 +222,7 @@ ...@@ -222,7 +222,7 @@
.right-sidebar { .right-sidebar {
position: absolute; position: absolute;
top: $new-navbar-height; top: $header-height;
bottom: 0; bottom: 0;
right: 0; right: 0;
transition: width $right-sidebar-transition-duration; transition: width $right-sidebar-transition-duration;
...@@ -487,10 +487,10 @@ ...@@ -487,10 +487,10 @@
} }
.with-performance-bar .right-sidebar { .with-performance-bar .right-sidebar {
top: $new-navbar-height + $performance-bar-height; top: $header-height + $performance-bar-height;
.issuable-sidebar { .issuable-sidebar {
height: calc(100% - #{$new-navbar-height} - #{$performance-bar-height}); height: calc(100% - #{$header-height} - #{$performance-bar-height});
} }
} }
......
...@@ -649,7 +649,7 @@ ...@@ -649,7 +649,7 @@
} }
.merge-request-tabs-holder { .merge-request-tabs-holder {
top: $new-navbar-height; top: $header-height;
z-index: 200; z-index: 200;
background-color: $white-light; background-color: $white-light;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
...@@ -679,7 +679,7 @@ ...@@ -679,7 +679,7 @@
} }
.with-performance-bar .merge-request-tabs-holder { .with-performance-bar .merge-request-tabs-holder {
top: $new-navbar-height + $performance-bar-height; top: $header-height + $performance-bar-height;
} }
.merge-request-tabs { .merge-request-tabs {
......
...@@ -161,10 +161,13 @@ ...@@ -161,10 +161,13 @@
} }
.discussion-form { .discussion-form {
padding: $gl-padding-top $gl-padding $gl-padding;
background-color: $white-light; background-color: $white-light;
} }
.discussion-form-container {
padding: $gl-padding-top $gl-padding $gl-padding;
}
.discussion-notes .disabled-comment { .discussion-notes .disabled-comment {
padding: 6px 0; padding: 6px 0;
} }
......
...@@ -650,29 +650,12 @@ ul.notes { ...@@ -650,29 +650,12 @@ ul.notes {
} }
.add-diff-note { .add-diff-note {
@include btn-comment-icon;
opacity: 0; opacity: 0;
margin-top: -2px; margin-top: -2px;
border-radius: 50%;
background: $white-light;
padding: 1px 5px;
font-size: 12px;
color: $blue-500;
margin-left: -55px; margin-left: -55px;
position: absolute; position: absolute;
z-index: 10; z-index: 10;
width: 23px;
height: 23px;
border: 1px solid $blue-500;
&:hover {
background: $blue-500;
border-color: $blue-600;
color: $white-light;
}
&:active {
outline: 0;
}
} }
.discussion-body, .discussion-body,
......
...@@ -209,9 +209,11 @@ ...@@ -209,9 +209,11 @@
} }
.stage-cell { .stage-cell {
@media (min-width: $screen-md-min) { &.table-section {
min-width: 148px; @media (min-width: $screen-md-min) {
margin-right: -4px; min-width: 148px;
margin-right: -4px;
}
} }
.mini-pipeline-graph-dropdown-toggle svg { .mini-pipeline-graph-dropdown-toggle svg {
......
...@@ -47,6 +47,7 @@ input[type="checkbox"]:hover { ...@@ -47,6 +47,7 @@ input[type="checkbox"]:hover {
} }
.location-badge { .location-badge {
height: 32px;
font-size: 12px; font-size: 12px;
margin: -4px 4px -4px -4px; margin: -4px 4px -4px -4px;
line-height: 25px; line-height: 25px;
......
...@@ -3,9 +3,23 @@ ...@@ -3,9 +3,23 @@
# Automatically sets the layout and ensures an administrator is logged in # Automatically sets the layout and ensures an administrator is logged in
class Admin::ApplicationController < ApplicationController class Admin::ApplicationController < ApplicationController
before_action :authenticate_admin! before_action :authenticate_admin!
before_action :display_read_only_information
layout 'admin' layout 'admin'
def authenticate_admin! def authenticate_admin!
render_404 unless current_user.admin? render_404 unless current_user.admin?
end end
def display_read_only_information
return unless Gitlab::Database.read_only?
flash.now[:notice] = read_only_message
end
private
# Overridden in EE
def read_only_message
_('You are on a read-only GitLab instance.')
end
end end
...@@ -10,7 +10,7 @@ module Boards ...@@ -10,7 +10,7 @@ module Boards
def index def index
issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute
issues = issues.page(params[:page]).per(params[:per] || 20) issues = issues.page(params[:page]).per(params[:per] || 20)
make_sure_position_is_set(issues) make_sure_position_is_set(issues) if Gitlab::Database.read_write?
issues = issues.preload(:project, issues = issues.preload(:project,
:milestone, :milestone,
:assignees, :assignees,
......
...@@ -96,7 +96,8 @@ module NotesActions ...@@ -96,7 +96,8 @@ module NotesActions
id: note.id, id: note.id,
discussion_id: note.discussion_id(noteable), discussion_id: note.discussion_id(noteable),
html: note_html(note), html: note_html(note),
note: note.note note: note.note,
on_image: note.try(:on_image?)
) )
discussion = note.to_discussion(noteable) discussion = note.to_discussion(noteable)
...@@ -122,7 +123,9 @@ module NotesActions ...@@ -122,7 +123,9 @@ module NotesActions
def diff_discussion_html(discussion) def diff_discussion_html(discussion)
return unless discussion.diff_discussion? return unless discussion.diff_discussion?
if params[:view] == 'parallel' on_image = discussion.on_image?
if params[:view] == 'parallel' && !on_image
template = "discussions/_parallel_diff_discussion" template = "discussions/_parallel_diff_discussion"
locals = locals =
if params[:line_type] == 'old' if params[:line_type] == 'old'
...@@ -132,7 +135,9 @@ module NotesActions ...@@ -132,7 +135,9 @@ module NotesActions
end end
else else
template = "discussions/_diff_discussion" template = "discussions/_diff_discussion"
locals = { discussions: [discussion] } @fresh_discussion = true
locals = { discussions: [discussion], on_image: on_image }
end end
render_to_string( render_to_string(
......
class Dashboard::GroupsController < Dashboard::ApplicationController class Dashboard::GroupsController < Dashboard::ApplicationController
def index def index
@sort = params[:sort] || 'id_desc' @sort = params[:sort] || 'created_desc'
@groups = @groups =
if params[:parent_id] && Group.supports_nested_groups? if params[:parent_id] && Group.supports_nested_groups?
......
...@@ -2,6 +2,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController ...@@ -2,6 +2,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
include LfsRequest include LfsRequest
skip_before_action :lfs_check_access!, only: [:deprecated] skip_before_action :lfs_check_access!, only: [:deprecated]
before_action :lfs_check_batch_operation!, only: [:batch]
def batch def batch
unless objects.present? unless objects.present?
...@@ -90,4 +91,21 @@ class Projects::LfsApiController < Projects::GitHttpClientController ...@@ -90,4 +91,21 @@ class Projects::LfsApiController < Projects::GitHttpClientController
} }
} }
end end
def lfs_check_batch_operation!
if upload_request? && Gitlab::Database.read_only?
render(
json: {
message: lfs_read_only_message
},
content_type: 'application/vnd.git-lfs+json',
status: 403
)
end
end
# Overridden in EE
def lfs_read_only_message
_('You cannot write to this read-only GitLab instance.')
end
end end
...@@ -13,7 +13,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont ...@@ -13,7 +13,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
# Make sure merge requests created before 8.0 # Make sure merge requests created before 8.0
# have head file in refs/merge-requests/ # have head file in refs/merge-requests/
def ensure_ref_fetched def ensure_ref_fetched
@merge_request.ensure_ref_fetched @merge_request.ensure_ref_fetched if Gitlab::Database.read_write?
end end
def merge_request_params def merge_request_params
......
...@@ -120,10 +120,13 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -120,10 +120,13 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
end end
def selected_target_project def selected_target_project
if @project.id.to_s == params[:target_project_id] || @project.forked_project_link.nil? if @project.id.to_s == params[:target_project_id] || !@project.forked?
@project @project
elsif params[:target_project_id].present?
MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: @project)
.execute.find(params[:target_project_id])
else else
@project.forked_project_link.forked_from_project @project.forked_from_project
end end
end end
end end
...@@ -8,8 +8,7 @@ class SessionsController < Devise::SessionsController ...@@ -8,8 +8,7 @@ class SessionsController < Devise::SessionsController
prepend_before_action :check_initial_setup, only: [:new] prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor, prepend_before_action :authenticate_with_two_factor,
if: :two_factor_enabled?, only: [:create] if: :two_factor_enabled?, only: [:create]
prepend_before_action :store_redirect_path, only: [:new] prepend_before_action :store_redirect_uri, only: [:new]
before_action :auto_sign_in_with_provider, only: [:new] before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha before_action :load_recaptcha
...@@ -86,28 +85,36 @@ class SessionsController < Devise::SessionsController ...@@ -86,28 +85,36 @@ class SessionsController < Devise::SessionsController
end end
end end
def store_redirect_path def stored_redirect_uri
redirect_path = @redirect_to ||= stored_location_for(:redirect)
end
def store_redirect_uri
redirect_uri =
if request.referer.present? && (params['redirect_to_referer'] == 'yes') if request.referer.present? && (params['redirect_to_referer'] == 'yes')
referer_uri = URI(request.referer) URI(request.referer)
if referer_uri.host == Gitlab.config.gitlab.host
referer_uri.request_uri
else
request.fullpath
end
else else
request.fullpath URI(request.url)
end end
# Prevent a 'you are already signed in' message directly after signing: # Prevent a 'you are already signed in' message directly after signing:
# we should never redirect to '/users/sign_in' after signing in successfully. # we should never redirect to '/users/sign_in' after signing in successfully.
unless URI(redirect_path).path == new_user_session_path return true if redirect_uri.path == new_user_session_path
store_location_for(:redirect, redirect_path)
end redirect_to = redirect_uri.to_s if redirect_allowed_to?(redirect_uri)
@redirect_to = redirect_to
store_location_for(:redirect, redirect_to)
end
# Overridden in EE
def redirect_allowed_to?(uri)
uri.host == Gitlab.config.gitlab.host &&
uri.port == Gitlab.config.gitlab.port
end end
def two_factor_enabled? def two_factor_enabled?
find_user.try(:two_factor_enabled?) find_user&.two_factor_enabled?
end end
def auto_sign_in_with_provider def auto_sign_in_with_provider
......
class MergeRequestTargetProjectFinder
attr_reader :current_user, :source_project
def initialize(current_user: nil, source_project:)
@current_user = current_user
@source_project = source_project
end
def execute
if @source_project.fork_network
@source_project.fork_network.projects
.public_or_visible_to_user(current_user)
.with_feature_available_for_user(:merge_requests, current_user)
else
Project.where(id: source_project)
end
end
end
...@@ -73,7 +73,8 @@ module MergeRequestsHelper ...@@ -73,7 +73,8 @@ module MergeRequestsHelper
end end
def target_projects(project) def target_projects(project)
[project, project.default_merge_request_target].uniq MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: project)
.execute
end end
def merge_request_button_visibility(merge_request, closed) def merge_request_button_visibility(merge_request, closed)
......
...@@ -11,6 +11,7 @@ module Ci ...@@ -11,6 +11,7 @@ module Ci
has_many :deployments, as: :deployable has_many :deployments, as: :deployable
has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment' has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
# The "environment" field for builds is a String, and is the unexpanded name # The "environment" field for builds is a String, and is the unexpanded name
def persisted_environment def persisted_environment
...@@ -265,6 +266,10 @@ module Ci ...@@ -265,6 +266,10 @@ module Ci
update_attributes(coverage: coverage) if coverage.present? update_attributes(coverage: coverage) if coverage.present?
end end
def parse_trace_sections!
ExtractSectionsFromBuildTraceService.new(project, user).execute(self)
end
def trace def trace
Gitlab::Ci::Trace.new(self) Gitlab::Ci::Trace.new(self)
end end
......
module Ci
class BuildTraceSection < ActiveRecord::Base
extend Gitlab::Ci::Model
belongs_to :build, class_name: 'Ci::Build'
belongs_to :project
belongs_to :section_name, class_name: 'Ci::BuildTraceSectionName'
validates :section_name, :build, :project, presence: true, allow_blank: false
end
end
module Ci
class BuildTraceSectionName < ActiveRecord::Base
extend Gitlab::Ci::Model
belongs_to :project
has_many :trace_sections, class_name: 'Ci::BuildTraceSection', foreign_key: :section_name_id
validates :name, :project, presence: true, allow_blank: false
validates :name, uniqueness: { scope: :project_id }
end
end
...@@ -59,7 +59,7 @@ module CacheMarkdownField ...@@ -59,7 +59,7 @@ module CacheMarkdownField
# Update every column in a row if any one is invalidated, as we only store # Update every column in a row if any one is invalidated, as we only store
# one version per row # one version per row
def refresh_markdown_cache!(do_update: false) def refresh_markdown_cache
options = { skip_project_check: skip_project_check? } options = { skip_project_check: skip_project_check? }
updates = cached_markdown_fields.markdown_fields.map do |markdown_field| updates = cached_markdown_fields.markdown_fields.map do |markdown_field|
...@@ -71,8 +71,14 @@ module CacheMarkdownField ...@@ -71,8 +71,14 @@ module CacheMarkdownField
updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION
updates.each {|html_field, data| write_attribute(html_field, data) } updates.each {|html_field, data| write_attribute(html_field, data) }
end
def refresh_markdown_cache!
updates = refresh_markdown_cache
return unless persisted? && Gitlab::Database.read_write?
update_columns(updates) if persisted? && do_update update_columns(updates)
end end
def cached_html_up_to_date?(markdown_field) def cached_html_up_to_date?(markdown_field)
...@@ -124,8 +130,8 @@ module CacheMarkdownField ...@@ -124,8 +130,8 @@ module CacheMarkdownField
end end
# Using before_update here conflicts with elasticsearch-model somehow # Using before_update here conflicts with elasticsearch-model somehow
before_create :refresh_markdown_cache!, if: :invalidated_markdown_cache? before_create :refresh_markdown_cache, if: :invalidated_markdown_cache?
before_update :refresh_markdown_cache!, if: :invalidated_markdown_cache? before_update :refresh_markdown_cache, if: :invalidated_markdown_cache?
end end
class_methods do class_methods do
......
...@@ -28,6 +28,10 @@ module DiscussionOnDiff ...@@ -28,6 +28,10 @@ module DiscussionOnDiff
true true
end end
def file_new_path
first_note.position.new_path
end
# Returns an array of at most 16 highlighted lines above a diff note # Returns an array of at most 16 highlighted lines above a diff note
def truncated_diff_lines(highlight: true) def truncated_diff_lines(highlight: true)
lines = highlight ? highlighted_diff_lines : diff_lines lines = highlight ? highlighted_diff_lines : diff_lines
......
...@@ -256,23 +256,22 @@ module Issuable ...@@ -256,23 +256,22 @@ module Issuable
participants(user).include?(user) participants(user).include?(user)
end end
def to_hook_data(user) def to_hook_data(user, old_labels: [], old_assignees: [])
hook_data = { changes = previous_changes
object_kind: self.class.name.underscore,
user: user.hook_attrs, if old_labels != labels
project: project.hook_attrs, changes[:labels] = [old_labels.map(&:hook_attrs), labels.map(&:hook_attrs)]
object_attributes: hook_attrs, end
labels: labels.map(&:hook_attrs),
# DEPRECATED if old_assignees != assignees
repository: project.hook_attrs.slice(:name, :url, :description, :homepage) if self.is_a?(Issue)
} changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
if self.is_a?(Issue) else
hook_data[:assignees] = assignees.map(&:hook_attrs) if assignees.any? changes[:assignee] = [old_assignees&.first&.hook_attrs, assignee&.hook_attrs]
else end
hook_data[:assignee] = assignee.hook_attrs if assignee
end end
hook_data Gitlab::HookData::IssuableBuilder.new(self).build(user: user, changes: changes)
end end
def labels_array def labels_array
......
...@@ -156,6 +156,8 @@ module Routable ...@@ -156,6 +156,8 @@ module Routable
end end
def update_route def update_route
return if Gitlab::Database.read_only?
prepare_route prepare_route
route.save route.save
end end
......
...@@ -43,15 +43,17 @@ module TokenAuthenticatable ...@@ -43,15 +43,17 @@ module TokenAuthenticatable
write_attribute(token_field, token) if token write_attribute(token_field, token) if token
end end
# Returns a token, but only saves when the database is in read & write mode
define_method("ensure_#{token_field}!") do define_method("ensure_#{token_field}!") do
send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend
read_attribute(token_field) read_attribute(token_field)
end end
# Resets the token, but only saves when the database is in read & write mode
define_method("reset_#{token_field}!") do define_method("reset_#{token_field}!") do
write_new_token(token_field) write_new_token(token_field)
save! save! if Gitlab::Database.read_write?
end end
end end
end end
......
...@@ -11,6 +11,8 @@ class DiffDiscussion < Discussion ...@@ -11,6 +11,8 @@ class DiffDiscussion < Discussion
delegate :position, delegate :position,
:original_position, :original_position,
:change_position, :change_position,
:on_text?,
:on_image?,
to: :first_note to: :first_note
......
...@@ -12,8 +12,8 @@ class DiffNote < Note ...@@ -12,8 +12,8 @@ class DiffNote < Note
validates :original_position, presence: true validates :original_position, presence: true
validates :position, presence: true validates :position, presence: true
validates :diff_line, presence: true validates :diff_line, presence: true, if: :on_text?
validates :line_code, presence: true, line_code: true validates :line_code, presence: true, line_code: true, if: :on_text?
validates :noteable_type, inclusion: { in: NOTEABLE_TYPES } validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
validate :positions_complete validate :positions_complete
validate :verify_supported validate :verify_supported
...@@ -43,6 +43,14 @@ class DiffNote < Note ...@@ -43,6 +43,14 @@ class DiffNote < Note
end end
end end
def on_text?
position.position_type == "text"
end
def on_image?
position.position_type == "image"
end
def diff_file def diff_file
@diff_file ||= self.original_position.diff_file(self.project.repository) @diff_file ||= self.original_position.diff_file(self.project.repository)
end end
...@@ -56,6 +64,8 @@ class DiffNote < Note ...@@ -56,6 +64,8 @@ class DiffNote < Note
end end
def original_line_code def original_line_code
return unless on_text?
self.diff_file.line_code(self.diff_line) self.diff_file.line_code(self.diff_line)
end end
......
...@@ -66,6 +66,10 @@ class Discussion ...@@ -66,6 +66,10 @@ class Discussion
@context_noteable = context_noteable @context_noteable = context_noteable
end end
def on_image?
false
end
def ==(other) def ==(other)
other.class == self.class && other.class == self.class &&
other.context_noteable == self.context_noteable && other.context_noteable == self.context_noteable &&
......
class ForkNetwork < ActiveRecord::Base
belongs_to :root_project, class_name: 'Project'
has_many :fork_network_members
has_many :projects, through: :fork_network_members
after_create :add_root_as_member, if: :root_project
def add_root_as_member
projects << root_project
end
def find_forks_in(other_projects)
projects.where(id: other_projects)
end
end
class ForkNetworkMember < ActiveRecord::Base
belongs_to :fork_network
belongs_to :project
belongs_to :forked_from_project, class_name: 'Project'
validates :fork_network, :project, presence: true
end
...@@ -60,6 +60,8 @@ class GpgSignature < ActiveRecord::Base ...@@ -60,6 +60,8 @@ class GpgSignature < ActiveRecord::Base
end end
def gpg_commit def gpg_commit
return unless commit
Gitlab::Gpg::Commit.new(commit) Gitlab::Gpg::Commit.new(commit)
end end
end end
...@@ -74,20 +74,6 @@ class Issue < ActiveRecord::Base ...@@ -74,20 +74,6 @@ class Issue < ActiveRecord::Base
end end
end end
def hook_attrs
assignee_ids = self.assignee_ids
attrs = {
total_time_spent: total_time_spent,
human_total_time_spent: human_total_time_spent,
human_time_estimate: human_time_estimate,
assignee_ids: assignee_ids,
assignee_id: assignee_ids.first # This key is deprecated
}
attributes.merge!(attrs)
end
def self.reference_prefix def self.reference_prefix
'#' '#'
end end
...@@ -131,6 +117,10 @@ class Issue < ActiveRecord::Base ...@@ -131,6 +117,10 @@ class Issue < ActiveRecord::Base
"id DESC") "id DESC")
end end
def hook_attrs
Gitlab::HookData::IssueBuilder.new(self).build
end
# Returns a Hash of attributes to be used for Twitter card metadata # Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes def card_attributes
{ {
......
...@@ -17,6 +17,14 @@ class LegacyDiffDiscussion < Discussion ...@@ -17,6 +17,14 @@ class LegacyDiffDiscussion < Discussion
true true
end end
def on_image?
false
end
def on_text?
true
end
def active?(*args) def active?(*args)
return @active if @active.present? return @active if @active.present?
......
...@@ -179,6 +179,10 @@ class MergeRequest < ActiveRecord::Base ...@@ -179,6 +179,10 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}" work_in_progress?(title) ? title : "WIP: #{title}"
end end
def hook_attrs
Gitlab::HookData::MergeRequestBuilder.new(self).build
end
# Returns a Hash of attributes to be used for Twitter card metadata # Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes def card_attributes
{ {
...@@ -403,7 +407,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -403,7 +407,7 @@ class MergeRequest < ActiveRecord::Base
return false unless for_fork? return false unless for_fork?
return true unless source_project return true unless source_project
!source_project.forked_from?(target_project) !source_project.in_fork_network_of?(target_project)
end end
def reopenable? def reopenable?
...@@ -477,7 +481,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -477,7 +481,7 @@ class MergeRequest < ActiveRecord::Base
end end
def check_if_can_be_merged def check_if_can_be_merged
return unless unchecked? return unless unchecked? && Gitlab::Database.read_write?
can_be_merged = can_be_merged =
!broken? && project.repository.can_be_merged?(diff_head_sha, target_branch) !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
...@@ -587,24 +591,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -587,24 +591,6 @@ class MergeRequest < ActiveRecord::Base
!discussions_to_be_resolved? !discussions_to_be_resolved?
end end
def hook_attrs
attrs = {
source: source_project.try(:hook_attrs),
target: target_project.hook_attrs,
last_commit: nil,
work_in_progress: work_in_progress?,
total_time_spent: total_time_spent,
human_total_time_spent: human_total_time_spent,
human_time_estimate: human_time_estimate
}
if diff_head_commit
attrs[:last_commit] = diff_head_commit.hook_attrs
end
attributes.merge!(attrs)
end
def for_fork? def for_fork?
target_project != source_project target_project != source_project
end end
......
...@@ -139,7 +139,9 @@ class Namespace < ActiveRecord::Base ...@@ -139,7 +139,9 @@ class Namespace < ActiveRecord::Base
end end
def find_fork_of(project) def find_fork_of(project)
projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id) return nil unless project.fork_network
project.fork_network.find_forks_in(projects).first
end end
def lfs_enabled? def lfs_enabled?
......
...@@ -134,14 +134,22 @@ class Note < ActiveRecord::Base ...@@ -134,14 +134,22 @@ class Note < ActiveRecord::Base
Discussion.build(notes) Discussion.build(notes)
end end
# Group diff discussions by line code or file path.
# It is not needed to group by line code when comment is
# on an image.
def grouped_diff_discussions(diff_refs = nil) def grouped_diff_discussions(diff_refs = nil)
groups = {} groups = {}
diff_notes.fresh.discussions.each do |discussion| diff_notes.fresh.discussions.each do |discussion|
line_code = discussion.line_code_in_diffs(diff_refs) group_key =
if discussion.on_image?
if line_code discussion.file_new_path
discussions = groups[line_code] ||= [] else
discussion.line_code_in_diffs(diff_refs)
end
if group_key
discussions = groups[group_key] ||= []
discussions << discussion discussions << discussion
end end
end end
......
...@@ -118,11 +118,20 @@ class Project < ActiveRecord::Base ...@@ -118,11 +118,20 @@ class Project < ActiveRecord::Base
has_one :mock_monitoring_service has_one :mock_monitoring_service
has_one :microsoft_teams_service has_one :microsoft_teams_service
# TODO: replace these relations with the fork network versions
has_one :forked_project_link, foreign_key: "forked_to_project_id" has_one :forked_project_link, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link has_one :forked_from_project, through: :forked_project_link
has_many :forked_project_links, foreign_key: "forked_from_project_id" has_many :forked_project_links, foreign_key: "forked_from_project_id"
has_many :forks, through: :forked_project_links, source: :forked_to_project has_many :forks, through: :forked_project_links, source: :forked_to_project
# TODO: replace these relations with the fork network versions
has_one :root_of_fork_network,
foreign_key: 'root_project_id',
inverse_of: :root_project,
class_name: 'ForkNetwork'
has_one :fork_network_member
has_one :fork_network, through: :fork_network_member
# Merge Requests for target project should be removed with it # Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id' has_many :merge_requests, foreign_key: 'target_project_id'
...@@ -180,6 +189,7 @@ class Project < ActiveRecord::Base ...@@ -180,6 +189,7 @@ class Project < ActiveRecord::Base
# bulk that doesn't involve loading the rows into memory. As a result we're # bulk that doesn't involve loading the rows into memory. As a result we're
# still using `dependent: :destroy` here. # still using `dependent: :destroy` here.
has_many :builds, class_name: 'Ci::Build', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :builds, class_name: 'Ci::Build', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
has_many :runner_projects, class_name: 'Ci::RunnerProject' has_many :runner_projects, class_name: 'Ci::RunnerProject'
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, class_name: 'Ci::Variable' has_many :variables, class_name: 'Ci::Variable'
...@@ -814,7 +824,7 @@ class Project < ActiveRecord::Base ...@@ -814,7 +824,7 @@ class Project < ActiveRecord::Base
end end
def cache_has_external_issue_tracker def cache_has_external_issue_tracker
update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write?
end end
def has_wiki? def has_wiki?
...@@ -834,7 +844,7 @@ class Project < ActiveRecord::Base ...@@ -834,7 +844,7 @@ class Project < ActiveRecord::Base
end end
def cache_has_external_wiki def cache_has_external_wiki
update_column(:has_external_wiki, services.external_wikis.any?) update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
end end
def find_or_initialize_services(exceptions: []) def find_or_initialize_services(exceptions: [])
...@@ -999,6 +1009,11 @@ class Project < ActiveRecord::Base ...@@ -999,6 +1009,11 @@ class Project < ActiveRecord::Base
end end
def forked? def forked?
return true if fork_network && fork_network.root_project != self
# TODO: Use only the above conditional using the `fork_network`
# This is the old conditional that looks at the `forked_project_link`, we
# fall back to this while we're migrating the new models
!(forked_project_link.nil? || forked_project_link.forked_from_project.nil?) !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
end end
...@@ -1118,8 +1133,19 @@ class Project < ActiveRecord::Base ...@@ -1118,8 +1133,19 @@ class Project < ActiveRecord::Base
end end
end end
def forked_from?(project) def forked_from?(other_project)
forked? && project == forked_from_project forked? && forked_from_project == other_project
end
def in_fork_network_of?(other_project)
# TODO: Remove this in a next release when all fork_networks are populated
# This makes sure all MergeRequests remain valid while the projects don't
# have a fork_network yet.
return true if forked_from?(other_project)
return false if fork_network.nil? || other_project.fork_network.nil?
fork_network == other_project.fork_network
end end
def origin_merge_requests def origin_merge_requests
......
...@@ -3,6 +3,7 @@ require 'slack-notifier' ...@@ -3,6 +3,7 @@ require 'slack-notifier'
module ChatMessage module ChatMessage
class BaseMessage class BaseMessage
attr_reader :markdown attr_reader :markdown
attr_reader :user_full_name
attr_reader :user_name attr_reader :user_name
attr_reader :user_avatar attr_reader :user_avatar
attr_reader :project_name attr_reader :project_name
...@@ -12,10 +13,19 @@ module ChatMessage ...@@ -12,10 +13,19 @@ module ChatMessage
@markdown = params[:markdown] || false @markdown = params[:markdown] || false
@project_name = params.dig(:project, :path_with_namespace) || params[:project_name] @project_name = params.dig(:project, :path_with_namespace) || params[:project_name]
@project_url = params.dig(:project, :web_url) || params[:project_url] @project_url = params.dig(:project, :web_url) || params[:project_url]
@user_full_name = params.dig(:user, :name) || params[:user_full_name]
@user_name = params.dig(:user, :username) || params[:user_name] @user_name = params.dig(:user, :username) || params[:user_name]
@user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar] @user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar]
end end
def user_combined_name
if user_full_name.present?
"#{user_full_name} (#{user_name})"
else
user_name
end
end
def pretext def pretext
return message if markdown return message if markdown
......
...@@ -29,7 +29,7 @@ module ChatMessage ...@@ -29,7 +29,7 @@ module ChatMessage
def activity def activity
{ {
title: "Issue #{state} by #{user_name}", title: "Issue #{state} by #{user_combined_name}",
subtitle: "in #{project_link}", subtitle: "in #{project_link}",
text: issue_link, text: issue_link,
image: user_avatar image: user_avatar
...@@ -40,9 +40,9 @@ module ChatMessage ...@@ -40,9 +40,9 @@ module ChatMessage
def message def message
if state == 'opened' if state == 'opened'
"[#{project_link}] Issue #{state} by #{user_name}" "[#{project_link}] Issue #{state} by #{user_combined_name}"
else else
"[#{project_link}] Issue #{issue_link} #{state} by #{user_name}" "[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}"
end end
end end
......
...@@ -24,7 +24,7 @@ module ChatMessage ...@@ -24,7 +24,7 @@ module ChatMessage
def activity def activity
{ {
title: "Merge Request #{state} by #{user_name}", title: "Merge Request #{state} by #{user_combined_name}",
subtitle: "in #{project_link}", subtitle: "in #{project_link}",
text: merge_request_link, text: merge_request_link,
image: user_avatar image: user_avatar
...@@ -46,7 +46,7 @@ module ChatMessage ...@@ -46,7 +46,7 @@ module ChatMessage
end end
def merge_request_message def merge_request_message
"#{user_name} #{state} #{merge_request_link} in #{project_link}: #{title}" "#{user_combined_name} #{state} #{merge_request_link} in #{project_link}: #{title}"
end end
def merge_request_link def merge_request_link
......
...@@ -32,7 +32,7 @@ module ChatMessage ...@@ -32,7 +32,7 @@ module ChatMessage
def activity def activity
{ {
title: "#{user_name} #{link('commented on ' + target, note_url)}", title: "#{user_combined_name} #{link('commented on ' + target, note_url)}",
subtitle: "in #{project_link}", subtitle: "in #{project_link}",
text: formatted_title, text: formatted_title,
image: user_avatar image: user_avatar
...@@ -42,7 +42,7 @@ module ChatMessage ...@@ -42,7 +42,7 @@ module ChatMessage
private private
def message def message
"#{user_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*" "#{user_combined_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*"
end end
def format_title(title) def format_title(title)
......
...@@ -9,7 +9,7 @@ module ChatMessage ...@@ -9,7 +9,7 @@ module ChatMessage
def initialize(data) def initialize(data)
super super
@user_name = data.dig(:user, :name) || 'API' @user_name = data.dig(:user, :username) || 'API'
pipeline_attributes = data[:object_attributes] pipeline_attributes = data[:object_attributes]
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
...@@ -35,7 +35,7 @@ module ChatMessage ...@@ -35,7 +35,7 @@ module ChatMessage
def activity def activity
{ {
title: "Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_name} #{humanized_status}", title: "Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_combined_name} #{humanized_status}",
subtitle: "in #{project_link}", subtitle: "in #{project_link}",
text: "in #{pretty_duration(duration)}", text: "in #{pretty_duration(duration)}",
image: user_avatar || '' image: user_avatar || ''
...@@ -45,7 +45,7 @@ module ChatMessage ...@@ -45,7 +45,7 @@ module ChatMessage
private private
def message def message
"#{project_link}: Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_name} #{humanized_status} in #{pretty_duration(duration)}" "#{project_link}: Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_combined_name} #{humanized_status} in #{pretty_duration(duration)}"
end end
def humanized_status def humanized_status
......
...@@ -33,7 +33,7 @@ module ChatMessage ...@@ -33,7 +33,7 @@ module ChatMessage
end end
{ {
title: "#{user_name} #{action} #{ref_type}", title: "#{user_combined_name} #{action} #{ref_type}",
subtitle: "in #{project_link}", subtitle: "in #{project_link}",
text: compare_link, text: compare_link,
image: user_avatar image: user_avatar
...@@ -57,15 +57,15 @@ module ChatMessage ...@@ -57,15 +57,15 @@ module ChatMessage
end end
def new_branch_message def new_branch_message
"#{user_name} pushed new #{ref_type} #{branch_link} to #{project_link}" "#{user_combined_name} pushed new #{ref_type} #{branch_link} to #{project_link}"
end end
def removed_branch_message def removed_branch_message
"#{user_name} removed #{ref_type} #{ref} from #{project_link}" "#{user_combined_name} removed #{ref_type} #{ref} from #{project_link}"
end end
def push_message def push_message
"#{user_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})" "#{user_combined_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})"
end end
def commit_messages def commit_messages
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment