Commit 23334ac0 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into issue-discussions-refactor

* master: (481 commits)
  Make sure that we have author and committer
  disable file upload button while uploading
  Fix bar chart does not display label at hour 0
  Fixed activity not loading on project homepage
  Expose noteable_iid in Note
  Fix fly-out width when it has long items
  Add a test to show that threshold 40 would corrupt
  Add changelog entry
  Raise encoding confidence threshold to 50
  Fix the /projects/:id/repository/commits endpoint to handle dots in the ref name when the project full path contains a `/`
  Fix the /projects/:id/repository/tags endpoint to handle dots in the tag name when the project full path contains a `/`
  Add Italian translations of Pipeline Schedules
  Restrict InlineJavaScript for haml_lint to dev and test environment
  Incorporate Gitaly's CommitService.FindCommit RPC
  Move `deltas` and `diff_from_parents` logic to Gitlab::Git::Commit
  fix repo_edit_button_spec.js
  fix test failures in repo_preview_spec.js
  fix repo_loading_file_spec tests
  Refactor Gitlab::Git::Commit to include a repository
  use 100vh instead of flip flopping between the two - works on all suported browsers
  ...
parents 15441f0e 6a6a1e5b
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
"filenames/match-regex": [2, "^[a-z0-9_]+$"], "filenames/match-regex": [2, "^[a-z0-9_]+$"],
"import/no-commonjs": "error", "import/no-commonjs": "error",
"no-multiple-empty-lines": ["error", { "max": 1 }], "no-multiple-empty-lines": ["error", { "max": 1 }],
"promise/catch-or-return": "error" "promise/catch-or-return": "error",
"no-underscore-dangle": ["error", { "allow": ["__"]}]
} }
} }
...@@ -3,4 +3,5 @@ lib/gitlab/sanitizers/svg/whitelist.rb ...@@ -3,4 +3,5 @@ lib/gitlab/sanitizers/svg/whitelist.rb
lib/gitlab/diff/position_tracer.rb lib/gitlab/diff/position_tracer.rb
app/policies/project_policy.rb app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb app/models/concerns/relative_positioning.rb
app/workers/stuck_merge_jobs_worker.rb
lib/gitlab/redis/*.rb lib/gitlab/redis/*.rb
...@@ -35,9 +35,21 @@ linters: ...@@ -35,9 +35,21 @@ linters:
HtmlAttributes: HtmlAttributes:
enabled: true enabled: true
IdNames:
enabled: false
ImplicitDiv: ImplicitDiv:
enabled: true enabled: true
InlineJavaScript:
enabled: true
InlineStyles:
enabled: false
InstanceVariables:
enabled: false
LeadingCommentSpace: LeadingCommentSpace:
enabled: false enabled: false
...@@ -54,6 +66,9 @@ linters: ...@@ -54,6 +66,9 @@ linters:
ObjectReferenceAttributes: ObjectReferenceAttributes:
enabled: true enabled: true
RepeatedId:
enabled: false
RuboCop: RuboCop:
enabled: false enabled: false
# These cops are incredibly noisy when it comes to HAML templates, so we # These cops are incredibly noisy when it comes to HAML templates, so we
...@@ -101,3 +116,6 @@ linters: ...@@ -101,3 +116,6 @@ linters:
UnnecessaryStringOutput: UnnecessaryStringOutput:
enabled: true enabled: true
ViewLength:
enabled: false
...@@ -342,7 +342,7 @@ group :development, :test do ...@@ -342,7 +342,7 @@ group :development, :test do
gem 'rubocop', '~> 0.49.1', require: false gem 'rubocop', '~> 0.49.1', require: false
gem 'rubocop-rspec', '~> 1.15.1', require: false gem 'rubocop-rspec', '~> 1.15.1', require: false
gem 'scss_lint', '~> 0.54.0', require: false gem 'scss_lint', '~> 0.54.0', require: false
gem 'haml_lint', '~> 0.21.0', require: false gem 'haml_lint', '~> 0.26.0', require: false
gem 'simplecov', '~> 0.14.0', require: false gem 'simplecov', '~> 0.14.0', require: false
gem 'flay', '~> 2.8.0', require: false gem 'flay', '~> 2.8.0', require: false
gem 'bundler-audit', '~> 0.5.0', require: false gem 'bundler-audit', '~> 0.5.0', require: false
...@@ -390,8 +390,18 @@ gem 'health_check', '~> 2.6.0' ...@@ -390,8 +390,18 @@ gem 'health_check', '~> 2.6.0'
gem 'vmstat', '~> 2.3.0' gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6' gem 'sys-filesystem', '~> 1.1.6'
# SSH host key support
gem 'net-ssh', '~> 4.1.0'
# Required for ED25519 SSH host key support
group :ed25519 do
gem 'rbnacl-libsodium'
gem 'rbnacl', '~> 3.2'
gem 'bcrypt_pbkdf', '~> 1.0'
end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly', '~> 0.24.0' gem 'gitaly', '~> 0.26.0'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -75,6 +75,7 @@ GEM ...@@ -75,6 +75,7 @@ GEM
babosa (1.0.2) babosa (1.0.2)
base32 (0.3.2) base32 (0.3.2)
bcrypt (3.1.11) bcrypt (3.1.11)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0) benchmark-ips (2.3.0)
better_errors (2.1.1) better_errors (2.1.1)
coderay (>= 1.0.0) coderay (>= 1.0.0)
...@@ -269,7 +270,7 @@ GEM ...@@ -269,7 +270,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly (0.24.0) gitaly (0.26.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -356,10 +357,11 @@ GEM ...@@ -356,10 +357,11 @@ GEM
googleauth (~> 0.5.1) googleauth (~> 0.5.1)
haml (4.0.7) haml (4.0.7)
tilt tilt
haml_lint (0.21.0) haml_lint (0.26.0)
haml (~> 4.0) haml (>= 4.0, < 5.1)
rainbow
rake (>= 10, < 13) rake (>= 10, < 13)
rubocop (>= 0.47.0) rubocop (>= 0.49.0)
sysexits (~> 1.1) sysexits (~> 1.1)
hamlit (2.6.1) hamlit (2.6.1)
temple (~> 0.7.6) temple (~> 0.7.6)
...@@ -474,6 +476,7 @@ GEM ...@@ -474,6 +476,7 @@ GEM
mustermann (~> 1.0.0) mustermann (~> 1.0.0)
mysql2 (0.4.5) mysql2 (0.4.5)
net-ldap (0.16.0) net-ldap (0.16.0)
net-ssh (4.1.0)
netrc (0.11.0) netrc (0.11.0)
nokogiri (1.6.8.1) nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0) mini_portile2 (~> 2.1.0)
...@@ -661,6 +664,10 @@ GEM ...@@ -661,6 +664,10 @@ GEM
rake (12.0.0) rake (12.0.0)
rblineprof (0.3.6) rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3) debugger-ruby_core_source (~> 1.3)
rbnacl (3.4.0)
ffi
rbnacl-libsodium (1.0.11)
rbnacl (>= 3.0.1)
rdoc (4.2.2) rdoc (4.2.2)
json (~> 1.4) json (~> 1.4)
re2 (1.1.1) re2 (1.1.1)
...@@ -923,6 +930,7 @@ DEPENDENCIES ...@@ -923,6 +930,7 @@ DEPENDENCIES
awesome_print (~> 1.2.0) awesome_print (~> 1.2.0)
babosa (~> 1.0.2) babosa (~> 1.0.2)
base32 (~> 0.3.0) base32 (~> 0.3.0)
bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0) benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0) better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
...@@ -974,7 +982,7 @@ DEPENDENCIES ...@@ -974,7 +982,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.24.0) gitaly (~> 0.26.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1) gitlab-markup (~> 1.5.1)
...@@ -987,7 +995,7 @@ DEPENDENCIES ...@@ -987,7 +995,7 @@ DEPENDENCIES
grape (~> 0.19.2) grape (~> 0.19.2)
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.0.0) grape-route-helpers (~> 2.0.0)
haml_lint (~> 0.21.0) haml_lint (~> 0.26.0)
hamlit (~> 2.6.1) hamlit (~> 2.6.1)
hashie-forbidden_attributes hashie-forbidden_attributes
health_check (~> 2.6.0) health_check (~> 2.6.0)
...@@ -1015,6 +1023,7 @@ DEPENDENCIES ...@@ -1015,6 +1023,7 @@ DEPENDENCIES
mousetrap-rails (~> 1.4.6) mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.5) mysql2 (~> 0.4.5)
net-ldap net-ldap
net-ssh (~> 4.1.0)
nokogiri (~> 1.6.7, >= 1.6.7.2) nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.4) oauth2 (~> 1.4)
octokit (~> 4.6.2) octokit (~> 4.6.2)
...@@ -1061,6 +1070,8 @@ DEPENDENCIES ...@@ -1061,6 +1070,8 @@ DEPENDENCIES
rainbow (~> 2.2) rainbow (~> 2.2)
raindrops (~> 0.18) raindrops (~> 0.18)
rblineprof (~> 0.3.6) rblineprof (~> 0.3.6)
rbnacl (~> 3.2)
rbnacl-libsodium
rdoc (~> 4.2) rdoc (~> 4.2)
re2 (~> 1.1.1) re2 (~> 1.1.1)
recaptcha (~> 3.0) recaptcha (~> 3.0)
......
...@@ -13,6 +13,7 @@ const Api = { ...@@ -13,6 +13,7 @@ const Api = {
dockerfilePath: '/api/:version/templates/dockerfiles/:key', dockerfilePath: '/api/:version/templates/dockerfiles/:key',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json', usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits',
group(groupId, callback) { group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath) const url = Api.buildUrl(Api.groupPath)
...@@ -95,6 +96,21 @@ const Api = { ...@@ -95,6 +96,21 @@ const Api = {
.done(projects => callback(projects)); .done(projects => callback(projects));
}, },
commitMultiple(id, data, callback) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const url = Api.buildUrl(Api.commitPath)
.replace(':id', id);
return $.ajax({
url,
type: 'POST',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(data),
dataType: 'json',
})
.done(commitData => callback(commitData))
.fail(message => callback(message.responseJSON));
},
// Return text for a specific license // Return text for a specific license
licenseText(key, data, callback) { licenseText(key, data, callback) {
const url = Api.buildUrl(Api.licensePath) const url = Api.buildUrl(Api.licensePath)
......
/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */ /* eslint-disable func-names, object-shorthand, prefer-arrow-callback */
/* global Dropzone */ /* global Dropzone */
import '../lib/utils/url_utility';
import { HIDDEN_CLASS } from '../lib/utils/constants';
function toggleLoading($el, $icon, loading) {
if (loading) {
$el.disable();
$icon.removeClass(HIDDEN_CLASS);
} else {
$el.enable();
$icon.addClass(HIDDEN_CLASS);
}
}
export default class BlobFileDropzone { export default class BlobFileDropzone {
constructor(form, method) { constructor(form, method) {
const formDropzone = form.find('.dropzone'); const formDropzone = form.find('.dropzone');
const submitButton = form.find('#submit-all');
const submitButtonLoadingIcon = submitButton.find('.js-loading-icon');
const dropzoneMessage = form.find('.dz-message');
Dropzone.autoDiscover = false; Dropzone.autoDiscover = false;
const dropzone = formDropzone.dropzone({ const dropzone = formDropzone.dropzone({
...@@ -26,12 +41,20 @@ export default class BlobFileDropzone { ...@@ -26,12 +41,20 @@ export default class BlobFileDropzone {
}, },
init: function () { init: function () {
this.on('addedfile', function () { this.on('addedfile', function () {
toggleLoading(submitButton, submitButtonLoadingIcon, false);
dropzoneMessage.addClass(HIDDEN_CLASS);
$('.dropzone-alerts').html('').hide(); $('.dropzone-alerts').html('').hide();
}); });
this.on('removedfile', function () {
toggleLoading(submitButton, submitButtonLoadingIcon, false);
dropzoneMessage.removeClass(HIDDEN_CLASS);
});
this.on('success', function (header, response) { this.on('success', function (header, response) {
window.location.href = response.filePath; $('#modal-upload-blob').modal('hide');
window.gl.utils.visitUrl(response.filePath);
}); });
this.on('maxfilesexceeded', function (file) { this.on('maxfilesexceeded', function (file) {
dropzoneMessage.addClass(HIDDEN_CLASS);
this.removeFile(file); this.removeFile(file);
}); });
this.on('sending', function (file, xhr, formData) { this.on('sending', function (file, xhr, formData) {
...@@ -48,14 +71,15 @@ export default class BlobFileDropzone { ...@@ -48,14 +71,15 @@ export default class BlobFileDropzone {
}, },
}); });
const submitButton = form.find('#submit-all')[0]; submitButton.on('click', (e) => {
submitButton.addEventListener('click', function (e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (dropzone[0].dropzone.getQueuedFiles().length === 0) { if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
alert('Please select a file'); alert('Please select a file');
return false;
} }
toggleLoading(submitButton, submitButtonLoadingIcon, true);
dropzone[0].dropzone.processQueue(); dropzone[0].dropzone.processQueue();
return false; return false;
}); });
......
...@@ -97,9 +97,8 @@ gl.issueBoards.IssueCardInner = Vue.extend({ ...@@ -97,9 +97,8 @@ gl.issueBoards.IssueCardInner = Vue.extend({
return `Avatar for ${assignee.name}`; return `Avatar for ${assignee.name}`;
}, },
showLabel(label) { showLabel(label) {
if (!this.list) return true; if (!this.list || !label) return true;
return true;
return !this.list.label || label.id !== this.list.label.id;
}, },
filterByLabel(label, e) { filterByLabel(label, e) {
if (!this.updateFilters) return; if (!this.updateFilters) return;
......
...@@ -164,7 +164,6 @@ window.Build = (function () { ...@@ -164,7 +164,6 @@ window.Build = (function () {
Build.prototype.initSidebar = function () { Build.prototype.initSidebar = function () {
this.$sidebar = $('.js-build-sidebar'); this.$sidebar = $('.js-build-sidebar');
this.$sidebar.niceScroll();
}; };
Build.prototype.getBuildTrace = function () { Build.prototype.getBuildTrace = function () {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
$(function() { $(function() {
$('.reveal-variables').off('click').on('click', function() { $('.reveal-variables').off('click').on('click', function() {
$('.js-build').toggle().niceScroll(); $('.js-build-variables').toggle();
$(this).hide(); $(this).hide();
}); });
}); });
...@@ -3,13 +3,13 @@ import $ from 'jquery'; ...@@ -3,13 +3,13 @@ import $ from 'jquery';
// bootstrap jQuery plugins // bootstrap jQuery plugins
import 'bootstrap-sass/assets/javascripts/bootstrap/affix'; import 'bootstrap-sass/assets/javascripts/bootstrap/affix';
import 'bootstrap-sass/assets/javascripts/bootstrap/alert'; import 'bootstrap-sass/assets/javascripts/bootstrap/alert';
import 'bootstrap-sass/assets/javascripts/bootstrap/button';
import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown'; import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown';
import 'bootstrap-sass/assets/javascripts/bootstrap/modal'; import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
import 'bootstrap-sass/assets/javascripts/bootstrap/tab'; import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
import 'bootstrap-sass/assets/javascripts/bootstrap/transition'; import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip'; import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
import 'bootstrap-sass/assets/javascripts/bootstrap/popover'; import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
import 'bootstrap-sass/assets/javascripts/bootstrap/button';
// custom jQuery functions // custom jQuery functions
$.fn.extend({ $.fn.extend({
......
...@@ -6,6 +6,5 @@ import 'vendor/jquery.endless-scroll'; ...@@ -6,6 +6,5 @@ import 'vendor/jquery.endless-scroll';
import 'vendor/jquery.caret'; import 'vendor/jquery.caret';
import 'vendor/jquery.atwho'; import 'vendor/jquery.atwho';
import 'vendor/jquery.scrollTo'; import 'vendor/jquery.scrollTo';
import 'vendor/jquery.nicescroll';
import 'vendor/jquery.waitforimages'; import 'vendor/jquery.waitforimages';
import 'select2/select2'; import 'select2/select2';
...@@ -75,6 +75,7 @@ import initNotes from './init_notes'; ...@@ -75,6 +75,7 @@ import initNotes from './init_notes';
import initLegacyFilters from './init_legacy_filters'; import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar'; import initIssuableSidebar from './init_issuable_sidebar';
import GpgBadges from './gpg_badges'; import GpgBadges from './gpg_badges';
import UserFeatureHelper from './helpers/user_feature_helper';
(function() { (function() {
var Dispatcher; var Dispatcher;
...@@ -92,6 +93,7 @@ import GpgBadges from './gpg_badges'; ...@@ -92,6 +93,7 @@ import GpgBadges from './gpg_badges';
if (!page) { if (!page) {
return false; return false;
} }
path = page.split(':'); path = page.split(':');
shortcut_handler = null; shortcut_handler = null;
...@@ -331,19 +333,16 @@ import GpgBadges from './gpg_badges'; ...@@ -331,19 +333,16 @@ import GpgBadges from './gpg_badges';
break; break;
case 'projects:commits:show': case 'projects:commits:show':
CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit); CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit);
new gl.Activities();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
GpgBadges.fetch(); GpgBadges.fetch();
break; break;
case 'projects:show': case 'projects:show':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new NotificationsForm(); new NotificationsForm();
if ($('#tree-slider').length) {
new TreeView(); if ($('#tree-slider').length) new TreeView();
} if ($('.blob-viewer').length) new BlobViewer();
if ($('.blob-viewer').length) { if ($('.project-show-activity').length) new gl.Activities();
new BlobViewer();
}
break; break;
case 'projects:edit': case 'projects:edit':
setupProjectEdit(); setupProjectEdit();
...@@ -407,6 +406,9 @@ import GpgBadges from './gpg_badges'; ...@@ -407,6 +406,9 @@ import GpgBadges from './gpg_badges';
break; break;
case 'projects:tree:show': case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
if (UserFeatureHelper.isNewRepo()) break;
new TreeView(); new TreeView();
new BlobViewer(); new BlobViewer();
new NewCommitForm($('.js-create-dir-form')); new NewCommitForm($('.js-create-dir-form'));
...@@ -425,6 +427,7 @@ import GpgBadges from './gpg_badges'; ...@@ -425,6 +427,7 @@ import GpgBadges from './gpg_badges';
shortcut_handler = true; shortcut_handler = true;
break; break;
case 'projects:blob:show': case 'projects:blob:show':
if (UserFeatureHelper.isNewRepo()) break;
new BlobViewer(); new BlobViewer();
initBlob(); initBlob();
break; break;
...@@ -577,7 +580,6 @@ import GpgBadges from './gpg_badges'; ...@@ -577,7 +580,6 @@ import GpgBadges from './gpg_badges';
shortcut_handler = new ShortcutsWiki(); shortcut_handler = new ShortcutsWiki();
new ZenMode(); new ZenMode();
new gl.GLForm($('.wiki-form'), true); new gl.GLForm($('.wiki-form'), true);
new Sidebar();
break; break;
case 'snippets': case 'snippets':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
......
...@@ -23,6 +23,7 @@ export const showSubLevelItems = (el) => { ...@@ -23,6 +23,7 @@ export const showSubLevelItems = (el) => {
const top = calculateTop(boundingRect, subItems.offsetHeight); const top = calculateTop(boundingRect, subItems.offsetHeight);
const isAbove = top < boundingRect.top; const isAbove = top < boundingRect.top;
subItems.classList.add('fly-out-list');
subItems.style.transform = `translate3d(0, ${Math.floor(top)}px, 0)`; subItems.style.transform = `translate3d(0, ${Math.floor(top)}px, 0)`;
if (isAbove) { if (isAbove) {
......
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */ /* eslint-disable func-names, no-underscore-dangle, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */ /* global fuzzaldrinPlus */
import _ from 'underscore'; import _ from 'underscore';
import { isObject } from './lib/utils/type_utility'; import { isObject } from './lib/utils/type_utility';
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote; var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput;
GitLabDropdownInput = (function() {
function GitLabDropdownInput(input, options) {
var $inputContainer, $clearButton;
var _this = this;
this.input = input;
this.options = options;
this.fieldName = this.options.fieldName || 'field-name';
$inputContainer = this.input.parent();
$clearButton = $inputContainer.find('.js-dropdown-input-clear');
$clearButton.on('click', (function(_this) {
// Clear click
return function(e) {
e.preventDefault();
e.stopPropagation();
return _this.input.val('').trigger('input').focus();
};
})(this));
this.input
.on('keydown', function (e) {
var keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) {
e.preventDefault();
}
})
.on('input', function(e) {
var val = e.currentTarget.value || _this.options.inputFieldName;
val = val.split(' ').join('-') // replaces space with dash
.replace(/[^a-zA-Z0-9 -]/g, '').toLowerCase() // replace non alphanumeric
.replace(/(-)\1+/g, '-'); // replace repeated dashes
_this.cb(_this.options.fieldName, val, {}, true);
_this.input.closest('.dropdown')
.find('.dropdown-toggle-text')
.text(val);
});
}
GitLabDropdownInput.prototype.onInput = function(cb) {
this.cb = cb;
};
return GitLabDropdownInput;
})();
GitLabDropdownFilter = (function() { GitLabDropdownFilter = (function() {
var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS; var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS;
...@@ -191,7 +235,7 @@ GitLabDropdownRemote = (function() { ...@@ -191,7 +235,7 @@ GitLabDropdownRemote = (function() {
})(); })();
GitLabDropdown = (function() { GitLabDropdown = (function() {
var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex; var ACTIVE_CLASS, FILTER_INPUT, NO_FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
LOADING_CLASS = "is-loading"; LOADING_CLASS = "is-loading";
...@@ -209,7 +253,9 @@ GitLabDropdown = (function() { ...@@ -209,7 +253,9 @@ GitLabDropdown = (function() {
CURSOR_SELECT_SCROLL_PADDING = 5; CURSOR_SELECT_SCROLL_PADDING = 5;
FILTER_INPUT = '.dropdown-input .dropdown-input-field'; FILTER_INPUT = '.dropdown-input .dropdown-input-field:not(.dropdown-no-filter)';
NO_FILTER_INPUT = '.dropdown-input .dropdown-input-field.dropdown-no-filter';
function GitLabDropdown(el1, options) { function GitLabDropdown(el1, options) {
var searchFields, selector, self; var searchFields, selector, self;
...@@ -224,6 +270,7 @@ GitLabDropdown = (function() { ...@@ -224,6 +270,7 @@ GitLabDropdown = (function() {
this.dropdown = selector != null ? $(selector) : $(this.el).parent(); this.dropdown = selector != null ? $(selector) : $(this.el).parent();
// Set Defaults // Set Defaults
this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT); this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT);
this.highlight = !!this.options.highlight; this.highlight = !!this.options.highlight;
this.filterInputBlur = this.options.filterInputBlur != null this.filterInputBlur = this.options.filterInputBlur != null
? this.options.filterInputBlur ? this.options.filterInputBlur
...@@ -262,6 +309,10 @@ GitLabDropdown = (function() { ...@@ -262,6 +309,10 @@ GitLabDropdown = (function() {
}); });
} }
} }
if (this.noFilterInput.length) {
this.plainInput = new GitLabDropdownInput(this.noFilterInput, this.options);
this.plainInput.onInput(this.addInput.bind(this));
}
// Init filterable // Init filterable
if (this.options.filterable) { if (this.options.filterable) {
this.filter = new GitLabDropdownFilter(this.filterInput, { this.filter = new GitLabDropdownFilter(this.filterInput, {
...@@ -753,9 +804,13 @@ GitLabDropdown = (function() { ...@@ -753,9 +804,13 @@ GitLabDropdown = (function() {
} }
}; };
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) { GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject, single) {
var $input; var $input;
// Create hidden input for form // Create hidden input for form
if (single) {
$('input[name="' + fieldName + '"]').remove();
}
$input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value); $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
if (this.options.inputId != null) { if (this.options.inputId != null) {
$input.attr('id', this.options.inputId); $input.attr('id', this.options.inputId);
...@@ -771,7 +826,7 @@ GitLabDropdown = (function() { ...@@ -771,7 +826,7 @@ GitLabDropdown = (function() {
$input.attr('data-meta', selectedObject[this.options.inputMeta]); $input.attr('data-meta', selectedObject[this.options.inputMeta]);
} }
return this.dropdown.before($input); this.dropdown.before($input).trigger('change');
}; };
GitLabDropdown.prototype.selectRowAtIndex = function(index) { GitLabDropdown.prototype.selectRowAtIndex = function(index) {
......
import Chart from 'vendor/Chart'; import Chart from 'vendor/Chart';
import _ from 'underscore';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const projectChartData = JSON.parse(document.getElementById('projectChartData').innerHTML); const projectChartData = JSON.parse(document.getElementById('projectChartData').innerHTML);
...@@ -27,28 +28,25 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -27,28 +28,25 @@ document.addEventListener('DOMContentLoaded', () => {
return generateChart(); return generateChart();
}; };
const chartData = (keys, values) => { const chartData = data => ({
const data = { labels: Object.keys(data),
labels: keys, datasets: [{
datasets: [{ fillColor: 'rgba(220,220,220,0.5)',
fillColor: 'rgba(220,220,220,0.5)', strokeColor: 'rgba(220,220,220,1)',
strokeColor: 'rgba(220,220,220,1)', barStrokeWidth: 1,
barStrokeWidth: 1, barValueSpacing: 1,
barValueSpacing: 1, barDatasetSpacing: 1,
barDatasetSpacing: 1, data: _.values(data),
data: values, }],
}], });
};
return data; const hourData = chartData(projectChartData.hour);
};
const hourData = chartData(projectChartData.hour.keys, projectChartData.hour.values);
responsiveChart($('#hour-chart'), hourData); responsiveChart($('#hour-chart'), hourData);
const dayData = chartData(projectChartData.weekDays.keys, projectChartData.weekDays.values); const dayData = chartData(projectChartData.weekDays);
responsiveChart($('#weekday-chart'), dayData); responsiveChart($('#weekday-chart'), dayData);
const monthData = chartData(projectChartData.month.keys, projectChartData.month.values); const monthData = chartData(projectChartData.month);
responsiveChart($('#month-chart'), monthData); responsiveChart($('#month-chart'), monthData);
const data = projectChartData.languages; const data = projectChartData.languages;
......
import Cookies from 'js-cookie';
function isNewRepo() {
return Cookies.get('new_repo') === 'true';
}
const UserFeatureHelper = {
isNewRepo,
};
export default UserFeatureHelper;
/* eslint-disable import/prefer-default-export */ /* eslint-disable import/prefer-default-export */
export const BYTES_IN_KIB = 1024; export const BYTES_IN_KIB = 1024;
export const HIDDEN_CLASS = 'hidden';
import Cookies from 'js-cookie';
import _ from 'underscore';
/* global bp */
import './breakpoints';
export default class NewNavSidebar { export default class NewNavSidebar {
constructor() { constructor() {
this.initDomElements(); this.initDomElements();
this.render();
} }
initDomElements() { initDomElements() {
this.$page = $('.page-with-sidebar');
this.$sidebar = $('.nav-sidebar'); this.$sidebar = $('.nav-sidebar');
this.$overlay = $('.mobile-overlay'); this.$overlay = $('.mobile-overlay');
this.$openSidebar = $('.toggle-mobile-nav'); this.$openSidebar = $('.toggle-mobile-nav');
this.$closeSidebar = $('.close-nav-button'); this.$closeSidebar = $('.close-nav-button');
this.$sidebarToggle = $('.js-toggle-sidebar');
} }
bindEvents() { bindEvents() {
this.$openSidebar.on('click', () => this.toggleSidebarNav(true)); this.$openSidebar.on('click', () => this.toggleSidebarNav(true));
this.$closeSidebar.on('click', () => this.toggleSidebarNav(false)); this.$closeSidebar.on('click', () => this.toggleSidebarNav(false));
this.$overlay.on('click', () => this.toggleSidebarNav(false)); this.$overlay.on('click', () => this.toggleSidebarNav(false));
this.$sidebarToggle.on('click', () => {
const value = !this.$sidebar.hasClass('sidebar-icons-only');
this.toggleCollapsedSidebar(value);
});
$(window).on('resize', () => _.debounce(this.render(), 100));
}
static setCollapsedCookie(value) {
if (bp.getBreakpointSize() !== 'lg') {
return;
}
Cookies.set('sidebar_collapsed', value, { expires: 365 * 10 });
} }
toggleSidebarNav(show) { toggleSidebarNav(show) {
this.$sidebar.toggleClass('nav-sidebar-expanded', show); this.$sidebar.toggleClass('nav-sidebar-expanded', show);
this.$overlay.toggleClass('mobile-nav-open', show); this.$overlay.toggleClass('mobile-nav-open', show);
this.$sidebar.removeClass('sidebar-icons-only');
}
toggleCollapsedSidebar(collapsed) {
this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
if (this.$sidebar.length) {
this.$page.toggleClass('page-with-new-sidebar', !collapsed);
this.$page.toggleClass('page-with-icon-sidebar', collapsed);
}
NewNavSidebar.setCollapsedCookie(collapsed);
}
render() {
const breakpoint = bp.getBreakpointSize();
if (breakpoint === 'sm' || breakpoint === 'md') {
this.toggleCollapsedSidebar(true);
} else if (breakpoint === 'lg') {
const collapse = Cookies.get('sidebar_collapsed') === 'true';
this.toggleCollapsedSidebar(collapse);
}
} }
} }
...@@ -90,6 +90,7 @@ import Cookies from 'js-cookie'; ...@@ -90,6 +90,7 @@ import Cookies from 'js-cookie';
filterable: true, filterable: true,
filterRemote: true, filterRemote: true,
filterByText: true, filterByText: true,
inputFieldName: $dropdown.data('input-field-name'),
fieldName: $dropdown.data('field-name'), fieldName: $dropdown.data('field-name'),
renderRow: function(ref) { renderRow: function(ref) {
var li = refListItem.cloneNode(false); var li = refListItem.cloneNode(false);
...@@ -123,9 +124,14 @@ import Cookies from 'js-cookie'; ...@@ -123,9 +124,14 @@ import Cookies from 'js-cookie';
e.preventDefault(); e.preventDefault();
if ($('input[name="ref"]').length) { if ($('input[name="ref"]').length) {
var $form = $dropdown.closest('form'); var $form = $dropdown.closest('form');
var $visit = $dropdown.data('visit');
var shouldVisit = typeof $visit === 'undefined' ? true : $visit;
var action = $form.attr('action'); var action = $form.attr('action');
var divider = action.indexOf('?') === -1 ? '?' : '&'; var divider = action.indexOf('?') === -1 ? '?' : '&';
gl.utils.visitUrl(action + '' + divider + '' + $form.serialize()); if (shouldVisit) {
gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
}
} }
} }
}); });
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */
import Api from './api'; import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
(function() { (function() {
this.ProjectSelect = (function() { this.ProjectSelect = (function() {
...@@ -58,7 +59,8 @@ import Api from './api'; ...@@ -58,7 +59,8 @@ import Api from './api';
if (this.includeGroups) { if (this.includeGroups) {
placeholder += " or group"; placeholder += " or group";
} }
return $(select).select2({
$(select).select2({
placeholder: placeholder, placeholder: placeholder,
minimumInputLength: 0, minimumInputLength: 0,
query: (function(_this) { query: (function(_this) {
...@@ -96,21 +98,18 @@ import Api from './api'; ...@@ -96,21 +98,18 @@ import Api from './api';
}; };
})(this), })(this),
id: function(project) { id: function(project) {
return project.web_url; return JSON.stringify({
name: project.name,
url: project.web_url,
});
}, },
text: function(project) { text: function(project) {
return project.name_with_namespace || project.name; return project.name_with_namespace || project.name;
}, },
dropdownCssClass: "ajax-project-dropdown" dropdownCssClass: "ajax-project-dropdown"
}); });
});
$('.new-project-item-select-button').on('click', function() {
$('.project-item-select', this.parentNode).select2('open');
});
$('.project-item-select').on('click', function() { return new ProjectSelectComboButton(select);
window.location = `${$(this).val()}/${this.dataset.relativePath}`;
}); });
} }
......
import AccessorUtilities from './lib/utils/accessor';
export default class ProjectSelectComboButton {
constructor(select) {
this.projectSelectInput = $(select);
this.newItemBtn = $('.new-project-item-link');
this.newItemBtnBaseText = this.newItemBtn.data('label');
this.itemType = this.deriveItemTypeFromLabel();
this.groupId = this.projectSelectInput.data('groupId');
this.bindEvents();
this.initLocalStorage();
}
bindEvents() {
this.projectSelectInput.siblings('.new-project-item-select-button')
.on('click', this.openDropdown);
this.projectSelectInput.on('change', () => this.selectProject());
}
initLocalStorage() {
const localStorageIsSafe = AccessorUtilities.isLocalStorageAccessSafe();
if (localStorageIsSafe) {
const itemTypeKebabed = this.newItemBtnBaseText.toLowerCase().split(' ').join('-');
this.localStorageKey = ['group', this.groupId, itemTypeKebabed, 'recent-project'].join('-');
this.setBtnTextFromLocalStorage();
}
}
openDropdown() {
$(this).siblings('.project-item-select').select2('open');
}
selectProject() {
const selectedProjectData = JSON.parse(this.projectSelectInput.val());
const projectUrl = `${selectedProjectData.url}/${this.projectSelectInput.data('relativePath')}`;
const projectName = selectedProjectData.name;
const projectMeta = {
url: projectUrl,
name: projectName,
};
this.setNewItemBtnAttributes(projectMeta);
this.setProjectInLocalStorage(projectMeta);
}
setBtnTextFromLocalStorage() {
const cachedProjectData = this.getProjectFromLocalStorage();
this.setNewItemBtnAttributes(cachedProjectData);
}
setNewItemBtnAttributes(project) {
if (project) {
this.newItemBtn.attr('href', project.url);
this.newItemBtn.text(`${this.newItemBtnBaseText} in ${project.name}`);
this.newItemBtn.enable();
} else {
this.newItemBtn.text(`Select project to create ${this.itemType}`);
this.newItemBtn.disable();
}
}
deriveItemTypeFromLabel() {
// label is either 'New issue' or 'New merge request'
return this.newItemBtnBaseText.split(' ').slice(1).join(' ');
}
getProjectFromLocalStorage() {
const projectString = localStorage.getItem(this.localStorageKey);
return JSON.parse(projectString);
}
setProjectInLocalStorage(projectMeta) {
const projectString = JSON.stringify(projectMeta);
localStorage.setItem(this.localStorageKey, projectString);
}
}
import '../lib/utils/url_utility';
const bindEvents = () => {
const path = gl.utils.getParameterValues('path')[0];
// get the path url and append it in the inputS
$('.js-path-name').val(path);
};
document.addEventListener('DOMContentLoaded', bindEvents);
export default {
bindEvents,
};
let hasUserDefinedProjectPath = false; let hasUserDefinedProjectPath = false;
const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => { const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => {
if ($projectImportUrl.attr('disabled') || hasUserDefinedProjectPath) { if (hasUserDefinedProjectPath) {
return; return;
} }
...@@ -27,8 +27,6 @@ const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => { ...@@ -27,8 +27,6 @@ const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => {
const bindEvents = () => { const bindEvents = () => {
const $newProjectForm = $('#new_project'); const $newProjectForm = $('#new_project');
const importBtnTooltip = 'Please enter a valid project name.';
const $importBtnWrapper = $('.import_gitlab_project');
const $projectImportUrl = $('#project_import_url'); const $projectImportUrl = $('#project_import_url');
const $projectPath = $('#project_path'); const $projectPath = $('#project_path');
...@@ -50,31 +48,15 @@ const bindEvents = () => { ...@@ -50,31 +48,15 @@ const bindEvents = () => {
$('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`); $('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`);
}); });
$('.btn_import_gitlab_project').attr('disabled', !$projectPath.val().trim().length);
$importBtnWrapper.attr('title', importBtnTooltip);
$newProjectForm.on('submit', () => { $newProjectForm.on('submit', () => {
$projectPath.val($projectPath.val().trim()); $projectPath.val($projectPath.val().trim());
}); });
$projectPath.on('keyup', () => { $projectPath.on('keyup', () => {
hasUserDefinedProjectPath = $projectPath.val().trim().length > 0; hasUserDefinedProjectPath = $projectPath.val().trim().length > 0;
if (hasUserDefinedProjectPath) {
$('.btn_import_gitlab_project').attr('disabled', false);
$importBtnWrapper.attr('title', '');
$importBtnWrapper.removeClass('has-tooltip');
} else {
$('.btn_import_gitlab_project').attr('disabled', true);
$importBtnWrapper.addClass('has-tooltip');
}
}); });
$projectImportUrl.disable();
$projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl, $projectPath)); $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl, $projectPath));
$('.import_git').on('click', () => {
$projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'));
});
}; };
document.addEventListener('DOMContentLoaded', bindEvents); document.addEventListener('DOMContentLoaded', bindEvents);
......
<script>
import RepoSidebar from './repo_sidebar.vue';
import RepoCommitSection from './repo_commit_section.vue';
import RepoTabs from './repo_tabs.vue';
import RepoFileButtons from './repo_file_buttons.vue';
import RepoPreview from './repo_preview.vue';
import RepoMixin from '../mixins/repo_mixin';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
export default {
data: () => Store,
mixins: [RepoMixin],
components: {
'repo-sidebar': RepoSidebar,
'repo-tabs': RepoTabs,
'repo-file-buttons': RepoFileButtons,
'repo-editor': MonacoLoaderHelper.repoEditorLoader,
'repo-commit-section': RepoCommitSection,
'popup-dialog': PopupDialog,
'repo-preview': RepoPreview,
},
mounted() {
Helper.getContent().catch(Helper.loadingError);
},
methods: {
dialogToggled(toggle) {
this.dialog.open = toggle;
},
dialogSubmitted(status) {
this.dialog.open = false;
this.dialog.status = status;
},
toggleBlobView: Store.toggleBlobView,
},
};
</script>
<template>
<div class="repository-view tree-content-holder">
<repo-sidebar/><div class="panel-right" :class="{'edit-mode': editMode}">
<repo-tabs/>
<component :is="currentBlobView" class="blob-viewer-container"></component>
<repo-file-buttons/>
</div>
<repo-commit-section/>
<popup-dialog
:primary-button-label="__('Discard changes')"
:open="dialog.open"
kind="warning"
:title="__('Are you sure?')"
:body="__('Are you sure you want to discard your changes?')"
@toggle="dialogToggled"
@submit="dialogSubmitted"
/>
</div>
</template>
<script>
/* global Flash */
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
const RepoCommitSection = {
data: () => Store,
mixins: [RepoMixin],
computed: {
branchPaths() {
const branch = Helper.getBranch();
return this.changedFiles.map(f => Helper.getFilePathFromFullPath(f.url, branch));
},
cantCommitYet() {
return !this.commitMessage || this.submitCommitsLoading;
},
filePluralize() {
return this.changedFiles.length > 1 ? 'files' : 'file';
},
},
methods: {
makeCommit() {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const branch = Helper.getBranch();
const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({
action: 'update',
file_path: Helper.getFilePathFromFullPath(f.url, branch),
content: f.newContent,
}));
const payload = {
branch: Store.targetBranch,
commit_message: commitMessage,
actions,
};
Store.submitCommitsLoading = true;
Service.commitFiles(payload, this.resetCommitState);
},
resetCommitState() {
this.submitCommitsLoading = false;
this.changedFiles = [];
this.openedFiles = [];
this.commitMessage = '';
this.editMode = false;
$('html, body').animate({ scrollTop: 0 }, 'fast');
},
},
};
export default RepoCommitSection;
</script>
<template>
<div id="commit-area" v-if="isCommitable && changedFiles.length" >
<form class="form-horizontal">
<fieldset>
<div class="form-group">
<label class="col-md-4 control-label staged-files">Staged files ({{changedFiles.length}})</label>
<div class="col-md-4">
<ul class="list-unstyled changed-files">
<li v-for="file in branchPaths" :key="file.id">
<span class="help-block">{{file}}</span>
</li>
</ul>
</div>
</div>
<!-- Textarea
-->
<div class="form-group">
<label class="col-md-4 control-label" for="commit-message">Commit message</label>
<div class="col-md-4">
<textarea class="form-control" id="commit-message" name="commit-message" v-model="commitMessage"></textarea>
</div>
</div>
<!-- Button Drop Down
-->
<div class="form-group target-branch">
<label class="col-md-4 control-label" for="target-branch">Target branch</label>
<div class="col-md-4">
<span class="help-block">{{targetBranch}}</span>
</div>
</div>
<div class="col-md-offset-4 col-md-4">
<button type="submit" :disabled="cantCommitYet" class="btn btn-success submit-commit" @click.prevent="makeCommit">
<i class="fa fa-spinner fa-spin" v-if="submitCommitsLoading"></i>
<span class="commit-summary">Commit {{changedFiles.length}} {{filePluralize}}</span>
</button>
</div>
</fieldset>
</form>
</div>
</template>
<script>
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
export default {
data: () => Store,
mixins: [RepoMixin],
computed: {
buttonLabel() {
return this.editMode ? this.__('Cancel edit') : this.__('Edit');
},
buttonIcon() {
return this.editMode ? [] : ['fa', 'fa-pencil'];
},
},
methods: {
editClicked() {
if (this.changedFiles.length) {
this.dialog.open = true;
return;
}
this.editMode = !this.editMode;
Store.toggleBlobView();
},
},
watch: {
editMode() {
if (this.editMode) {
$('.project-refs-form').addClass('disabled');
$('.fa-long-arrow-right').show();
$('.project-refs-target-form').show();
} else {
$('.project-refs-form').removeClass('disabled');
$('.fa-long-arrow-right').hide();
$('.project-refs-target-form').hide();
}
},
},
};
</script>
<template>
<button class="btn btn-default" @click.prevent="editClicked" v-cloak v-if="isCommitable && !activeFile.render_error" :disabled="binary">
<i :class="buttonIcon"></i>
<span>{{buttonLabel}}</span>
</button>
</template>
<script>
/* global monaco */
import Store from '../stores/repo_store';
import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper';
const RepoEditor = {
data: () => Store,
destroyed() {
// this.monacoInstance.getModels().forEach((m) => {
// m.dispose();
// });
this.monacoInstance.destroy();
},
mounted() {
Service.getRaw(this.activeFile.raw_path)
.then((rawResponse) => {
Store.blobRaw = rawResponse.data;
Helper.findOpenedFileFromActive().plain = rawResponse.data;
const monacoInstance = this.monaco.editor.create(this.$el, {
model: null,
readOnly: false,
contextmenu: false,
});
Store.monacoInstance = monacoInstance;
this.addMonacoEvents();
const languages = this.monaco.languages.getLanguages();
const languageID = Helper.getLanguageIDForFile(this.activeFile, languages);
this.showHide();
const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
this.monacoInstance.setModel(newModel);
}).catch(Helper.loadingError);
},
methods: {
showHide() {
if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
this.$el.style.display = 'none';
} else {
this.$el.style.display = 'inline-block';
}
},
addMonacoEvents() {
this.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
this.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
},
onMonacoEditorKeysPressed() {
Store.setActiveFileContents(this.monacoInstance.getValue());
},
onMonacoEditorMouseUp(e) {
const lineNumber = e.target.position.lineNumber;
if (e.target.element.className === 'line-numbers') {
location.hash = `L${lineNumber}`;
Store.activeLine = lineNumber;
}
},
},
watch: {
activeLine() {
this.monacoInstance.setPosition({
lineNumber: this.activeLine,
column: 1,
});
},
activeFileLabel() {
this.showHide();
},
dialog: {
handler(obj) {
const newObj = obj;
if (newObj.status) {
newObj.status = false;
this.openedFiles.map((file) => {
const f = file;
if (f.active) {
this.blobRaw = f.plain;
}
f.changed = false;
delete f.newContent;
return f;
});
this.editMode = false;
}
},
deep: true,
},
isTree() {
this.showHide();
},
openedFiles() {
this.showHide();
},
binary() {
this.showHide();
},
blobRaw() {
this.showHide();
if (this.isTree) return;
this.monacoInstance.setModel(null);
const languages = this.monaco.languages.getLanguages();
const languageID = Helper.getLanguageIDForFile(this.activeFile, languages);
const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
this.monacoInstance.setModel(newModel);
},
},
};
export default RepoEditor;
</script>
<template>
<div id="ide"></div>
</template>
<script>
import TimeAgoMixin from '../../vue_shared/mixins/timeago';
const RepoFile = {
mixins: [TimeAgoMixin],
props: {
file: {
type: Object,
required: true,
},
isMini: {
type: Boolean,
required: false,
default: false,
},
loading: {
type: Object,
required: false,
default() { return { tree: false }; },
},
hasFiles: {
type: Boolean,
required: false,
default: false,
},
activeFile: {
type: Object,
required: true,
},
},
computed: {
canShowFile() {
return !this.loading.tree || this.hasFiles;
},
},
methods: {
linkClicked(file) {
this.$emit('linkclicked', file);
},
},
};
export default RepoFile;
</script>
<template>
<tr class="file" v-if="canShowFile" :class="{'active': activeFile.url === file.url}">
<td @click.prevent="linkClicked(file)">
<i class="fa file-icon" v-if="!file.loading" :class="file.icon" :style="{'margin-left': file.level * 10 + 'px'}"></i>
<i class="fa fa-spinner fa-spin" v-if="file.loading" :style="{'margin-left': file.level * 10 + 'px'}"></i>
<a :href="file.url" class="repo-file-name" :title="file.url">{{file.name}}</a>
</td>
<td v-if="!isMini" class="hidden-sm hidden-xs">
<div class="commit-message">
<a :href="file.lastCommitUrl">{{file.lastCommitMessage}}</a>
</div>
</td>
<td v-if="!isMini" class="hidden-xs">
<span class="commit-update" :title="tooltipTitle(file.lastCommitUpdate)">{{timeFormated(file.lastCommitUpdate)}}</span>
</td>
</tr>
</template>
<script>
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import RepoMixin from '../mixins/repo_mixin';
const RepoFileButtons = {
data: () => Store,
mixins: [RepoMixin],
computed: {
rawDownloadButtonLabel() {
return this.binary ? 'Download' : 'Raw';
},
canPreview() {
return Helper.isKindaBinary();
},
},
methods: {
rawPreviewToggle: Store.toggleRawPreview,
},
};
export default RepoFileButtons;
</script>
<template>
<div id="repo-file-buttons" v-if="isMini">
<a :href="activeFile.raw_path" target="_blank" class="btn btn-default raw" rel="noopener noreferrer">{{rawDownloadButtonLabel}}</a>
<div class="btn-group" role="group" aria-label="File actions">
<a :href="activeFile.blame_path" class="btn btn-default blame">Blame</a>
<a :href="activeFile.commits_path" class="btn btn-default history">History</a>
<a :href="activeFile.permalink" class="btn btn-default permalink">Permalink</a>
</div>
<a href="#" v-if="canPreview" @click.prevent="rawPreviewToggle" class="btn btn-default preview">{{activeFileLabel}}</a>
</div>
</template>
<script>
const RepoFileOptions = {
props: {
isMini: {
type: Boolean,
required: false,
default: false,
},
projectName: {
type: String,
required: true,
},
},
};
export default RepoFileOptions;
</script>
<template>
<tr v-if="isMini" class="repo-file-options">
<td>
<span class="title">{{projectName}}</span>
</td>
</tr>
</template>
<script>
const RepoLoadingFile = {
props: {
loading: {
type: Object,
required: false,
default: {},
},
hasFiles: {
type: Boolean,
required: false,
default: false,
},
isMini: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
lineOfCode(n) {
return `line-of-code-${n}`;
},
},
};
export default RepoLoadingFile;
</script>
<template>
<tr v-if="loading.tree && !hasFiles" class="loading-file">
<td>
<div class="animation-container animation-container-small">
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
</div>
</td>
<td v-if="!isMini" class="hidden-sm hidden-xs">
<div class="animation-container">
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
</div>
</td>
<td v-if="!isMini" class="hidden-xs">
<div class="animation-container animation-container-small">
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
</div>
</td>
</tr>
</template>
<script>
const RepoPreviousDirectory = {
props: {
prevUrl: {
type: String,
required: true,
},
},
methods: {
linkClicked(file) {
this.$emit('linkclicked', file);
},
},
};
export default RepoPreviousDirectory;
</script>
<template>
<tr class="prev-directory">
<td colspan="3">
<a :href="prevUrl" @click.prevent="linkClicked(prevUrl)">..</a>
</td>
</tr>
</template>
<script>
import Store from '../stores/repo_store';
export default {
data: () => Store,
mounted() {
$(this.$el).find('.file-content').syntaxHighlight();
},
computed: {
html() {
return this.activeFile.html;
},
},
watch: {
html() {
this.$nextTick(() => {
$(this.$el).find('.file-content').syntaxHighlight();
});
},
},
};
</script>
<template>
<div>
<div v-if="!activeFile.render_error" v-html="activeFile.html"></div>
<div v-if="activeFile.render_error" class="vertical-center render-error">
<p class="text-center">The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.</p>
</div>
</div>
</template>
<script>
import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper';
import Store from '../stores/repo_store';
import RepoPreviousDirectory from './repo_prev_directory.vue';
import RepoFileOptions from './repo_file_options.vue';
import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue';
import RepoMixin from '../mixins/repo_mixin';
const RepoSidebar = {
mixins: [RepoMixin],
components: {
'repo-file-options': RepoFileOptions,
'repo-previous-directory': RepoPreviousDirectory,
'repo-file': RepoFile,
'repo-loading-file': RepoLoadingFile,
},
created() {
this.addPopEventListener();
},
data: () => Store,
methods: {
addPopEventListener() {
window.addEventListener('popstate', () => {
if (location.href.indexOf('#') > -1) return;
this.linkClicked({
url: location.href,
});
});
},
linkClicked(clickedFile) {
let url = '';
let file = clickedFile;
if (typeof file === 'object') {
file.loading = true;
if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file);
file.loading = false;
} else {
url = file.url;
Service.url = url;
// I need to refactor this to do the `then` here.
// Not a callback. For now this is good enough.
// it works.
Helper.getContent(file, () => {
file.loading = false;
Helper.scrollTabsRight();
});
}
} else if (typeof file === 'string') {
// go back
url = file;
Service.url = url;
Helper.getContent(null, () => Helper.scrollTabsRight());
}
},
},
};
export default RepoSidebar;
</script>
<template>
<div id="sidebar" :class="{'sidebar-mini' : isMini}" v-cloak>
<table class="table">
<thead v-if="!isMini">
<tr>
<th class="name">Name</th>
<th class="hidden-sm hidden-xs last-commit">Last Commit</th>
<th class="hidden-xs last-update">Last Update</th>
</tr>
</thead>
<tbody>
<repo-file-options
:is-mini="isMini"
:project-name="projectName"/>
<repo-previous-directory
v-if="isRoot"
:prev-url="prevURL"
@linkclicked="linkClicked(prevURL)"/>
<repo-loading-file
v-for="n in 5"
:key="n"
:loading="loading"
:has-files="!!files.length"
:is-mini="isMini"/>
<repo-file
v-for="file in files"
:key="file.id"
:file="file"
:is-mini="isMini"
@linkclicked="linkClicked(file)"
:is-tree="isTree"
:has-files="!!files.length"
:active-file="activeFile"/>
</tbody>
</table>
</div>
</template>
<script>
import Store from '../stores/repo_store';
const RepoTab = {
props: {
tab: {
type: Object,
required: true,
},
},
computed: {
changedClass() {
const tabChangedObj = {
'fa-times': !this.tab.changed,
'fa-circle': this.tab.changed,
};
return tabChangedObj;
},
},
methods: {
tabClicked: Store.setActiveFiles,
xClicked(file) {
if (file.changed) return;
this.$emit('xclicked', file);
},
},
};
export default RepoTab;
</script>
<template>
<li>
<a href="#" class="close" @click.prevent="xClicked(tab)" v-if="!tab.loading">
<i class="fa" :class="changedClass"></i>
</a>
<a href="#" class="repo-tab" v-if="!tab.loading" :title="tab.url" @click.prevent="tabClicked(tab)">{{tab.name}}</a>
<i v-if="tab.loading" class="fa fa-spinner fa-spin"></i>
</li>
</template>
<script>
import Vue from 'vue';
import Store from '../stores/repo_store';
import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin';
const RepoTabs = {
mixins: [RepoMixin],
components: {
'repo-tab': RepoTab,
},
data: () => Store,
methods: {
isOverflow() {
return this.$el.scrollWidth > this.$el.offsetWidth;
},
xClicked(file) {
Store.removeFromOpenedFiles(file);
},
},
watch: {
openedFiles() {
Vue.nextTick(() => {
this.tabsOverflow = this.isOverflow();
});
},
},
};
export default RepoTabs;
</script>
<template>
<ul id="tabs" v-if="isMini" v-cloak :class="{'overflown': tabsOverflow}">
<repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/>
<li class="tabs-divider" />
</ul>
</template>
/* global monaco */
import RepoEditor from '../components/repo_editor.vue';
import Store from '../stores/repo_store';
import monacoLoader from '../monaco_loader';
function repoEditorLoader() {
Store.monacoLoading = true;
return new Promise((resolve, reject) => {
monacoLoader(['vs/editor/editor.main'], () => {
Store.monaco = monaco;
Store.monacoLoading = false;
resolve(RepoEditor);
}, reject);
});
}
const MonacoLoaderHelper = {
repoEditorLoader,
};
export default MonacoLoaderHelper;
/* global Flash */
import Service from '../services/repo_service';
import Store from '../stores/repo_store';
import '../../flash';
const RepoHelper = {
getDefaultActiveFile() {
return {
active: true,
binary: false,
extension: '',
html: '',
mime_type: '',
name: '',
plain: '',
size: 0,
url: '',
raw: false,
newContent: '',
changed: false,
loading: false,
};
},
key: '',
isTree(data) {
return Object.hasOwnProperty.call(data, 'blobs');
},
Time: window.performance
&& window.performance.now
? window.performance
: Date,
getBranch() {
return $('button.dropdown-menu-toggle').attr('data-ref');
},
getLanguageIDForFile(file, langs) {
const ext = file.name.split('.').pop();
const foundLang = RepoHelper.findLanguage(ext, langs);
return foundLang ? foundLang.id : 'plaintext';
},
getFilePathFromFullPath(fullPath, branch) {
return fullPath.split(`${Store.projectUrl}/blob/${branch}`)[1];
},
findLanguage(ext, langs) {
return langs.find(lang => lang.extensions && lang.extensions.indexOf(`.${ext}`) > -1);
},
setDirectoryOpen(tree) {
const file = tree;
if (!file) return undefined;
file.opened = true;
file.icon = 'fa-folder-open';
RepoHelper.toURL(file.url, file.name);
return file;
},
isKindaBinary() {
const okExts = ['md', 'svg'];
return okExts.indexOf(Store.activeFile.extension) > -1;
},
setBinaryDataAsBase64(file) {
Service.getBase64Content(file.raw_path)
.then((response) => {
Store.blobRaw = response;
file.base64 = response; // eslint-disable-line no-param-reassign
})
.catch(RepoHelper.loadingError);
},
toggleFakeTab(loading, file) {
if (loading) return Store.addPlaceholderFile();
return Store.removeFromOpenedFiles(file);
},
setLoading(loading, file) {
if (Service.url.indexOf('blob') > -1) {
Store.loading.blob = loading;
return RepoHelper.toggleFakeTab(loading, file);
}
if (Service.url.indexOf('tree') > -1) Store.loading.tree = loading;
return undefined;
},
getNewMergedList(inDirectory, currentList, newList) {
const newListSorted = newList.sort(this.compareFilesCaseInsensitive);
if (!inDirectory) return newListSorted;
const indexOfFile = currentList.findIndex(file => file.url === inDirectory.url);
if (!indexOfFile) return newListSorted;
return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile);
},
mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) {
newList.reverse().forEach((newFile) => {
const fileIndex = indexOfFile + 1;
const file = newFile;
file.level = inDirectory.level + 1;
oldList.splice(fileIndex, 0, file);
});
return oldList;
},
compareFilesCaseInsensitive(a, b) {
const aName = a.name.toLowerCase();
const bName = b.name.toLowerCase();
if (a.level > 0) return 0;
if (aName < bName) { return -1; }
if (aName > bName) { return 1; }
return 0;
},
isRoot(url) {
// the url we are requesting -> split by the project URL. Grab the right side.
const isRoot = !!url.split(Store.projectUrl)[1]
// remove the first "/"
.slice(1)
// split this by "/"
.split('/')
// remove the first two items of the array... usually /tree/master.
.slice(2)
// we want to know the length of the array.
// If greater than 0 not root.
.length;
return isRoot;
},
getContent(treeOrFile, cb) {
let file = treeOrFile;
// const loadingData = RepoHelper.setLoading(true);
return Service.getContent()
.then((response) => {
const data = response.data;
// RepoHelper.setLoading(false, loadingData);
if (cb) cb();
Store.isTree = RepoHelper.isTree(data);
if (!Store.isTree) {
if (!file) file = data;
Store.binary = data.binary;
if (data.binary) {
Store.binaryMimeType = data.mime_type;
// file might be undefined
RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview();
} else if (!Store.isPreviewView()) {
if (!data.render_error) {
Service.getRaw(data.raw_path)
.then((rawResponse) => {
Store.blobRaw = rawResponse.data;
data.plain = rawResponse.data;
RepoHelper.setFile(data, file);
}).catch(RepoHelper.loadingError);
}
}
if (Store.isPreviewView()) {
RepoHelper.setFile(data, file);
}
// if the file tree is empty
if (Store.files.length === 0) {
const parentURL = Service.blobURLtoParentTree(Service.url);
Service.url = parentURL;
RepoHelper.getContent();
}
} else {
// it's a tree
if (!file) Store.isRoot = RepoHelper.isRoot(Service.url);
file = RepoHelper.setDirectoryOpen(file);
const newDirectory = RepoHelper.dataToListOfFiles(data);
Store.addFilesToDirectory(file, Store.files, newDirectory);
Store.prevURL = Service.blobURLtoParentTree(Service.url);
}
}).catch(RepoHelper.loadingError);
},
setFile(data, file) {
const newFile = data;
newFile.url = file.url || location.pathname;
newFile.url = file.url;
if (newFile.render_error === 'too_large') {
newFile.tooLarge = true;
}
newFile.newContent = '';
Store.addToOpenedFiles(newFile);
Store.setActiveFiles(newFile);
},
toFA(icon) {
return `fa-${icon}`;
},
serializeBlob(blob) {
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
simpleBlob.lastCommitMessage = blob.last_commit.message;
simpleBlob.lastCommitUpdate = blob.last_commit.committed_date;
simpleBlob.loading = false;
return simpleBlob;
},
serializeTree(tree) {
return RepoHelper.serializeRepoEntity('tree', tree);
},
serializeSubmodule(submodule) {
return RepoHelper.serializeRepoEntity('submodule', submodule);
},
serializeRepoEntity(type, entity) {
const { url, name, icon, last_commit } = entity;
const returnObj = {
type,
name,
url,
icon: RepoHelper.toFA(icon),
level: 0,
loading: false,
};
if (entity.last_commit) {
returnObj.lastCommitUrl = `${Store.projectUrl}/commit/${last_commit.id}`;
} else {
returnObj.lastCommitUrl = '';
}
return returnObj;
},
scrollTabsRight() {
// wait for the transition. 0.1 seconds.
setTimeout(() => {
const tabs = document.getElementById('tabs');
if (!tabs) return;
tabs.scrollLeft = 12000;
}, 200);
},
dataToListOfFiles(data) {
const a = [];
// push in blobs
data.blobs.forEach((blob) => {
a.push(RepoHelper.serializeBlob(blob));
});
data.trees.forEach((tree) => {
a.push(RepoHelper.serializeTree(tree));
});
data.submodules.forEach((submodule) => {
a.push(RepoHelper.serializeSubmodule(submodule));
});
return a;
},
genKey() {
return RepoHelper.Time.now().toFixed(3);
},
getStateKey() {
return RepoHelper.key;
},
setStateKey(key) {
RepoHelper.key = key;
},
toURL(url, title) {
const history = window.history;
RepoHelper.key = RepoHelper.genKey();
history.pushState({ key: RepoHelper.key }, '', url);
if (title) {
document.title = `${title} · GitLab`;
}
},
findOpenedFileFromActive() {
return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url);
},
loadingError() {
Flash('Unable to load the file at this time.');
},
};
export default RepoHelper;
import $ from 'jquery';
import Vue from 'vue';
import Service from './services/repo_service';
import Store from './stores/repo_store';
import Repo from './components/repo.vue';
import RepoEditButton from './components/repo_edit_button.vue';
import Translate from '../vue_shared/translate';
function initDropdowns() {
$('.project-refs-target-form').hide();
$('.fa-long-arrow-right').hide();
}
function addEventsForNonVueEls() {
$(document).on('change', '.dropdown', () => {
Store.targetBranch = $('.project-refs-target-form input[name="ref"]').val();
});
window.onbeforeunload = function confirmUnload(e) {
const hasChanged = Store.openedFiles
.some(file => file.changed);
if (!hasChanged) return undefined;
const event = e || window.event;
if (event) event.returnValue = 'Are you sure you want to lose unsaved changes?';
// For Safari
return 'Are you sure you want to lose unsaved changes?';
};
}
function setInitialStore(data) {
Store.service = Service;
Store.service.url = data.url;
Store.service.refsUrl = data.refsUrl;
Store.projectId = data.projectId;
Store.projectName = data.projectName;
Store.projectUrl = data.projectUrl;
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
Store.checkIsCommitable();
}
function initRepo(el) {
return new Vue({
el,
components: {
repo: Repo,
},
});
}
function initRepoEditButton(el) {
return new Vue({
el,
components: {
repoEditButton: RepoEditButton,
},
});
}
function initRepoBundle() {
const repo = document.getElementById('repo');
const editButton = document.querySelector('.editable-mode');
setInitialStore(repo.dataset);
addEventsForNonVueEls();
initDropdowns();
Vue.use(Translate);
initRepo(repo);
initRepoEditButton(editButton);
}
$(initRepoBundle);
export default initRepoBundle;
import Store from '../stores/repo_store';
const RepoMixin = {
computed: {
isMini() {
return !!Store.openedFiles.length;
},
changedFiles() {
const changedFileList = this.openedFiles
.filter(file => file.changed);
return changedFileList;
},
},
};
export default RepoMixin;
/* eslint-disable no-underscore-dangle, camelcase */
/* global __webpack_public_path__ */
import monacoContext from 'monaco-editor/dev/vs/loader';
monacoContext.require.config({
paths: {
vs: `${__webpack_public_path__}monaco-editor/vs`,
},
});
window.__monaco_context__ = monacoContext;
export default monacoContext.require;
/* global Flash */
import axios from 'axios';
import Store from '../stores/repo_store';
import Api from '../../api';
const RepoService = {
url: '',
options: {
params: {
format: 'json',
},
},
richExtensionRegExp: /md/,
checkCurrentBranchIsCommitable() {
const url = Store.service.refsUrl;
return axios.get(url, { params: {
ref: Store.currentBranch,
search: Store.currentBranch,
} });
},
getRaw(url) {
return axios.get(url, {
transformResponse: [res => res],
});
},
buildParams(url = this.url) {
// shallow clone object without reference
const params = Object.assign({}, this.options.params);
if (this.urlIsRichBlob(url)) params.viewer = 'rich';
return params;
},
urlIsRichBlob(url = this.url) {
const extension = url.split('.').pop();
return this.richExtensionRegExp.test(extension);
},
getContent(url = this.url) {
const params = this.buildParams(url);
return axios.get(url, {
params,
});
},
getBase64Content(url = this.url) {
const request = axios.get(url, {
responseType: 'arraybuffer',
});
return request.then(response => this.bufferToBase64(response.data));
},
bufferToBase64(data) {
return new Buffer(data, 'binary').toString('base64');
},
blobURLtoParentTree(url) {
const urlArray = url.split('/');
urlArray.pop();
const blobIndex = urlArray.lastIndexOf('blob');
if (blobIndex > -1) urlArray[blobIndex] = 'tree';
return urlArray.join('/');
},
commitFiles(payload, cb) {
Api.commitMultiple(Store.projectId, payload, (data) => {
Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
cb();
});
},
};
export default RepoService;
/* global Flash */
import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
const RepoStore = {
ideEl: {},
monaco: {},
monacoLoading: false,
monacoInstance: {},
service: '',
editor: '',
sidebar: '',
editMode: false,
isTree: false,
isRoot: false,
prevURL: '',
projectId: '',
projectName: '',
projectUrl: '',
trees: [],
blobs: [],
submodules: [],
blobRaw: '',
blobRendered: '',
currentBlobView: 'repo-preview',
openedFiles: [],
tabSize: 100,
defaultTabSize: 100,
minTabSize: 30,
tabsOverflow: 41,
submitCommitsLoading: false,
binaryLoaded: false,
dialog: {
open: false,
title: '',
status: false,
},
activeFile: Helper.getDefaultActiveFile(),
activeFileIndex: 0,
activeLine: 0,
activeFileLabel: 'Raw',
files: [],
isCommitable: false,
binary: false,
currentBranch: '',
targetBranch: 'new-branch',
commitMessage: '',
binaryMimeType: '',
// scroll bar space for windows
scrollWidth: 0,
binaryTypes: {
png: false,
md: false,
svg: false,
unknown: false,
},
loading: {
tree: false,
blob: false,
},
readOnly: true,
resetBinaryTypes() {
Object.keys(RepoStore.binaryTypes).forEach((key) => {
RepoStore.binaryTypes[key] = false;
});
},
// mutations
checkIsCommitable() {
RepoStore.service.checkCurrentBranchIsCommitable()
.then((data) => {
// you shouldn't be able to make commits on commits or tags.
const { Branches, Commits, Tags } = data.data;
if (Branches && Branches.length) RepoStore.isCommitable = true;
if (Commits && Commits.length) RepoStore.isCommitable = false;
if (Tags && Tags.length) RepoStore.isCommitable = false;
}).catch(() => Flash('Failed to check if branch can be committed to.'));
},
addFilesToDirectory(inDirectory, currentList, newList) {
RepoStore.files = Helper.getNewMergedList(inDirectory, currentList, newList);
},
toggleRawPreview() {
RepoStore.activeFile.raw = !RepoStore.activeFile.raw;
RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source';
},
setActiveFiles(file) {
if (RepoStore.isActiveFile(file)) return;
RepoStore.openedFiles = RepoStore.openedFiles
.map((openedFile, i) => RepoStore.setFileActivity(file, openedFile, i));
RepoStore.setActiveToRaw();
if (file.binary) {
RepoStore.blobRaw = file.base64;
RepoStore.binaryMimeType = file.mime_type;
} else if (file.newContent || file.plain) {
RepoStore.blobRaw = file.newContent || file.plain;
} else {
Service.getRaw(file.raw_path)
.then((rawResponse) => {
RepoStore.blobRaw = rawResponse.data;
Helper.findOpenedFileFromActive().plain = rawResponse.data;
}).catch(Helper.loadingError);
}
if (!file.loading) Helper.toURL(file.url, file.name);
RepoStore.binary = file.binary;
},
setFileActivity(file, openedFile, i) {
const activeFile = openedFile;
activeFile.active = file.url === activeFile.url;
if (activeFile.active) RepoStore.setActiveFile(activeFile, i);
return activeFile;
},
setActiveFile(activeFile, i) {
RepoStore.activeFile = Object.assign({}, RepoStore.activeFile, activeFile);
RepoStore.activeFileIndex = i;
},
setActiveToRaw() {
RepoStore.activeFile.raw = false;
// can't get vue to listen to raw for some reason so RepoStore for now.
RepoStore.activeFileLabel = 'Display source';
},
removeChildFilesOfTree(tree) {
let foundTree = false;
const treeToClose = tree;
let wereDone = false;
RepoStore.files = RepoStore.files.filter((file) => {
const isItTheTreeWeWant = file.url === treeToClose.url;
// if it's the next tree
if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) {
wereDone = true;
return true;
}
if (wereDone) return true;
if (isItTheTreeWeWant) foundTree = true;
if (foundTree) return file.level <= treeToClose.level;
return true;
});
treeToClose.opened = false;
treeToClose.icon = 'fa-folder';
return treeToClose;
},
removeFromOpenedFiles(file) {
if (file.type === 'tree') return;
let foundIndex;
RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => {
if (openedFile.url === file.url) foundIndex = i;
return openedFile.url !== file.url;
});
// now activate the right tab based on what you closed.
if (RepoStore.openedFiles.length === 0) {
RepoStore.activeFile = {};
return;
}
if (RepoStore.openedFiles.length === 1 || foundIndex === 0) {
RepoStore.setActiveFiles(RepoStore.openedFiles[0]);
return;
}
if (foundIndex) {
if (foundIndex > 0) {
RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]);
}
}
},
addPlaceholderFile() {
const randomURL = Helper.Time.now();
const newFakeFile = {
active: false,
binary: true,
type: 'blob',
loading: true,
mime_type: 'loading',
name: 'loading',
url: randomURL,
fake: true,
};
RepoStore.openedFiles.push(newFakeFile);
return newFakeFile;
},
addToOpenedFiles(file) {
const openFile = file;
const openedFilesAlreadyExists = RepoStore.openedFiles
.some(openedFile => openedFile.url === openFile.url);
if (openedFilesAlreadyExists) return;
openFile.changed = false;
RepoStore.openedFiles.push(openFile);
},
setActiveFileContents(contents) {
if (!RepoStore.editMode) return;
const currentFile = RepoStore.openedFiles[RepoStore.activeFileIndex];
RepoStore.activeFile.newContent = contents;
RepoStore.activeFile.changed = RepoStore.activeFile.plain !== RepoStore.activeFile.newContent;
currentFile.changed = RepoStore.activeFile.changed;
currentFile.newContent = contents;
},
toggleBlobView() {
RepoStore.currentBlobView = RepoStore.isPreviewView() ? 'repo-editor' : 'repo-preview';
},
setViewToPreview() {
RepoStore.currentBlobView = 'repo-preview';
},
// getters
isActiveFile(file) {
return file && file.url === RepoStore.activeFile.url;
},
isPreviewView() {
return RepoStore.currentBlobView === 'repo-preview';
},
};
export default RepoStore;
<script>
/* global Flash */
import editForm from './edit_form.vue';
export default {
components: {
editForm,
},
props: {
isConfidential: {
required: true,
type: Boolean,
},
isEditable: {
required: true,
type: Boolean,
},
service: {
required: true,
type: Object,
},
},
data() {
return {
edit: false,
};
},
computed: {
faEye() {
const eye = this.isConfidential ? 'fa-eye-slash' : 'fa-eye';
return {
[eye]: true,
};
},
},
methods: {
toggleForm() {
this.edit = !this.edit;
},
updateConfidentialAttribute(confidential) {
this.service.update('issue', { confidential })
.then(() => location.reload())
.catch(() => new Flash('Something went wrong trying to change the confidentiality of this issue'));
},
},
};
</script>
<template>
<div class="block confidentiality">
<div class="sidebar-collapsed-icon">
<i class="fa" :class="faEye" aria-hidden="true" data-hidden="true"></i>
</div>
<div class="title hide-collapsed">
Confidentiality
<a
v-if="isEditable"
class="pull-right confidential-edit"
href="#"
@click.prevent="toggleForm"
>
Edit
</a>
</div>
<div class="value confidential-value hide-collapsed">
<editForm
v-if="edit"
:toggle-form="toggleForm"
:is-confidential="isConfidential"
:update-confidential-attribute="updateConfidentialAttribute"
/>
<div v-if="!isConfidential" class="no-value confidential-value">
<i class="fa fa-eye is-not-confidential"></i>
None
</div>
<div v-else class="value confidential-value hide-collapsed">
<i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i>
This issue is confidential
</div>
</div>
</div>
</template>
<script>
import editFormButtons from './edit_form_buttons.vue';
export default {
components: {
editFormButtons,
},
props: {
isConfidential: {
required: true,
type: Boolean,
},
toggleForm: {
required: true,
type: Function,
},
updateConfidentialAttribute: {
required: true,
type: Function,
},
},
};
</script>
<template>
<div class="dropdown open">
<div class="dropdown-menu confidential-warning-message">
<div>
<p v-if="!isConfidential">
You are going to turn on the confidentiality. This means that only team members with
<strong>at least Reporter access</strong>
are able to see and leave comments on the issue.
</p>
<p v-else>
You are going to turn off the confidentiality. This means
<strong>everyone</strong>
will be able to see and leave a comment on this issue.
</p>
<edit-form-buttons
:is-confidential="isConfidential"
:toggle-form="toggleForm"
:update-confidential-attribute="updateConfidentialAttribute"
/>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
isConfidential: {
required: true,
type: Boolean,
},
toggleForm: {
required: true,
type: Function,
},
updateConfidentialAttribute: {
required: true,
type: Function,
},
},
computed: {
onOrOff() {
return this.isConfidential ? 'Turn Off' : 'Turn On';
},
updateConfidentialBool() {
return !this.isConfidential;
},
},
};
</script>
<template>
<div class="confidential-warning-message-actions">
<button
type="button"
class="btn btn-default append-right-10"
@click="toggleForm"
>
Cancel
</button>
<button
type="button"
class="btn btn-close"
@click.prevent="updateConfidentialAttribute(updateConfidentialBool)"
>
{{ onOrOff }}
</button>
</div>
</template>
import Vue from 'vue'; import Vue from 'vue';
import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking'; import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
import sidebarAssignees from './components/assignees/sidebar_assignees'; import sidebarAssignees from './components/assignees/sidebar_assignees';
import confidential from './components/confidential/confidential_issue_sidebar.vue';
import Mediator from './sidebar_mediator'; import Mediator from './sidebar_mediator';
...@@ -10,13 +11,28 @@ function domContentLoaded() { ...@@ -10,13 +11,28 @@ function domContentLoaded() {
mediator.fetch(); mediator.fetch();
const sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees'); const sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
const confidentialEl = document.querySelector('#js-confidential-entry-point');
// Only create the sidebarAssignees vue app if it is found in the DOM // Only create the sidebarAssignees vue app if it is found in the DOM
// We currently do not use sidebarAssignees for the MR page // We currently do not use sidebarAssignees for the MR page
if (sidebarAssigneesEl) { if (sidebarAssigneesEl) {
new Vue(sidebarAssignees).$mount(sidebarAssigneesEl); new Vue(sidebarAssignees).$mount(sidebarAssigneesEl);
} }
if (confidentialEl) {
const dataNode = document.getElementById('js-confidential-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
const ConfidentialComp = Vue.extend(confidential);
new ConfidentialComp({
propsData: {
isConfidential: initialData.is_confidential,
isEditable: initialData.is_editable,
service: mediator.service,
},
}).$mount(confidentialEl);
}
new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker'); new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker');
} }
......
import 'core-js/es6/map';
import 'core-js/es6/set';
import simulateDrag from './simulate_drag'; import simulateDrag from './simulate_drag';
// Export to global space for rspec to use // Export to global space for rspec to use
......
import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
name: 'MRWidgetAuthor', name: 'MRWidgetAuthor',
props: { props: {
...@@ -5,11 +7,14 @@ export default { ...@@ -5,11 +7,14 @@ export default {
showAuthorName: { type: Boolean, required: false, default: true }, showAuthorName: { type: Boolean, required: false, default: true },
showAuthorTooltip: { type: Boolean, required: false, default: false }, showAuthorTooltip: { type: Boolean, required: false, default: false },
}, },
directives: {
tooltip,
},
template: ` template: `
<a <a
:href="author.webUrl || author.web_url" :href="author.webUrl || author.web_url"
class="author-link" class="author-link inline"
:class="{ 'has-tooltip': showAuthorTooltip }" :v-tooltip="showAuthorTooltip"
:title="author.name"> :title="author.name">
<img <img
:src="author.avatarUrl || author.avatar_url" :src="author.avatarUrl || author.avatar_url"
......
/* global Flash */ /* global Flash */
import '~/lib/utils/datetime_utility'; import '~/lib/utils/datetime_utility';
import { statusIconEntityMap } from '../../vue_shared/ci_status_icons';
import MemoryUsage from './mr_widget_memory_usage'; import MemoryUsage from './mr_widget_memory_usage';
import StatusIcon from './mr_widget_status_icon';
import MRWidgetService from '../services/mr_widget_service'; import MRWidgetService from '../services/mr_widget_service';
export default { export default {
...@@ -13,11 +13,7 @@ export default { ...@@ -13,11 +13,7 @@ export default {
}, },
components: { components: {
'mr-widget-memory-usage': MemoryUsage, 'mr-widget-memory-usage': MemoryUsage,
}, 'status-icon': StatusIcon,
computed: {
svg() {
return statusIconEntityMap.icon_status_success;
},
}, },
methods: { methods: {
formatDate(date) { formatDate(date) {
...@@ -51,51 +47,51 @@ export default { ...@@ -51,51 +47,51 @@ export default {
}, },
}, },
template: ` template: `
<div class="mr-widget-heading"> <div class="mr-widget-heading deploy-heading">
<div v-for="deployment in mr.deployments"> <div v-for="deployment in mr.deployments">
<div class="ci-widget"> <div class="ci-widget media">
<div class="ci-status-icon ci-status-icon-success"> <div class="ci-status-icon ci-status-icon-success">
<span class="js-icon-link icon-link"> <span class="js-icon-link icon-link">
<span class="ci-status-icon" <status-icon status="success" />
v-html="svg"
aria-hidden="true"></span>
</span> </span>
</div> </div>
<span> <div class="media-body space-children">
<span <span>
v-if="hasDeploymentMeta(deployment)"> <span
Deployed to v-if="hasDeploymentMeta(deployment)">
</span> Deployed to
<a </span>
v-if="hasDeploymentMeta(deployment)" <a
:href="deployment.url" v-if="hasDeploymentMeta(deployment)"
target="_blank" :href="deployment.url"
rel="noopener noreferrer nofollow" target="_blank"
class="js-deploy-meta"> rel="noopener noreferrer nofollow"
{{deployment.name}} class="js-deploy-meta inline">
</a> {{deployment.name}}
<span </a>
v-if="hasExternalUrls(deployment)"> <span
on v-if="hasExternalUrls(deployment)">
</span> on
<a </span>
v-if="hasExternalUrls(deployment)" <a
:href="deployment.external_url" v-if="hasExternalUrls(deployment)"
target="_blank" :href="deployment.external_url"
rel="noopener noreferrer nofollow" target="_blank"
class="js-deploy-url"> rel="noopener noreferrer nofollow"
<i class="js-deploy-url inline">
class="fa fa-external-link" <i
aria-hidden="true" /> class="fa fa-external-link"
{{deployment.external_url_formatted}} aria-hidden="true" />
</a> {{deployment.external_url_formatted}}
<span </a>
v-if="hasDeploymentTime(deployment)" <span
:data-title="deployment.deployed_at_formatted" v-if="hasDeploymentTime(deployment)"
class="js-deploy-time" :data-title="deployment.deployed_at_formatted"
data-toggle="tooltip" class="js-deploy-time"
data-placement="top"> data-toggle="tooltip"
{{formatDate(deployment.deployed_at)}} data-placement="top">
{{formatDate(deployment.deployed_at)}}
</span>
</span> </span>
<button <button
type="button" type="button"
...@@ -104,13 +100,13 @@ export default { ...@@ -104,13 +100,13 @@ export default {
class="btn btn-default btn-xs"> class="btn btn-default btn-xs">
Stop environment Stop environment
</button> </button>
</span> <mr-widget-memory-usage
v-if="deployment.metrics_url"
:metrics-url="deployment.metrics_url"
:metrics-monitoring-url="deployment.metrics_monitoring_url"
/>
</div>
</div> </div>
<mr-widget-memory-usage
v-if="deployment.metrics_url"
:metrics-url="deployment.metrics_url"
:metrics-monitoring-url="deployment.metrics_monitoring_url"
/>
</div> </div>
</div> </div>
`, `,
......
import tooltip from '../../vue_shared/directives/tooltip';
import '../../lib/utils/text_utility'; import '../../lib/utils/text_utility';
export default { export default {
...@@ -5,6 +6,9 @@ export default { ...@@ -5,6 +6,9 @@ export default {
props: { props: {
mr: { type: Object, required: true }, mr: { type: Object, required: true },
}, },
directives: {
tooltip,
},
computed: { computed: {
shouldShowCommitsBehindText() { shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0; return this.mr.divergedCommitsCount > 0;
...@@ -29,18 +33,51 @@ export default { ...@@ -29,18 +33,51 @@ export default {
}, },
template: ` template: `
<div class="mr-source-target"> <div class="mr-source-target">
<div <div class="normal">
v-if="mr.isOpen" <strong>
class="pull-right"> Request to merge
<span
class="label-branch"
:class="{'label-truncated': isBranchTitleLong(mr.sourceBranch)}"
:title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
data-placement="bottom"
:v-tooltip="isBranchTitleLong(mr.sourceBranch)"
v-html="mr.sourceBranchLink"></span>
<button
v-tooltip
class="btn btn-transparent btn-clipboard"
data-title="Copy branch name to clipboard"
:data-clipboard-text="branchNameClipboardData">
<i
aria-hidden="true"
class="fa fa-clipboard"></i>
</button>
into
<span
class="label-branch"
:v-tooltip="isBranchTitleLong(mr.sourceBranch)"
:class="{'label-truncatedtooltip': isBranchTitleLong(mr.targetBranch)}"
:title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
data-placement="bottom">
<a :href="mr.targetBranchTreePath">{{mr.targetBranch}}</a>
</span>
</strong>
<span
v-if="shouldShowCommitsBehindText"
class="diverged-commits-count">
(<a :href="mr.targetBranchPath">{{mr.divergedCommitsCount}} {{commitsText}} behind</a>)
</span>
</div>
<div v-if="mr.isOpen">
<a <a
href="#modal_merge_info" href="#modal_merge_info"
data-toggle="modal" data-toggle="modal"
class="btn inline btn-grouped btn-sm"> class="btn btn-small inline">
Check out branch Check out branch
</a> </a>
<span class="dropdown inline prepend-left-5"> <span class="dropdown inline prepend-left-10">
<a <a
class="btn btn-sm dropdown-toggle" class="btn btn-xs dropdown-toggle"
data-toggle="dropdown" data-toggle="dropdown"
aria-label="Download as" aria-label="Download as"
role="button"> role="button">
...@@ -69,38 +106,6 @@ export default { ...@@ -69,38 +106,6 @@ export default {
</ul> </ul>
</span> </span>
</div> </div>
<div class="normal">
<strong>
Request to merge
<span
class="label-branch"
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.sourceBranch)}"
:title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
data-placement="bottom"
v-html="mr.sourceBranchLink"></span>
<button
class="btn btn-transparent btn-clipboard has-tooltip"
data-title="Copy branch name to clipboard"
:data-clipboard-text="branchNameClipboardData">
<i
aria-hidden="true"
class="fa fa-clipboard"></i>
</button>
into
<span
class="label-branch"
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}"
:title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
data-placement="bottom">
<a :href="mr.targetBranchTreePath">{{mr.targetBranch}}</a>
</span>
</strong>
<span
v-if="shouldShowCommitsBehindText"
class="diverged-commits-count">
(<a :href="mr.targetBranchPath">{{mr.divergedCommitsCount}} {{commitsText}} behind</a>)
</span>
</div>
</div> </div>
`, `,
}; };
...@@ -120,13 +120,12 @@ export default { ...@@ -120,13 +120,12 @@ export default {
}, },
template: ` template: `
<div class="mr-info-list clearfix mr-memory-usage js-mr-memory-usage"> <div class="mr-info-list clearfix mr-memory-usage js-mr-memory-usage">
<div class="legend"></div>
<p <p
v-if="shouldShowLoading" v-if="shouldShowLoading"
class="usage-info js-usage-info usage-info-loading"> class="usage-info js-usage-info usage-info-loading">
<i <i
class="fa fa-spinner fa-spin usage-info-load-spinner" class="fa fa-spinner fa-spin usage-info-load-spinner"
aria-hidden="true" />Loading deployment statistics. aria-hidden="true" />Loading deployment statistics
</p> </p>
<p <p
v-if="shouldShowMemoryGraph" v-if="shouldShowMemoryGraph"
...@@ -136,12 +135,12 @@ export default { ...@@ -136,12 +135,12 @@ export default {
<p <p
v-if="shouldShowLoadFailure" v-if="shouldShowLoadFailure"
class="usage-info js-usage-info usage-info-failed"> class="usage-info js-usage-info usage-info-failed">
Failed to load deployment statistics. Failed to load deployment statistics
</p> </p>
<p <p
v-if="shouldShowMetricsUnavailable" v-if="shouldShowMetricsUnavailable"
class="usage-info js-usage-info usage-info-unavailable"> class="usage-info js-usage-info usage-info-unavailable">
Deployment statistics are not available currently. Deployment statistics are not available currently
</p> </p>
<mr-memory-graph <mr-memory-graph
v-if="shouldShowMemoryGraph" v-if="shouldShowMemoryGraph"
......
...@@ -16,7 +16,7 @@ export default { ...@@ -16,7 +16,7 @@ export default {
<a <a
data-toggle="modal" data-toggle="modal"
href="#modal_merge_info"> href="#modal_merge_info">
command line. command line
</a> </a>
</section> </section>
`, `,
......
...@@ -29,58 +29,55 @@ export default { ...@@ -29,58 +29,55 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-heading"> <div class="mr-widget-heading">
<div class="ci-widget"> <div class="ci-widget media">
<template v-if="hasCIError"> <template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error"> <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
<span class="js-icon-link icon-link"> <span
<span v-html="svg"
v-html="svg" aria-hidden="true"></span>
aria-hidden="true"></span> </div>
</span> <div class="media-body">
Could not connect to the CI server. Please check your settings and try again
</div> </div>
<span>Could not connect to the CI server. Please check your settings and try again.</span>
</template> </template>
<template v-else> <template v-else>
<div> <div class="ci-status-icon append-right-10">
<a <a
class="icon-link" class="icon-link"
:href="this.status.details_path"> :href="this.status.details_path">
<ci-icon :status="status" /> <ci-icon :status="status" />
</a> </a>
</div> </div>
<span> <div class="media-body">
Pipeline <span>
<a Pipeline
:href="mr.pipeline.path" <a
class="pipeline-id">#{{mr.pipeline.id}}</a> :href="mr.pipeline.path"
{{mr.pipeline.details.status.label}} class="pipeline-id">#{{mr.pipeline.id}}</a>
</span> </span>
<span <span class="mr-widget-pipeline-graph">
v-if="mr.pipeline.details.stages.length > 0"> <span class="stage-cell">
with {{stageText}} <div
</span> v-if="mr.pipeline.details.stages.length > 0"
<div class="mr-widget-pipeline-graph"> v-for="stage in mr.pipeline.details.stages"
<div class="stage-cell"> class="stage-container dropdown js-mini-pipeline-graph">
<div <pipeline-stage :stage="stage" />
v-if="mr.pipeline.details.stages.length > 0" </div>
v-for="stage in mr.pipeline.details.stages" </span>
class="stage-container dropdown js-mini-pipeline-graph"> </span>
<pipeline-stage :stage="stage" /> <span>
</div> {{mr.pipeline.details.status.label}} for
</div> <a
:href="mr.pipeline.commit.commit_path"
class="commit-sha js-commit-link">
{{mr.pipeline.commit.short_id}}</a>.
</span>
<span
v-if="mr.pipeline.coverage"
class="js-mr-coverage">
Coverage {{mr.pipeline.coverage}}%
</span>
</div> </div>
<span>
for
<a
:href="mr.pipeline.commit.commit_path"
class="commit-sha js-commit-link">
{{mr.pipeline.commit.short_id}}</a>.
</span>
<span
v-if="mr.pipeline.coverage"
class="js-mr-coverage">
Coverage {{mr.pipeline.coverage}}%.
</span>
</template> </template>
</div> </div>
</div> </div>
......
...@@ -2,37 +2,32 @@ export default { ...@@ -2,37 +2,32 @@ export default {
name: 'MRWidgetRelatedLinks', name: 'MRWidgetRelatedLinks',
props: { props: {
relatedLinks: { type: Object, required: true }, relatedLinks: { type: Object, required: true },
state: { type: String, required: false },
}, },
computed: { computed: {
hasLinks() { hasLinks() {
const { closing, mentioned, assignToMe } = this.relatedLinks; const { closing, mentioned, assignToMe } = this.relatedLinks;
return closing || mentioned || assignToMe; return closing || mentioned || assignToMe;
}, },
}, closesText() {
methods: { if (this.state === 'merged') {
hasMultipleIssues(text) { return 'Closed';
return !text ? false : text.match(/<\/a> and <a/); }
}, if (this.state === 'closed') {
issueLabel(field) { return 'Did not close';
return this.hasMultipleIssues(this.relatedLinks[field]) ? 'issues' : 'issue'; }
}, return 'Closes';
verbLabel(field) {
return this.hasMultipleIssues(this.relatedLinks[field]) ? 'are' : 'is';
}, },
}, },
template: ` template: `
<section <section
v-if="hasLinks" v-if="hasLinks"
class="mr-info-list mr-links"> class="mr-info-list mr-links">
<div class="legend"></div>
<p v-if="relatedLinks.closing"> <p v-if="relatedLinks.closing">
Closes {{issueLabel('closing')}} {{closesText}} <span v-html="relatedLinks.closing"></span>
<span v-html="relatedLinks.closing"></span>.
</p> </p>
<p v-if="relatedLinks.mentioned"> <p v-if="relatedLinks.mentioned">
<span class="capitalize">{{issueLabel('mentioned')}}</span> Mentions <span v-html="relatedLinks.mentioned"></span>
<span v-html="relatedLinks.mentioned"></span>
{{verbLabel('mentioned')}} mentioned but will not be closed.
</p> </p>
<p v-if="relatedLinks.assignToMe"> <p v-if="relatedLinks.assignToMe">
<span v-html="relatedLinks.assignToMe"></span> <span v-html="relatedLinks.assignToMe"></span>
......
import ciIcon from '../../vue_shared/components/ci_icon.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
props: {
status: { type: String, required: true },
showDisabledButton: { type: Boolean, required: false },
},
components: {
ciIcon,
loadingIcon,
},
computed: {
statusObj() {
return {
group: this.status,
icon: `icon_status_${this.status}`,
};
},
},
template: `
<div class="space-children flex-container-block append-right-10">
<div v-if="status === 'loading'" class="mr-widget-icon">
<loading-icon />
</div>
<ci-icon v-else :status="statusObj" />
<button
v-if="showDisabledButton"
type="button"
class="btn btn-success btn-small"
disabled="true">
Merge
</button>
</div>
`,
};
import statusIcon from '../mr_widget_status_icon';
export default { export default {
name: 'MRWidgetArchived', name: 'MRWidgetArchived',
components: {
statusIcon,
},
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body media">
<button <div class="space-children">
type="button" <status-icon status="failed" />
class="btn btn-success btn-small" <button
disabled="true"> type="button"
Merge class="btn btn-success btn-small"
</button> disabled="true">
<span class="bold"> Merge
This project is archived, write access has been disabled. </button>
</span> </div>
<div class="media-body">
<span class="bold">
This project is archived, write access has been disabled
</span>
</div>
</div> </div>
`, `,
}; };
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon';
export default { export default {
name: 'MRWidgetAutoMergeFailed', name: 'MRWidgetAutoMergeFailed',
...@@ -10,6 +11,9 @@ export default { ...@@ -10,6 +11,9 @@ export default {
isRefreshing: false, isRefreshing: false,
}; };
}, },
components: {
statusIcon,
},
methods: { methods: {
refreshWidget() { refreshWidget() {
this.isRefreshing = true; this.isRefreshing = true;
...@@ -19,18 +23,16 @@ export default { ...@@ -19,18 +23,16 @@ export default {
}, },
}, },
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body media">
<button <status-icon status="failed" />
class="btn btn-success btn-small" <div class="media-body space-children">
disabled="true" <span class="bold">
type="button"> <template v-if="mr.mergeError">{{mr.mergeError}}.</template>
Merge This merge request failed to be merged automatically
</button> </span>
<span class="bold danger">
This merge request failed to be merged automatically.
<button <button
@click="refreshWidget" @click="refreshWidget"
:class="{ disabled: isRefreshing }" :disabled="isRefreshing"
type="button" type="button"
class="btn btn-xs btn-default"> class="btn btn-xs btn-default">
<i <i
...@@ -39,9 +41,6 @@ export default { ...@@ -39,9 +41,6 @@ export default {
aria-hidden="true" /> aria-hidden="true" />
Refresh Refresh
</button> </button>
</span>
<div class="merge-error-text danger bold">
{{mr.mergeError}}
</div> </div>
</div> </div>
`, `,
......
import statusIcon from '../mr_widget_status_icon';
export default { export default {
name: 'MRWidgetChecking', name: 'MRWidgetChecking',
components: {
statusIcon,
},
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body media">
<button <status-icon status="loading" showDisabledButton />
type="button" <div class="media-body space-children">
class="btn btn-success btn-small" <span class="bold">
disabled="true"> Checking ability to merge automatically
Merge </span>
</button> </div>
<span class="bold">
Checking ability to merge automatically.
<i
class="fa fa-spinner fa-spin"
aria-hidden="true" />
</span>
</div> </div>
`, `,
}; };
import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import statusIcon from '../mr_widget_status_icon';
export default { export default {
name: 'MRWidgetClosed', name: 'MRWidgetClosed',
...@@ -7,24 +8,28 @@ export default { ...@@ -7,24 +8,28 @@ export default {
}, },
components: { components: {
'mr-widget-author-and-time': mrWidgetAuthorTime, 'mr-widget-author-and-time': mrWidgetAuthorTime,
statusIcon,
}, },
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body media">
<mr-widget-author-and-time <status-icon status="failed" />
actionText="Closed by" <div class="media-body">
:author="mr.closedBy" <mr-widget-author-and-time
:dateTitle="mr.updatedAt" actionText="Closed by"
:dateReadable="mr.closedAt" :author="mr.closedBy"
/> :dateTitle="mr.updatedAt"
<section> :dateReadable="mr.closedAt"
<p> />
The changes were not merged into <section class="mr-info-list">
<a <p>
:href="mr.targetBranchPath" The changes were not merged into
class="label-branch"> <a
{{mr.targetBranch}}</a>. :href="mr.targetBranchPath"
</p> class="label-branch">
</section> {{mr.targetBranch}}</a>
</p>
</section>
</div>
</div> </div>
`, `,
}; };
import statusIcon from '../mr_widget_status_icon';
export default { export default {
name: 'MRWidgetConflicts', name: 'MRWidgetConflicts',
props: { props: {
mr: { type: Object, required: true }, mr: { type: Object, required: true },
}, },
components: {
statusIcon,
},
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body media">
<button <status-icon status="failed" showDisabledButton />
type="button" <div class="media-body space-children">
class="btn btn-success btn-small" <span class="bold">
disabled="true"> There are merge conflicts<span v-if="!mr.canMerge">.</span>
Merge <span v-if="!mr.canMerge">
</button> Resolve these conflicts or ask someone with write access to this repository to merge it locally
<span class="bold"> </span>
There are merge conflicts.
<span v-if="!mr.canMerge">
Resolve these conflicts or ask someone with write access to this repository to merge it locally.
</span> </span>
</span>
<div
v-if="mr.canMerge"
class="btn-group">
<a <a
v-if="mr.conflictResolutionPath" v-if="mr.canMerge && mr.conflictResolutionPath"
:href="mr.conflictResolutionPath" :href="mr.conflictResolutionPath"
class="btn btn-default btn-xs js-resolve-conflicts-button"> class="btn btn-default btn-xs js-resolve-conflicts-button">
Resolve conflicts Resolve conflicts
......
import statusIcon from '../mr_widget_status_icon';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
export default { export default {
...@@ -38,39 +39,40 @@ export default { ...@@ -38,39 +39,40 @@ export default {
} }
}, },
}, },
components: {
statusIcon,
},
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body media">
<button <template v-if="isRefreshing">
class="btn btn-success btn-small" <status-icon status="loading" />
disabled="true" <span class="media-body bold js-refresh-label">
type="button"> Refreshing now
Merge
</button>
<span
v-if="!isRefreshing"
class="bold danger">
<span
class="has-error-message"
v-if="mr.mergeError">
{{mr.mergeError}}
</span>
<span v-else>Merge failed.</span>
<span
:class="{ 'has-custom-error': mr.mergeError }">
Refreshing in {{timerText}} to show the updated status...
</span> </span>
<button </template>
@click="refresh" <template v-else>
class="btn btn-default btn-xs js-refresh-button" <status-icon status="failed" showDisabledButton />
type="button"> <div class="media-body space-children">
Refresh now <span class="bold">
</button> <span
</span> class="has-error-message"
<span v-if="mr.mergeError">
v-if="isRefreshing" {{mr.mergeError}}.
class="bold js-refresh-label"> </span>
Refreshing now... <span v-else>Merge failed.</span>
</span> <span
:class="{ 'has-custom-error': mr.mergeError }">
Refreshing in {{timerText}} to show the updated status...
</span>
</span>
<button
@click="refresh"
class="btn btn-default btn-xs js-refresh-button"
type="button">
Refresh now
</button>
</div>
</template>
</div> </div>
`, `,
}; };
export default {
name: 'MRWidgetLocked',
props: {
mr: { type: Object, required: true },
},
template: `
<div class="mr-widget-body mr-state-locked">
<span class="state-label">Locked</span>
This merge request is in the process of being merged, during which time it is locked and cannot be closed.
<i
class="fa fa-spinner fa-spin"
aria-hidden="true" />
<section class="mr-info-list mr-links">
<div class="legend"></div>
<p>
The changes will be merged into
<span class="label-branch">
<a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
</span>.
</p>
</section>
</div>
`,
};
/* global Flash */ /* global Flash */
import statusIcon from '../mr_widget_status_icon';
import MRWidgetAuthor from '../../components/mr_widget_author'; import MRWidgetAuthor from '../../components/mr_widget_author';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
...@@ -11,6 +11,7 @@ export default { ...@@ -11,6 +11,7 @@ export default {
}, },
components: { components: {
'mr-widget-author': MRWidgetAuthor, 'mr-widget-author': MRWidgetAuthor,
statusIcon,
}, },
data() { data() {
return { return {
...@@ -61,56 +62,56 @@ export default { ...@@ -61,56 +62,56 @@ export default {
}, },
}, },
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body media">
<h4> <status-icon status="success" />
Set by <div class="media-body">
<mr-widget-author :author="mr.setToMWPSBy" /> <h4>
to be merged automatically when the pipeline succeeds. Set by
<a <mr-widget-author :author="mr.setToMWPSBy" />
v-if="mr.canCancelAutomaticMerge" to be merged automatically when the pipeline succeeds
@click.prevent="cancelAutomaticMerge"
:disabled="isCancellingAutoMerge"
role="button"
href="#"
class="btn btn-xs btn-default js-cancel-auto-merge">
<i
v-if="isCancellingAutoMerge"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
Cancel automatic merge
</a>
</h4>
<section class="mr-info-list">
<div class="legend"></div>
<p>The changes will be merged into
<a
:href="mr.targetBranchPath"
class="label-branch">
{{mr.targetBranch}}
</a>.
</p>
<p v-if="mr.shouldRemoveSourceBranch">
The source branch will be removed.
</p>
<p
v-else
class="with-button">
The source branch will not be removed.
<a <a
v-if="canRemoveSourceBranch" v-if="mr.canCancelAutomaticMerge"
:disabled="isRemovingSourceBranch" @click.prevent="cancelAutomaticMerge"
@click.prevent="removeSourceBranch" :disabled="isCancellingAutoMerge"
role="button" role="button"
class="btn btn-xs btn-default js-remove-source-branch" href="#"
href="#"> class="btn btn-xs btn-default js-cancel-auto-merge">
<i <i
v-if="isRemovingSourceBranch" v-if="isCancellingAutoMerge"
class="fa fa-spinner fa-spin" class="fa fa-spinner fa-spin"
aria-hidden="true" /> aria-hidden="true" />
Remove source branch Cancel automatic merge
</a> </a>
</p> </h4>
</section> <section class="mr-info-list">
<p>The changes will be merged into
<a
:href="mr.targetBranchPath"
class="label-branch">
{{mr.targetBranch}}
</a>
</p>
<p v-if="mr.shouldRemoveSourceBranch">
The source branch will be removed
</p>
<p v-else>
The source branch will not be removed
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
@click.prevent="removeSourceBranch"
role="button"
class="btn btn-xs btn-default js-remove-source-branch"
href="#">
<i
v-if="isRemovingSourceBranch"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
Remove source branch
</a>
</p>
</section>
</div>
</div> </div>
`, `,
}; };
/* global Flash */ /* global Flash */
import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import tooltip from '../../../vue_shared/directives/tooltip';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import statusIcon from '../mr_widget_status_icon';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
export default { export default {
...@@ -9,14 +12,19 @@ export default { ...@@ -9,14 +12,19 @@ export default {
mr: { type: Object, required: true }, mr: { type: Object, required: true },
service: { type: Object, required: true }, service: { type: Object, required: true },
}, },
components: {
'mr-widget-author-and-time': mrWidgetAuthorTime,
},
data() { data() {
return { return {
isMakingRequest: false, isMakingRequest: false,
}; };
}, },
directives: {
tooltip,
},
components: {
'mr-widget-author-and-time': mrWidgetAuthorTime,
loadingIcon,
statusIcon,
},
computed: { computed: {
shouldShowRemoveSourceBranch() { shouldShowRemoveSourceBranch() {
const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr; const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr;
...@@ -55,75 +63,77 @@ export default { ...@@ -55,75 +63,77 @@ export default {
}, },
}, },
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body media">
<mr-widget-author-and-time <status-icon status="success" />
actionText="Merged by" <div class="media-body">
:author="mr.mergedBy" <div class="space-children">
:dateTitle="mr.updatedAt" <mr-widget-author-and-time
:dateReadable="mr.mergedAt" /> actionText="Merged by"
<section class="mr-info-list"> :author="mr.mergedBy"
<div class="legend"></div> :dateTitle="mr.updatedAt"
<p> :dateReadable="mr.mergedAt" />
The changes were merged into <a
<span class="label-branch"> v-if="mr.canRevertInCurrentMR"
<a :href="mr.targetBranchPath">{{mr.targetBranch}}</a> v-tooltip
</span> class="btn btn-close btn-xs"
</p> href="#modal-revert-commit"
<p v-if="mr.sourceBranchRemoved">The source branch has been removed.</p> data-toggle="modal"
<p v-if="shouldShowRemoveSourceBranch"> data-container="body"
You can remove source branch now. title="Revert this merge request in a new merge request">
<button Revert
@click="removeSourceBranch" </a>
:class="{ disabled: isMakingRequest }" <a
type="button" v-else-if="mr.revertInForkPath"
class="btn btn-xs btn-default js-remove-branch-button"> v-tooltip
Remove Source Branch class="btn btn-close btn-xs"
</button> data-method="post"
</p> :href="mr.revertInForkPath"
<p v-if="shouldShowSourceBranchRemoving"> title="Revert this merge request in a new merge request">
<i Revert
class="fa fa-spinner fa-spin" </a>
aria-hidden="true" /> <a
The source branch is being removed. v-if="mr.canCherryPickInCurrentMR"
</p> v-tooltip
</section> class="btn btn-default btn-xs"
<div href="#modal-cherry-pick-commit"
v-if="shouldShowMergedButtons" data-toggle="modal"
class="merged-buttons clearfix"> data-container="body"
<a title="Cherry-pick this merge request in a new merge request">
v-if="mr.canRevertInCurrentMR" Cherry-pick
class="btn btn-close btn-sm has-tooltip" </a>
href="#modal-revert-commit" <a
data-toggle="modal" v-else-if="mr.cherryPickInForkPath"
data-container="body" v-tooltip
title="Revert this merge request in a new merge request"> class="btn btn-default btn-xs"
Revert data-method="post"
</a> :href="mr.cherryPickInForkPath"
<a title="Cherry-pick this merge request in a new merge request">
v-else-if="mr.revertInForkPath" Cherry-pick
class="btn btn-close btn-sm has-tooltip" </a>
data-method="post" </div>
:href="mr.revertInForkPath" <section class="mr-info-list">
title="Revert this merge request in a new merge request"> <p>
Revert The changes were merged into
</a> <span class="label-branch">
<a <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
v-if="mr.canCherryPickInCurrentMR" </span>
class="btn btn-default btn-sm has-tooltip" </p>
href="#modal-cherry-pick-commit" <p v-if="mr.sourceBranchRemoved">The source branch has been removed</p>
data-toggle="modal" <p v-if="shouldShowRemoveSourceBranch" class="space-children">
data-container="body" <span>You can remove source branch now</span>
title="Cherry-pick this merge request in a new merge request"> <button
Cherry-pick @click="removeSourceBranch"
</a> :disabled="isMakingRequest"
<a type="button"
v-else-if="mr.cherryPickInForkPath" class="btn btn-xs btn-default js-remove-branch-button">
class="btn btn-default btn-sm has-tooltip" Remove Source Branch
data-method="post" </button>
:href="mr.cherryPickInForkPath" </p>
title="Cherry-pick this merge request in a new merge request"> <p v-if="shouldShowSourceBranchRemoving">
Cherry-pick <loading-icon inline />
</a> <span>The source branch is being removed</span>
</p>
</section>
</div> </div>
</div> </div>
`, `,
......
import statusIcon from '../mr_widget_status_icon';
export default {
name: 'MRWidgetMerging',
props: {
mr: { type: Object, required: true },
},
components: {
statusIcon,
},
template: `
<div class="mr-widget-body mr-state-locked media">
<status-icon status="loading" />
<div class="media-body">
<h4>
This merge request is in the process of being merged
</h4>
<section class="mr-info-list">
<p>
The changes will be merged into
<span class="label-branch">
<a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
</span>
</p>
</section>
</div>
</div>
`,
};
import statusIcon from '../mr_widget_status_icon';
import tooltip from '../../../vue_shared/directives/tooltip';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help'; import mrWidgetMergeHelp from '../../components/mr_widget_merge_help';
export default { export default {
...@@ -5,30 +7,37 @@ export default { ...@@ -5,30 +7,37 @@ export default {
props: { props: {
mr: { type: Object, required: true }, mr: { type: Object, required: true },
}, },
directives: {
tooltip,
},
components: { components: {
'mr-widget-merge-help': mrWidgetMergeHelp, 'mr-widget-merge-help': mrWidgetMergeHelp,
statusIcon,
}, },
computed: { computed: {
missingBranchName() { missingBranchName() {
return this.mr.sourceBranchRemoved ? 'source' : 'target'; return this.mr.sourceBranchRemoved ? 'source' : 'target';
}, },
message() {
return `If the ${this.missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line`;
},
}, },
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body media">
<button <status-icon status="failed" showDisabledButton />
type="button" <div class="media-body space-children">
class="btn btn-success btn-small" <span class="bold js-branch-text">
disabled="true"> <span class="capitalize">
Merge {{missingBranchName}}
</button> </span> branch does not exist.
<span class="bold js-branch-text"> Please restore it or use a different {{missingBranchName}} branch
<span class="capitalize"> <i
{{missingBranchName}} v-tooltip
</span> branch does not exist. class="fa fa-question-circle"
Please restore the {{missingBranchName}} branch or use a different {{missingBranchName}} branch. :title="message"
</span> :aria-label="message"></i>
<mr-widget-merge-help </span>
:missing-branch="missingBranchName" /> </div>
</div> </div>
`, `,
}; };
import statusIcon from '../mr_widget_status_icon';
export default { export default {
name: 'MRWidgetNotAllowed', name: 'MRWidgetNotAllowed',
components: {
statusIcon,
},
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body media">
<button <status-icon status="success" showDisabledButton />
type="button" <div class="media-body space-children">
class="btn btn-success btn-small" <span class="bold">
disabled="true"> Ready to be merged automatically.
Merge Ask someone with write access to this repository to merge this request
</button> </span>
<span class="bold"> </div>
Ready to be merged automatically.
Ask someone with write access to this repository to merge this request.
</span>
</div> </div>
`, `,
}; };
...@@ -12,7 +12,7 @@ export default { ...@@ -12,7 +12,7 @@ export default {
return { emptyStateSVG }; return { emptyStateSVG };
}, },
template: ` template: `
<div class="mr-widget-body empty-state"> <div class="mr-widget-body mr-widget-empty-state">
<div class="row"> <div class="row">
<div class="artwork col-sm-5 col-sm-push-7 col-xs-12 text-center"> <div class="artwork col-sm-5 col-sm-push-7 col-xs-12 text-center">
<span v-html="emptyStateSVG"></span> <span v-html="emptyStateSVG"></span>
...@@ -29,12 +29,14 @@ export default { ...@@ -29,12 +29,14 @@ export default {
Currently there are no changes in this merge request's source branch. Currently there are no changes in this merge request's source branch.
Please push new commits or use a different branch. Please push new commits or use a different branch.
</p> </p>
<a <div>
v-if="mr.newBlobPath" <a
:href="mr.newBlobPath" v-if="mr.newBlobPath"
class="btn btn-inverted btn-save"> :href="mr.newBlobPath"
Create file class="btn btn-inverted btn-save">
</a> Create file
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
......
import statusIcon from '../mr_widget_status_icon';
export default { export default {
name: 'MRWidgetPipelineBlocked', name: 'MRWidgetPipelineBlocked',
components: {
statusIcon,
},
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body media">
<button <status-icon status="failed" showDisabledButton />
type="button" <div class="media-body space-children">
class="btn btn-success btn-small" <span class="bold">
disabled="true"> Pipeline blocked. The pipeline for this merge request requires a manual action to proceed
Merge </span>
</button> </div>
<span class="bold">
Pipeline blocked. The pipeline for this merge request requires a manual action to proceed.
</span>
</div> </div>
`, `,
}; };
import statusIcon from '../mr_widget_status_icon';
export default { export default {
name: 'MRWidgetPipelineBlocked', name: 'MRWidgetPipelineBlocked',
components: {
statusIcon,
},
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body media">
<button <status-icon status="failed" showDisabledButton />
class="btn btn-success btn-small" <div class="media-body space-children">
disabled="true" <span class="bold">
type="button"> The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
Merge </span>
</button> </div>
<span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure.
</span>
</div> </div>
`, `,
}; };
/* global Flash */ /* global Flash */
import successSvg from 'icons/_icon_status_success.svg'; import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg'; import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll'; import simplePoll from '~/lib/utils/simple_poll';
import statusIcon from '../mr_widget_status_icon';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
export default { export default {
...@@ -25,6 +25,9 @@ export default { ...@@ -25,6 +25,9 @@ export default {
warningSvg, warningSvg,
}; };
}, },
components: {
statusIcon,
},
computed: { computed: {
commitMessageLinkTitle() { commitMessageLinkTitle() {
const withDesc = 'Include description in commit message'; const withDesc = 'Include description in commit message';
...@@ -196,84 +199,98 @@ export default { ...@@ -196,84 +199,98 @@ export default {
}, },
}, },
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body media">
<span class="btn-group"> <status-icon status="success" />
<button <div class="media-body">
@click="handleMergeButtonClick()" <div class="media space-children">
:disabled="isMergeButtonDisabled" <span class="btn-group">
:class="mergeButtonClass" <button
type="button"> @click="handleMergeButtonClick()"
<i :disabled="isMergeButtonDisabled"
v-if="isMakingRequest" :class="mergeButtonClass"
class="fa fa-spinner fa-spin" type="button">
aria-hidden="true" /> <i
{{mergeButtonText}} v-if="isMakingRequest"
</button> class="fa fa-spinner fa-spin"
<button aria-hidden="true" />
v-if="shouldShowMergeOptionsDropdown" {{mergeButtonText}}
:disabled="isMergeButtonDisabled" </button>
type="button" <button
class="btn btn-small btn-info dropdown-toggle" v-if="shouldShowMergeOptionsDropdown"
data-toggle="dropdown"> :disabled="isMergeButtonDisabled"
<i type="button"
class="fa fa-caret-down" class="btn btn-small btn-info dropdown-toggle js-merge-moment"
aria-hidden="true" /> data-toggle="dropdown"
<span class="sr-only"> aria-label="Select merge moment">
Select merge moment <i
class="fa fa-chevron-down"
aria-hidden="true" />
</button>
<ul
v-if="shouldShowMergeOptionsDropdown"
class="dropdown-menu dropdown-menu-right"
role="menu">
<li>
<a
@click.prevent="handleMergeButtonClick(true)"
class="merge_when_pipeline_succeeds"
href="#">
<span class="media">
<span
v-html="successSvg"
class="merge-opt-icon"
aria-hidden="true"></span>
<span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
</span>
</a>
</li>
<li>
<a
@click.prevent="handleMergeButtonClick(false, true)"
class="accept-merge-request"
href="#">
<span class="media">
<span
v-html="warningSvg"
class="merge-opt-icon"
aria-hidden="true"></span>
<span class="media-body merge-opt-title">Merge immediately</span>
</span>
</a>
</li>
</ul>
</span> </span>
</button> <div class="media-body space-children">
<ul <template v-if="isMergeAllowed()">
v-if="shouldShowMergeOptionsDropdown" <label>
class="dropdown-menu dropdown-menu-right" <input
role="menu"> id="remove-source-branch-input"
<li> v-model="removeSourceBranch"
<a :disabled="isRemoveSourceBranchButtonDisabled"
@click.prevent="handleMergeButtonClick(true)" type="checkbox"/> Remove source branch
class="merge_when_pipeline_succeeds" </label>
href="#">
<span
v-html="successSvg"
class="merge-opt-icon"
aria-hidden="true"></span>
<span class="merge-opt-title">Merge when pipeline succeeds</span>
</a>
</li>
<li>
<a
@click.prevent="handleMergeButtonClick(false, true)"
class="accept-merge-request"
href="#">
<span
v-html="warningSvg"
class="merge-opt-icon"
aria-hidden="true"></span>
<span class="merge-opt-title">Merge immediately</span>
</a>
</li>
</ul>
</span>
<template v-if="isMergeAllowed()">
<label class="spacing">
<input
id="remove-source-branch-input"
v-model="removeSourceBranch"
:disabled="isRemoveSourceBranchButtonDisabled"
type="checkbox"/> Remove source branch
</label>
<!-- Placeholder for EE extension of this component --> <!-- Placeholder for EE extension of this component -->
<squash-before-merge <squash-before-merge
v-if="shouldShowSquashBeforeMerge" v-if="shouldShowSquashBeforeMerge"
:mr="mr" :mr="mr"
:is-merge-button-disabled="isMergeButtonDisabled" /> :is-merge-button-disabled="isMergeButtonDisabled" />
<button <button
@click="toggleCommitMessageEditor" @click="toggleCommitMessageEditor"
:disabled="isMergeButtonDisabled" :disabled="isMergeButtonDisabled"
class="btn btn-default btn-xs" class="btn btn-default btn-xs"
type="button"> type="button">
Modify commit message Modify commit message
</button> </button>
</template>
<template v-else>
<span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
</span>
</template>
</div>
</div>
<div <div
v-if="showCommitMessageEditor" v-if="showCommitMessageEditor"
class="prepend-top-default commit-message-editor"> class="prepend-top-default commit-message-editor">
...@@ -293,7 +310,7 @@ export default { ...@@ -293,7 +310,7 @@ export default {
rows="14" rows="14"
name="Commit message"></textarea> name="Commit message"></textarea>
</div> </div>
<p class="hint">Try to keep the first line under 52 characters and the others under 72.</p> <p class="hint">Try to keep the first line under 52 characters and the others under 72</p>
<div class="hint"> <div class="hint">
<a <a
@click.prevent="updateCommitMessage" @click.prevent="updateCommitMessage"
...@@ -302,12 +319,7 @@ export default { ...@@ -302,12 +319,7 @@ export default {
</div> </div>
</div> </div>
</div> </div>
</template> </div>
<template v-else>
<span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure.
</span>
</template>
</div> </div>
`, `,
}; };
import statusIcon from '../mr_widget_status_icon';
export default { export default {
name: 'MRWidgetSHAMismatch', name: 'MRWidgetSHAMismatch',
components: {
statusIcon,
},
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body media">
<button <status-icon status="failed" showDisabledButton />
type="button" <div class="media-body space-children">
class="btn btn-success btn-small" <span class="bold">
disabled="true"> The source branch HEAD has recently changed. Please reload the page and review the changes before merging
Merge </span>
</button> </div>
<span class="bold">
The source branch HEAD has recently changed. Please reload the page and review the changes before merging.
</span>
</div> </div>
`, `,
}; };
import statusIcon from '../mr_widget_status_icon';
export default { export default {
name: 'MRWidgetUnresolvedDiscussions', name: 'MRWidgetUnresolvedDiscussions',
props: { props: {
mr: { type: Object, required: true }, mr: { type: Object, required: true },
}, },
components: {
statusIcon,
},
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body media">
<button <status-icon status="failed" showDisabledButton />
type="button" <div class="media-body space-children">
class="btn btn-success btn-small" <span class="bold">
disabled="true"> There are unresolved discussions. Please resolve these discussions
Merge </span>
</button> <a
<span class="bold"> v-if="mr.createIssueToResolveDiscussionsPath"
There are unresolved discussions. Please resolve these discussions :href="mr.createIssueToResolveDiscussionsPath"
<span v-if="mr.canCreateIssue">or</span> class="btn btn-default btn-xs js-create-issue">
<span v-else>.</span> Create an issue to resolve them later
</span> </a>
<a </div>
v-if="mr.createIssueToResolveDiscussionsPath"
:href="mr.createIssueToResolveDiscussionsPath"
class="btn btn-default btn-xs js-create-issue">
Create an issue to resolve them later
</a>
</div> </div>
`, `,
}; };
/* global Flash */ /* global Flash */
import statusIcon from '../mr_widget_status_icon';
import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
export default { export default {
...@@ -7,11 +9,17 @@ export default { ...@@ -7,11 +9,17 @@ export default {
mr: { type: Object, required: true }, mr: { type: Object, required: true },
service: { type: Object, required: true }, service: { type: Object, required: true },
}, },
directives: {
tooltip,
},
data() { data() {
return { return {
isMakingRequest: false, isMakingRequest: false,
}; };
}, },
components: {
statusIcon,
},
methods: { methods: {
removeWIP() { removeWIP() {
this.isMakingRequest = true; this.isMakingRequest = true;
...@@ -29,20 +37,20 @@ export default { ...@@ -29,20 +37,20 @@ export default {
}, },
}, },
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body media">
<button <status-icon status="failed" :showDisabledButton="Boolean(mr.removeWIPPath)" />
type="button" <div class="media-body space-children">
class="btn btn-success btn-small" <span class="bold">
disabled="true"> This is a Work in Progress
Merge</button> <i
<span class="bold"> v-tooltip
This merge request is currently Work In Progress and therefore unable to merge class="fa fa-question-circle"
</span> title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged"
<template v-if="mr.removeWIPPath"> aria-label="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged">
<i </i>
class="fa fa-question-circle has-tooltip" </span>
title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged." />
<button <button
v-if="mr.removeWIPPath"
@click="removeWIP" @click="removeWIP"
:disabled="isMakingRequest" :disabled="isMakingRequest"
type="button" type="button"
...@@ -53,7 +61,7 @@ export default { ...@@ -53,7 +61,7 @@ export default {
aria-hidden="true" /> aria-hidden="true" />
Resolve WIP status Resolve WIP status
</button> </button>
</template> </div>
</div> </div>
`, `,
}; };
...@@ -19,7 +19,7 @@ export { default as WidgetRelatedLinks } from './components/mr_widget_related_li ...@@ -19,7 +19,7 @@ export { default as WidgetRelatedLinks } from './components/mr_widget_related_li
export { default as MergedState } from './components/states/mr_widget_merged'; export { default as MergedState } from './components/states/mr_widget_merged';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge'; export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge';
export { default as ClosedState } from './components/states/mr_widget_closed'; export { default as ClosedState } from './components/states/mr_widget_closed';
export { default as LockedState } from './components/states/mr_widget_locked'; export { default as MergingState } from './components/states/mr_widget_merging';
export { default as WipState } from './components/states/mr_widget_wip'; export { default as WipState } from './components/states/mr_widget_wip';
export { default as ArchivedState } from './components/states/mr_widget_archived'; export { default as ArchivedState } from './components/states/mr_widget_archived';
export { default as ConflictsState } from './components/states/mr_widget_conflicts'; export { default as ConflictsState } from './components/states/mr_widget_conflicts';
......
...@@ -8,7 +8,7 @@ import { ...@@ -8,7 +8,7 @@ import {
WidgetRelatedLinks, WidgetRelatedLinks,
MergedState, MergedState,
ClosedState, ClosedState,
LockedState, MergingState,
WipState, WipState,
ArchivedState, ArchivedState,
ConflictsState, ConflictsState,
...@@ -35,8 +35,14 @@ import { ...@@ -35,8 +35,14 @@ import {
export default { export default {
el: '#js-vue-mr-widget', el: '#js-vue-mr-widget',
name: 'MRWidget', name: 'MRWidget',
props: {
mrData: {
type: Object,
required: false,
},
},
data() { data() {
const store = new MRWidgetStore(gl.mrWidgetData); const store = new MRWidgetStore(this.mrData || window.gl.mrWidgetData);
const service = this.createService(store); const service = this.createService(store);
return { return {
mr: store, mr: store,
...@@ -206,7 +212,7 @@ export default { ...@@ -206,7 +212,7 @@ export default {
'mr-widget-related-links': WidgetRelatedLinks, 'mr-widget-related-links': WidgetRelatedLinks,
'mr-widget-merged': MergedState, 'mr-widget-merged': MergedState,
'mr-widget-closed': ClosedState, 'mr-widget-closed': ClosedState,
'mr-widget-locked': LockedState, 'mr-widget-merging': MergingState,
'mr-widget-failed-to-merge': FailedToMerge, 'mr-widget-failed-to-merge': FailedToMerge,
'mr-widget-wip': WipState, 'mr-widget-wip': WipState,
'mr-widget-archived': ArchivedState, 'mr-widget-archived': ArchivedState,
...@@ -234,14 +240,21 @@ export default { ...@@ -234,14 +240,21 @@ export default {
v-if="shouldRenderDeployments" v-if="shouldRenderDeployments"
:mr="mr" :mr="mr"
:service="service" /> :service="service" />
<component <div class="mr-widget-section">
:is="componentName" <component
:mr="mr" :is="componentName"
:service="service" /> :mr="mr"
<mr-widget-related-links :service="service" />
v-if="shouldRenderRelatedLinks" <mr-widget-related-links
:related-links="mr.relatedLinks" /> v-if="shouldRenderRelatedLinks"
<mr-widget-merge-help v-if="shouldRenderMergeHelp" /> :state="mr.state"
:related-links="mr.relatedLinks" />
</div>
<div
class="mr-widget-footer"
v-if="shouldRenderMergeHelp">
<mr-widget-merge-help />
</div>
</div> </div>
`, `,
}; };
...@@ -73,6 +73,7 @@ export default class MergeRequestStore { ...@@ -73,6 +73,7 @@ export default class MergeRequestStore {
this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path; this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path;
this.hasSHAChanged = this.sha !== data.diff_head_sha; this.hasSHAChanged = this.sha !== data.diff_head_sha;
this.canBeMerged = data.can_be_merged || false; this.canBeMerged = data.can_be_merged || false;
this.mergeOngoing = data.merge_ongoing;
// Cherry-pick and Revert actions related // Cherry-pick and Revert actions related
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false; this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
...@@ -94,6 +95,11 @@ export default class MergeRequestStore { ...@@ -94,6 +95,11 @@ export default class MergeRequestStore {
} }
setState(data) { setState(data) {
if (this.mergeOngoing) {
this.state = 'merging';
return;
}
if (this.isOpen) { if (this.isOpen) {
this.state = getStateKey.call(this, data); this.state = getStateKey.call(this, data);
} else { } else {
...@@ -104,9 +110,6 @@ export default class MergeRequestStore { ...@@ -104,9 +110,6 @@ export default class MergeRequestStore {
case 'closed': case 'closed':
this.state = 'closed'; this.state = 'closed';
break; break;
case 'locked':
this.state = 'locked';
break;
default: default:
this.state = null; this.state = null;
} }
......
const stateToComponentMap = { const stateToComponentMap = {
merged: 'mr-widget-merged', merged: 'mr-widget-merged',
closed: 'mr-widget-closed', closed: 'mr-widget-closed',
locked: 'mr-widget-locked', merging: 'mr-widget-merging',
conflicts: 'mr-widget-conflicts', conflicts: 'mr-widget-conflicts',
missingBranch: 'mr-widget-missing-branch', missingBranch: 'mr-widget-missing-branch',
workInProgress: 'mr-widget-wip', workInProgress: 'mr-widget-wip',
...@@ -20,7 +20,7 @@ const stateToComponentMap = { ...@@ -20,7 +20,7 @@ const stateToComponentMap = {
}; };
const statesToShowHelpWidget = [ const statesToShowHelpWidget = [
'locked', 'merging',
'conflicts', 'conflicts',
'workInProgress', 'workInProgress',
'readyToMerge', 'readyToMerge',
......
<script>
const PopupDialog = {
name: 'popup-dialog',
props: {
open: Boolean,
title: String,
body: String,
kind: {
type: String,
default: 'primary',
},
closeButtonLabel: {
type: String,
default: 'Cancel',
},
primaryButtonLabel: {
type: String,
default: 'Save changes',
},
},
computed: {
typeOfClass() {
const className = `btn-${this.kind}`;
const returnObj = {};
returnObj[className] = true;
return returnObj;
},
},
methods: {
close() {
this.$emit('toggle', false);
},
yesClick() {
this.$emit('submit', true);
},
noClick() {
this.$emit('submit', false);
},
},
};
export default PopupDialog;
</script>
<template>
<div class="modal popup-dialog" tabindex="-1" v-show="open" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" @click="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">{{this.title}}</h4>
</div>
<div class="modal-body">
<p>{{this.body}}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal" @click="noClick">{{closeButtonLabel}}</button>
<button type="button" class="btn" :class="typeOfClass" @click="yesClick">{{primaryButtonLabel}}</button>
</div>
</div>
</div>
</div>
</template>
/* global Breakpoints */ /* global Breakpoints */
import 'vendor/jquery.nicescroll';
import './breakpoints'; import './breakpoints';
export default class Wikis { export default class Wikis {
...@@ -8,7 +7,6 @@ export default class Wikis { ...@@ -8,7 +7,6 @@ export default class Wikis {
this.bp = Breakpoints.get(); this.bp = Breakpoints.get();
this.sidebarEl = document.querySelector('.js-wiki-sidebar'); this.sidebarEl = document.querySelector('.js-wiki-sidebar');
this.sidebarExpanded = false; this.sidebarExpanded = false;
$(this.sidebarEl).niceScroll();
const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle'); const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle');
for (let i = 0; i < sidebarToggles.length; i += 1) { for (let i = 0; i < sidebarToggles.length; i += 1) {
......
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
@import "framework/lists"; @import "framework/lists";
@import "framework/logo"; @import "framework/logo";
@import "framework/markdown_area"; @import "framework/markdown_area";
@import "framework/media_object";
@import "framework/mobile"; @import "framework/mobile";
@import "framework/modal"; @import "framework/modal";
@import "framework/nav"; @import "framework/nav";
......
...@@ -100,6 +100,8 @@ ...@@ -100,6 +100,8 @@
margin: 0; margin: 0;
align-self: center; align-self: center;
} }
&.s40 { min-width: 40px; min-height: 40px; }
} }
.avatar-counter { .avatar-counter {
......
...@@ -372,6 +372,10 @@ table { ...@@ -372,6 +372,10 @@ table {
background: $gl-success !important; background: $gl-success !important;
} }
.dz-message {
margin: 0;
}
.space-right { .space-right {
margin-right: 10px; margin-right: 10px;
} }
......
...@@ -26,7 +26,7 @@ header { ...@@ -26,7 +26,7 @@ header {
&.navbar-gitlab { &.navbar-gitlab {
padding: 0 16px; padding: 0 16px;
z-index: 400; z-index: 1000;
margin-bottom: 0; margin-bottom: 0;
min-height: $header-height; min-height: $header-height;
background-color: $gray-light; background-color: $gray-light;
......
...@@ -109,18 +109,20 @@ body { ...@@ -109,18 +109,20 @@ body {
} }
} }
.page-with-sidebar > .content-wrapper {
/* The following prevents side effects related to iOS Safari's implementation of -webkit-overflow-scrolling: touch, min-height: calc(100vh - #{$header-height});
which is applied to the body by jquery.nicescroling plugin to force hardware acceleration for momentum scrolling. Side
effects are commonly related to inconsisent z-index behavior (e.g. tooltips). By applying the following to direct children
of the body element here, we negate cascading side effects but allow momentum scrolling to be applied to the body */
.navbar,
.page-gutter,
.page-with-sidebar {
-webkit-overflow-scrolling: auto;
} }
.with-performance-bar .page-with-sidebar { .with-performance-bar .page-with-sidebar {
margin-top: $header-height + $performance-bar-height; margin-top: $header-height + $performance-bar-height;
} }
[v-cloak] {
display: none;
}
.vertical-center {
min-height: 100vh;
display: flex;
align-items: center;
}
.media {
display: flex;
align-items: flex-start;
}
.media-body {
flex: 1;
}
...@@ -251,7 +251,6 @@ ...@@ -251,7 +251,6 @@
// Applies on /dashboard/issues // Applies on /dashboard/issues
.project-item-select-holder { .project-item-select-holder {
display: block;
margin: 0; margin: 0;
} }
} }
...@@ -283,6 +282,31 @@ ...@@ -283,6 +282,31 @@
} }
} }
.project-item-select-holder.btn-group {
display: flex;
max-width: 350px;
overflow: hidden;
@media(max-width: $screen-xs-max) {
width: 100%;
max-width: none;
}
.new-project-item-link {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.new-project-item-select-button {
width: 32px;
}
}
.new-project-item-select-button .fa-caret-down {
margin-left: 2px;
}
.layout-nav { .layout-nav {
width: 100%; width: 100%;
background: $gray-light; background: $gray-light;
......
...@@ -78,15 +78,12 @@ ...@@ -78,15 +78,12 @@
.right-sidebar { .right-sidebar {
border-left: 1px solid $border-color; border-left: 1px solid $border-color;
height: calc(100% - #{$header-height});
&.affix { &.affix {
position: fixed; position: fixed;
top: $header-height; top: $header-height;
} }
&:not(.affix-top) {
min-height: 100%;
}
} }
.with-performance-bar .right-sidebar.affix { .with-performance-bar .right-sidebar.affix {
......
...@@ -162,3 +162,5 @@ $pre-color: $gl-text-color !default; ...@@ -162,3 +162,5 @@ $pre-color: $gl-text-color !default;
$pre-border-color: $border-color; $pre-border-color: $border-color;
$table-bg-accent: $gray-light; $table-bg-accent: $gray-light;
$zindex-popover: 900;
...@@ -88,6 +88,7 @@ $indigo-950: #1a1a40; ...@@ -88,6 +88,7 @@ $indigo-950: #1a1a40;
$black: #000; $black: #000;
$black-transparent: rgba(0, 0, 0, 0.3); $black-transparent: rgba(0, 0, 0, 0.3);
$almost-black: #242424;
$border-white-light: darken($white-light, $darken-border-factor); $border-white-light: darken($white-light, $darken-border-factor);
$border-white-normal: darken($white-normal, $darken-border-factor); $border-white-normal: darken($white-normal, $darken-border-factor);
...@@ -206,7 +207,6 @@ $general-hover-transition-curve: linear; ...@@ -206,7 +207,6 @@ $general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232); $highlight-changes-color: rgb(235, 255, 232);
$performance-bar-height: 35px; $performance-bar-height: 35px;
/* /*
* Common component specific colors * Common component specific colors
*/ */
...@@ -315,6 +315,12 @@ $btn-white-active: #848484; ...@@ -315,6 +315,12 @@ $btn-white-active: #848484;
$badge-bg: rgba(0, 0, 0, 0.07); $badge-bg: rgba(0, 0, 0, 0.07);
$badge-color: $gl-text-color-secondary; $badge-color: $gl-text-color-secondary;
/*
* Status icons
*/
$status-icon-size: 22px;
$status-icon-margin: $gl-btn-padding;
/* /*
* Award emoji * Award emoji
*/ */
...@@ -613,6 +619,13 @@ $color-high-score: $green-400; ...@@ -613,6 +619,13 @@ $color-high-score: $green-400;
$color-average-score: $orange-400; $color-average-score: $orange-400;
$color-low-score: $red-400; $color-low-score: $red-400;
/*
Repo editor
*/
$repo-editor-grey: #f6f7f9;
$repo-editor-grey-darker: #e9ebee;
$repo-editor-linear-gradient: linear-gradient(to right, $repo-editor-grey 0%, $repo-editor-grey-darker, 20%, $repo-editor-grey 40%, $repo-editor-grey 100%);
/* /*
Performance Bar Performance Bar
*/ */
...@@ -624,3 +637,11 @@ $perf-bar-bucket-bg: #111; ...@@ -624,3 +637,11 @@ $perf-bar-bucket-bg: #111;
$perf-bar-bucket-color: #ccc; $perf-bar-bucket-color: #ccc;
$perf-bar-bucket-box-shadow-from: rgba($white-light, .2); $perf-bar-bucket-box-shadow-from: rgba($white-light, .2);
$perf-bar-bucket-box-shadow-to: rgba($black, .25); $perf-bar-bucket-box-shadow-to: rgba($black, .25);
/*
Project Templates Icons
*/
$rails: #c00;
$node: #353535;
$java: #70ad51;
...@@ -8,20 +8,25 @@ $active-color: $indigo-700; ...@@ -8,20 +8,25 @@ $active-color: $indigo-700;
$active-hover-background: $active-background; $active-hover-background: $active-background;
$active-hover-color: $gl-text-color; $active-hover-color: $gl-text-color;
$inactive-badge-background: rgba(0, 0, 0, .08); $inactive-badge-background: rgba(0, 0, 0, .08);
$hover-background: $indigo-700; $hover-background: $white-light;
$hover-color: $white-light; $hover-color: $gl-text-color;
$inactive-color: $gl-text-color-secondary; $inactive-color: $gl-text-color-secondary;
$new-sidebar-width: 220px; $new-sidebar-width: 220px;
$new-sidebar-collapsed-width: 50px;
.page-with-new-sidebar { .page-with-new-sidebar {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-md-min) {
padding-left: $new-sidebar-collapsed-width;
}
@media (min-width: $screen-lg-min) {
padding-left: $new-sidebar-width; padding-left: $new-sidebar-width;
} }
// Override position: absolute // Override position: absolute
.right-sidebar { .right-sidebar {
position: fixed; position: fixed;
height: 100%; 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 {
...@@ -29,8 +34,15 @@ $new-sidebar-width: 220px; ...@@ -29,8 +34,15 @@ $new-sidebar-width: 220px;
} }
} }
.page-with-icon-sidebar {
@media (min-width: $screen-sm-min) {
padding-left: $new-sidebar-collapsed-width;
}
}
.context-header { .context-header {
position: relative; position: relative;
margin-right: 2px;
a { a {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
...@@ -39,26 +51,16 @@ $new-sidebar-width: 220px; ...@@ -39,26 +51,16 @@ $new-sidebar-width: 220px;
align-items: center; align-items: center;
padding: 10px 16px 10px 10px; padding: 10px 16px 10px 10px;
color: $gl-text-color; color: $gl-text-color;
}
@media (max-width: $screen-xs-max) { &:hover,
padding-right: 30px; a:hover {
} background-color: $hover-background;
color: $hover-color;
&:hover {
background-color: $hover-background;
color: $hover-color;
border-color: $hover-background;
.avatar-container {
border-color: transparent;
}
.settings-avatar {
background-color: $indigo-500;
i { .settings-avatar {
color: $hover-color; i {
} color: $hover-color;
} }
} }
} }
...@@ -73,32 +75,6 @@ $new-sidebar-width: 220px; ...@@ -73,32 +75,6 @@ $new-sidebar-width: 220px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
&:hover {
.close-nav-button {
color: $white-light;
}
}
.close-nav-button {
display: none;
position: absolute;
top: 0;
right: 0;
height: 100%;
background-color: transparent;
border: 0;
padding: 0 10px;
@media (max-width: $screen-xs-max) {
display: block;
}
&:hover {
color: $gl-text-color;
}
}
} }
.settings-avatar { .settings-avatar {
...@@ -125,6 +101,16 @@ $new-sidebar-width: 220px; ...@@ -125,6 +101,16 @@ $new-sidebar-width: 220px;
background-color: $gray-normal; background-color: $gray-normal;
box-shadow: inset -2px 0 0 $border-color; box-shadow: inset -2px 0 0 $border-color;
&.sidebar-icons-only {
width: $new-sidebar-collapsed-width;
.nav-item-name,
.badge,
.project-title {
display: none;
}
}
&.nav-sidebar-expanded { &.nav-sidebar-expanded {
left: 0; left: 0;
} }
...@@ -219,6 +205,8 @@ $new-sidebar-width: 220px; ...@@ -219,6 +205,8 @@ $new-sidebar-width: 220px;
} }
.sidebar-top-level-items { .sidebar-top-level-items {
margin-bottom: 60px;
> li { > li {
> a { > a {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
...@@ -233,15 +221,15 @@ $new-sidebar-width: 220px; ...@@ -233,15 +221,15 @@ $new-sidebar-width: 220px;
&:not(.active) { &:not(.active) {
> a { > a {
margin-left: 1px; margin-left: 1px;
margin-right: 3px; margin-right: 2px;
} }
.sidebar-sub-level-items { .sidebar-sub-level-items {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
position: fixed; position: fixed;
top: 0; top: 0;
left: 220px; left: $new-sidebar-width;
width: 150px; min-width: 150px;
margin-top: -1px; margin-top: -1px;
padding: 8px 1px; padding: 8px 1px;
background-color: $white-light; background-color: $white-light;
...@@ -326,6 +314,95 @@ $new-sidebar-width: 220px; ...@@ -326,6 +314,95 @@ $new-sidebar-width: 220px;
} }
} }
// Collapsed nav
.toggle-sidebar-button,
.close-nav-button {
width: $new-sidebar-width - 2px;
position: fixed;
bottom: 0;
padding: 16px;
background-color: $gray-normal;
border: 0;
border-top: 2px solid $border-color;
color: $gl-text-color-secondary;
display: flex;
align-items: center;
i {
font-size: 20px;
margin-right: 8px;
}
.fa-angle-double-right {
display: none;
}
&:hover {
background-color: $border-color;
color: $gl-text-color;
}
}
.toggle-sidebar-button {
@media (max-width: $screen-xs-max) {
display: none;
}
}
.sidebar-icons-only {
.context-header {
height: 60px;
a {
padding: 10px 4px;
}
}
li a {
padding: 12px 15px;
}
.sidebar-top-level-items > li {
&.active a {
padding-left: 12px;
}
.sidebar-sub-level-items {
@media (min-width: $screen-sm-min) {
left: $new-sidebar-collapsed-width;
}
&:not(.flyout-list) {
display: none;
}
}
}
.toggle-sidebar-button {
width: $new-sidebar-collapsed-width - 2px;
padding: 16px 18px;
.collapse-text,
.fa-angle-double-left {
display: none;
}
.fa-angle-double-right {
display: block;
}
}
}
// Mobile nav
.close-nav-button {
display: none;
}
.toggle-mobile-nav { .toggle-mobile-nav {
display: none; display: none;
background-color: transparent; background-color: transparent;
...@@ -345,6 +422,12 @@ $new-sidebar-width: 220px; ...@@ -345,6 +422,12 @@ $new-sidebar-width: 220px;
} }
} }
@media (max-width: $screen-xs-max) {
.close-nav-button {
display: flex;
}
}
.mobile-overlay { .mobile-overlay {
display: none; display: none;
......
...@@ -165,6 +165,7 @@ ...@@ -165,6 +165,7 @@
.board-title { .board-title {
padding-top: ($gl-padding - 3px); padding-top: ($gl-padding - 3px);
padding-bottom: $gl-padding;
} }
} }
} }
...@@ -178,6 +179,7 @@ ...@@ -178,6 +179,7 @@
position: relative; position: relative;
margin: 0; margin: 0;
padding: $gl-padding; padding: $gl-padding;
padding-bottom: ($gl-padding + 3px);
font-size: 1em; font-size: 1em;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
......
...@@ -235,8 +235,18 @@ ...@@ -235,8 +235,18 @@
display: none; display: none;
} }
.sidebar-container {
width: calc(100% + 100px);
padding-right: 100px;
height: 100%;
overflow-y: scroll;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}
.blocks-container { .blocks-container {
padding: 0 $gl-padding; padding: 0 $gl-padding;
width: 289px;
} }
.block { .block {
...@@ -259,7 +269,15 @@ ...@@ -259,7 +269,15 @@
padding: 16px 0; padding: 16px 0;
} }
.trigger-build-variables {
margin: 0;
overflow-x: auto;
-ms-overflow-style: scrollbar;
-webkit-overflow-scrolling: touch;
}
.trigger-build-variable { .trigger-build-variable {
font-weight: normal;
color: $code-color; color: $code-color;
} }
...@@ -326,6 +344,7 @@ ...@@ -326,6 +344,7 @@
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
max-height: 300px; max-height: 300px;
width: 289px;
overflow: auto; overflow: auto;
svg { svg {
......
...@@ -5,6 +5,30 @@ ...@@ -5,6 +5,30 @@
margin-right: auto; margin-right: auto;
} }
.is-confidential {
color: $orange-600;
background-color: $orange-50;
border-radius: 3px;
padding: 5px;
margin: 0 3px 0 -4px;
}
.is-not-confidential {
border-radius: 3px;
padding: 5px;
margin: 0 3px 0 -4px;
}
.confidentiality {
.is-not-confidential {
margin: auto;
}
.is-confidential {
margin: auto;
}
}
.limit-container-width { .limit-container-width {
.detail-page-header, .detail-page-header,
.page-content-header, .page-content-header,
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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