Commit caf49264 authored by Mike Greiling's avatar Mike Greiling

Merge branch 'master' into upgrade-to-webpack-v4

* master: (252 commits)
  Upgrade underscore.js
  Enable prometheus metrics by default
  Add signature verification badge to compare view
  Add Changelog
  Update instalation from source guide
  fix Web IDE file tree scroll issue
  Enable quick support actions default
  Backport of 4084-epics-username-autocomplete
  Remove top margin on the terms page with performance bar
  Backports every CE related change from ee-44542 to CE
  Fix typo in changelog entry
  fix missing space
  Backport IdentityLinker#failed? from GroupSaml callback flow
  Add ci_cd_settings delete_all dependency on project
  AutoDevOps Docs fix invalid external link
  Ignore knapsack and rspec_flaky
  Ensure web hook 'blocked URL' errors are stored in as web hook logs and properly surfaced to the user
  Partially revert ebcd5711 to fix runner pages
  Reuses `InternalRedirect` when possible
  Enforces terms in the web application
  ...
parents 33bd0d4f 9f7a6742
...@@ -68,6 +68,8 @@ eslint-report.html ...@@ -68,6 +68,8 @@ eslint-report.html
/shared/* /shared/*
/.gitlab_workhorse_secret /.gitlab_workhorse_secret
/webpack-report/ /webpack-report/
/knapsack/
/rspec_flaky/
/locale/**/LC_MESSAGES /locale/**/LC_MESSAGES
/locale/**/*.time_stamp /locale/**/*.time_stamp
/.rspec /.rspec
......
...@@ -2,6 +2,20 @@ ...@@ -2,6 +2,20 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.7.3 (2018-05-02)
### Fixed (8 changes)
- Fixed wrong avatar URL when the avatar is on object storage. !18092
- Fix errors on pushing to an empty repository. !18462
- Update doorkeeper to 4.3.2 to fix GitLab OAuth authentication. !18543
- Ports omniauth-jwt gem onto GitLab OmniAuth Strategies suite. !18580
- Fix redirection error for applications using OpenID. !18599
- Fix commit trailer rendering when Gravatar is disabled.
- Fix file_store for artifacts and lfs when saving.
- Fix users not seeing labels from private groups when being a member of a child project.
## 10.7.2 (2018-04-25) ## 10.7.2 (2018-04-25)
### Security (2 changes) ### Security (2 changes)
......
...@@ -9,6 +9,10 @@ terms. ...@@ -9,6 +9,10 @@ terms.
[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md) [DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md)
All Documentation content that resides under the [doc/ directory](/doc) of this
repository is licensed under Creative Commons:
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
_This notice should stay as the first item in the CONTRIBUTING.md file._ _This notice should stay as the first item in the CONTRIBUTING.md file._
--- ---
......
...@@ -33,7 +33,7 @@ gem 'grape-route-helpers', '~> 2.1.0' ...@@ -33,7 +33,7 @@ gem 'grape-route-helpers', '~> 2.1.0'
gem 'faraday', '~> 0.12' gem 'faraday', '~> 0.12'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.4'
gem 'doorkeeper', '~> 4.3' gem 'doorkeeper', '~> 4.3'
gem 'doorkeeper-openid_connect', '~> 1.3' gem 'doorkeeper-openid_connect', '~> 1.3'
gem 'omniauth', '~> 1.8' gem 'omniauth', '~> 1.8'
...@@ -41,7 +41,7 @@ gem 'omniauth-auth0', '~> 2.0.0' ...@@ -41,7 +41,7 @@ gem 'omniauth-auth0', '~> 2.0.0'
gem 'omniauth-azure-oauth2', '~> 0.0.9' gem 'omniauth-azure-oauth2', '~> 0.0.9'
gem 'omniauth-cas3', '~> 1.1.4' gem 'omniauth-cas3', '~> 1.1.4'
gem 'omniauth-facebook', '~> 4.0.0' gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-github', '~> 1.3'
gem 'omniauth-gitlab', '~> 1.0.2' gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.5.3' gem 'omniauth-google-oauth2', '~> 0.5.3'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
...@@ -90,7 +90,7 @@ gem 'github-linguist', '~> 5.3.3', require: 'linguist' ...@@ -90,7 +90,7 @@ gem 'github-linguist', '~> 5.3.3', require: 'linguist'
# API # API
gem 'grape', '~> 1.0' gem 'grape', '~> 1.0'
gem 'grape-entity', '~> 0.6.0' gem 'grape-entity', '~> 0.7.1'
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors' gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
# Disable strong_params so that Mash does not respond to :permitted? # Disable strong_params so that Mash does not respond to :permitted?
...@@ -184,6 +184,9 @@ gem 're2', '~> 1.1.1' ...@@ -184,6 +184,9 @@ gem 're2', '~> 1.1.1'
gem 'version_sorter', '~> 2.1.0' gem 'version_sorter', '~> 2.1.0'
# User agent parsing
gem 'device_detector'
# Cache # Cache
gem 'redis-rails', '~> 5.0.2' gem 'redis-rails', '~> 5.0.2'
......
...@@ -143,7 +143,7 @@ GEM ...@@ -143,7 +143,7 @@ GEM
connection_pool (2.2.1) connection_pool (2.2.1)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
crass (1.0.3) crass (1.0.4)
creole (0.5.0) creole (0.5.0)
css_parser (1.5.0) css_parser (1.5.0)
addressable addressable
...@@ -161,10 +161,11 @@ GEM ...@@ -161,10 +161,11 @@ GEM
activerecord (>= 3.2.0, < 5.1) activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
devise (4.2.0) device_detector (1.0.0)
devise (4.4.3)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0, < 5.1) railties (>= 4.1.0, < 6.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-two-factor (3.0.0) devise-two-factor (3.0.0)
...@@ -365,8 +366,8 @@ GEM ...@@ -365,8 +366,8 @@ GEM
rack (>= 1.3.0) rack (>= 1.3.0)
rack-accept rack-accept
virtus (>= 1.0.0) virtus (>= 1.0.0)
grape-entity (0.6.0) grape-entity (0.7.1)
activesupport activesupport (>= 4.0)
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
grape-route-helpers (2.1.0) grape-route-helpers (2.1.0)
activesupport activesupport
...@@ -545,9 +546,9 @@ GEM ...@@ -545,9 +546,9 @@ GEM
omniauth (~> 1.2) omniauth (~> 1.2)
omniauth-facebook (4.0.0) omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2) omniauth-oauth2 (~> 1.2)
omniauth-github (1.1.2) omniauth-github (1.3.0)
omniauth (~> 1.0) omniauth (~> 1.5)
omniauth-oauth2 (~> 1.1) omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-gitlab (1.0.2) omniauth-gitlab (1.0.2)
omniauth (~> 1.0) omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0) omniauth-oauth2 (~> 1.0)
...@@ -645,7 +646,7 @@ GEM ...@@ -645,7 +646,7 @@ GEM
pry (>= 0.9.10) pry (>= 0.9.10)
public_suffix (3.0.2) public_suffix (3.0.2)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
rack (1.6.9) rack (1.6.10)
rack-accept (0.4.5) rack-accept (0.4.5)
rack (>= 0.4) rack (>= 0.4)
rack-attack (4.4.1) rack-attack (4.4.1)
...@@ -693,7 +694,7 @@ GEM ...@@ -693,7 +694,7 @@ GEM
rainbow (2.2.2) rainbow (2.2.2)
rake rake
raindrops (0.18.0) raindrops (0.18.0)
rake (12.3.0) rake (12.3.1)
rb-fsevent (0.10.2) rb-fsevent (0.10.2)
rb-inotify (0.9.10) rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2) ffi (>= 0.5.0, < 2)
...@@ -734,8 +735,9 @@ GEM ...@@ -734,8 +735,9 @@ GEM
declarative-option (< 0.2.0) declarative-option (< 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
request_store (1.3.1) request_store (1.3.1)
responders (2.3.0) responders (2.4.0)
railties (>= 4.2.0, < 5.1) actionpack (>= 4.2.0, < 5.3)
railties (>= 4.2.0, < 5.3)
rest-client (2.0.2) rest-client (2.0.2)
http-cookie (>= 1.0.2, < 2.0) http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0) mime-types (>= 1.16, < 4.0)
...@@ -965,7 +967,7 @@ GEM ...@@ -965,7 +967,7 @@ GEM
descendants_tracker (~> 0.0, >= 0.0.3) descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9) equalizer (~> 0.0, >= 0.0.9)
vmstat (2.3.0) vmstat (2.3.0)
warden (1.2.6) warden (1.2.7)
rack (>= 1.0) rack (>= 1.0)
webmock (2.3.2) webmock (2.3.2)
addressable (>= 2.3.6) addressable (>= 2.3.6)
...@@ -1026,7 +1028,8 @@ DEPENDENCIES ...@@ -1026,7 +1028,8 @@ DEPENDENCIES
database_cleaner (~> 1.5.0) database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0) deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.0) default_value_for (~> 3.0.0)
devise (~> 4.2) device_detector
devise (~> 4.4)
devise-two-factor (~> 3.0.0) devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0) diffy (~> 3.1.0)
doorkeeper (~> 4.3) doorkeeper (~> 4.3)
...@@ -1070,7 +1073,7 @@ DEPENDENCIES ...@@ -1070,7 +1073,7 @@ DEPENDENCIES
google-protobuf (= 3.5.1) google-protobuf (= 3.5.1)
gpgme gpgme
grape (~> 1.0) grape (~> 1.0)
grape-entity (~> 0.6.0) grape-entity (~> 0.7.1)
grape-route-helpers (~> 2.1.0) grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7) grape_logging (~> 1.7)
grpc (~> 1.11.0) grpc (~> 1.11.0)
...@@ -1111,7 +1114,7 @@ DEPENDENCIES ...@@ -1111,7 +1114,7 @@ DEPENDENCIES
omniauth-azure-oauth2 (~> 0.0.9) omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4) omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0) omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1) omniauth-github (~> 1.3)
omniauth-gitlab (~> 1.0.2) omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.3) omniauth-google-oauth2 (~> 0.5.3)
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
......
...@@ -162,6 +162,7 @@ GEM ...@@ -162,6 +162,7 @@ GEM
activerecord (>= 3.2.0, < 5.2) activerecord (>= 3.2.0, < 5.2)
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
device_detector (1.0.1)
devise (4.4.1) devise (4.4.1)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
...@@ -304,12 +305,12 @@ GEM ...@@ -304,12 +305,12 @@ GEM
flowdock (~> 0.7) flowdock (~> 0.7)
gitlab-grit (>= 2.4.1) gitlab-grit (>= 2.4.1)
multi_json multi_json
gitlab-gollum-lib (4.2.7.1) gitlab-gollum-lib (4.2.7.2)
gemojione (~> 3.2) gemojione (~> 3.2)
github-markup (~> 1.6) github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0) gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0) nokogiri (>= 1.6.1, < 2.0)
rouge (~> 2.1) rouge (~> 3.1)
sanitize (~> 2.1) sanitize (~> 2.1)
stringex (~> 2.6) stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4) gitlab-gollum-rugged_adapter (0.4.4)
...@@ -375,7 +376,7 @@ GEM ...@@ -375,7 +376,7 @@ GEM
rake rake
grape_logging (1.7.0) grape_logging (1.7.0)
grape grape
grpc (1.10.0) grpc (1.11.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0) googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7) googleauth (>= 0.5.1, < 0.7)
...@@ -554,9 +555,6 @@ GEM ...@@ -554,9 +555,6 @@ GEM
jwt (>= 1.5) jwt (>= 1.5)
omniauth (>= 1.1.1) omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.5) omniauth-oauth2 (>= 1.5)
omniauth-jwt (0.0.2)
jwt
omniauth (~> 1.1)
omniauth-kerberos (0.3.0) omniauth-kerberos (0.3.0)
omniauth-multipassword omniauth-multipassword
timfel-krb5-auth (~> 0.8) timfel-krb5-auth (~> 0.8)
...@@ -602,8 +600,6 @@ GEM ...@@ -602,8 +600,6 @@ GEM
atomic (>= 1.0.0) atomic (>= 1.0.0)
mysql2 mysql2
peek peek
peek-performance_bar (1.3.1)
peek (>= 0.1.0)
peek-pg (1.3.0) peek-pg (1.3.0)
concurrent-ruby concurrent-ruby
concurrent-ruby-ext concurrent-ruby-ext
...@@ -752,7 +748,7 @@ GEM ...@@ -752,7 +748,7 @@ GEM
retriable (3.1.1) retriable (3.1.1)
rinku (2.0.4) rinku (2.0.4)
rotp (2.1.2) rotp (2.1.2)
rouge (2.2.1) rouge (3.1.1)
rqrcode (0.10.1) rqrcode (0.10.1)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
...@@ -1035,6 +1031,7 @@ DEPENDENCIES ...@@ -1035,6 +1031,7 @@ DEPENDENCIES
database_cleaner (~> 1.5.0) database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0) deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.5) default_value_for (~> 3.0.5)
device_detector
devise (~> 4.2) devise (~> 4.2)
devise-two-factor (~> 3.0.0) devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0) diffy (~> 3.1.0)
...@@ -1082,7 +1079,7 @@ DEPENDENCIES ...@@ -1082,7 +1079,7 @@ DEPENDENCIES
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0) grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7) grape_logging (~> 1.7)
grpc (~> 1.10.0) grpc (~> 1.11.0)
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
hamlit (~> 2.6.1) hamlit (~> 2.6.1)
hashie-forbidden_attributes hashie-forbidden_attributes
...@@ -1123,7 +1120,6 @@ DEPENDENCIES ...@@ -1123,7 +1120,6 @@ DEPENDENCIES
omniauth-github (~> 1.1.1) omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.2) omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.3) omniauth-google-oauth2 (~> 0.5.3)
omniauth-jwt (~> 0.0.2)
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2) omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10) omniauth-saml (~> 1.10)
...@@ -1134,7 +1130,6 @@ DEPENDENCIES ...@@ -1134,7 +1130,6 @@ DEPENDENCIES
peek (~> 1.0.1) peek (~> 1.0.1)
peek-gc (~> 0.0.2) peek-gc (~> 0.0.2)
peek-mysql2 (~> 1.1.0) peek-mysql2 (~> 1.1.0)
peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0) peek-pg (~> 1.3.0)
peek-rblineprof (~> 0.2.0) peek-rblineprof (~> 0.2.0)
peek-redis (~> 1.2.0) peek-redis (~> 1.2.0)
...@@ -1166,7 +1161,7 @@ DEPENDENCIES ...@@ -1166,7 +1161,7 @@ DEPENDENCIES
redis-rails (~> 5.0.2) redis-rails (~> 5.0.2)
request_store (~> 1.3) request_store (~> 1.3)
responders (~> 2.0) responders (~> 2.0)
rouge (~> 2.0) rouge (~> 3.1)
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-parameterized rspec-parameterized
rspec-rails (~> 3.6.0) rspec-rails (~> 3.6.0)
......
...@@ -4,4 +4,9 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of ...@@ -4,4 +4,9 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
---
All Documentation content that resides under the doc/ directory of this
repository is licensed under Creative Commons: CC BY-SA 4.0.
...@@ -7,27 +7,24 @@ export default function installGlEmojiElement() { ...@@ -7,27 +7,24 @@ export default function installGlEmojiElement() {
const GlEmojiElementProto = Object.create(HTMLElement.prototype); const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() { GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim(); const emojiUnicode = this.textContent.trim();
const { const { name, unicodeVersion, fallbackSrc, fallbackSpriteClass } = this.dataset;
name,
unicodeVersion,
fallbackSrc,
fallbackSpriteClass,
} = this.dataset;
const isEmojiUnicode = this.childNodes && Array.prototype.every.call( const isEmojiUnicode =
this.childNodes, this.childNodes &&
childNode => childNode.nodeType === 3, Array.prototype.every.call(this.childNodes, childNode => childNode.nodeType === 3);
);
const hasImageFallback = fallbackSrc && fallbackSrc.length > 0; const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0; const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
if ( if (emojiUnicode && isEmojiUnicode && !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)) {
emojiUnicode &&
isEmojiUnicode &&
!isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
) {
// CSS sprite fallback takes precedence over image fallback // CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) { if (hasCssSpriteFalback) {
if (!gon.emoji_sprites_css_added && gon.emoji_sprites_css_path) {
const emojiSpriteLinkTag = document.createElement('link');
emojiSpriteLinkTag.setAttribute('rel', 'stylesheet');
emojiSpriteLinkTag.setAttribute('href', gon.emoji_sprites_css_path);
document.head.appendChild(emojiSpriteLinkTag);
gon.emoji_sprites_css_added = true;
}
// IE 11 doesn't like adding multiple at once :( // IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon'); this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass); this.classList.add(fallbackSpriteClass);
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
import $ from 'jquery';
import { localTimeAgo } from './lib/utils/datetime_utility';
import axios from './lib/utils/axios_utils';
export default class Compare {
constructor(opts) {
this.opts = opts;
this.source_loading = $(".js-source-loading");
this.target_loading = $(".js-target-loading");
$('.js-compare-dropdown').each((function(_this) {
return function(i, dropdown) {
var $dropdown;
$dropdown = $(dropdown);
return $dropdown.glDropdown({
selectable: true,
fieldName: $dropdown.data('fieldName'),
filterable: true,
id: function(obj, $el) {
return $el.data('id');
},
toggleLabel: function(obj, $el) {
return $el.text().trim();
},
clicked: function(e, el) {
if ($dropdown.is('.js-target-branch')) {
return _this.getTargetHtml();
} else if ($dropdown.is('.js-source-branch')) {
return _this.getSourceHtml();
} else if ($dropdown.is('.js-target-project')) {
return _this.getTargetProject();
}
}
});
};
})(this));
this.initialState();
}
initialState() {
this.getSourceHtml();
this.getTargetHtml();
}
getTargetProject() {
$('.mr_target_commit').empty();
return axios.get(this.opts.targetProjectUrl, {
params: {
target_project_id: $("input[name='merge_request[target_project_id]']").val(),
},
}).then(({ data }) => {
$('.js-target-branch-dropdown .dropdown-content').html(data);
});
}
getSourceHtml() {
return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
ref: $("input[name='merge_request[source_branch]']").val()
});
}
getTargetHtml() {
return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
target_project_id: $("input[name='merge_request[target_project_id]']").val(),
ref: $("input[name='merge_request[target_branch]']").val()
});
}
static sendAjax(url, loading, target, params) {
const $target = $(target);
loading.show();
$target.empty();
return axios.get(url, {
params,
}).then(({ data }) => {
loading.hide();
$target.html(data);
const className = '.' + $target[0].className.replace(' ', '.');
localTimeAgo($('.js-timeago', className));
});
}
}
...@@ -4,8 +4,9 @@ import $ from 'jquery'; ...@@ -4,8 +4,9 @@ import $ from 'jquery';
import { __ } from './locale'; import { __ } from './locale';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import flash from './flash'; import flash from './flash';
import { capitalizeFirstCharacter } from './lib/utils/text_utility';
export default function initCompareAutocomplete() { export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
$('.js-compare-dropdown').each(function() { $('.js-compare-dropdown').each(function() {
var $dropdown, selected; var $dropdown, selected;
$dropdown = $(this); $dropdown = $(this);
...@@ -15,14 +16,27 @@ export default function initCompareAutocomplete() { ...@@ -15,14 +16,27 @@ export default function initCompareAutocomplete() {
const $filterInput = $('input[type="search"]', $dropdownContainer); const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown.glDropdown({ $dropdown.glDropdown({
data: function(term, callback) { data: function(term, callback) {
axios.get($dropdown.data('refsUrl'), { const params = {
params: { ref: $dropdown.data('ref'),
ref: $dropdown.data('ref'), search: term,
search: term, };
},
}).then(({ data }) => { if (limitTo) {
callback(data); params.find = limitTo;
}).catch(() => flash(__('Error fetching refs'))); }
axios
.get($dropdown.data('refsUrl'), {
params,
})
.then(({ data }) => {
if (limitTo) {
callback(data[capitalizeFirstCharacter(limitTo)] || []);
} else {
callback(data);
}
})
.catch(() => flash(__('Error fetching refs')));
}, },
selectable: true, selectable: true,
filterable: true, filterable: true,
...@@ -32,9 +46,15 @@ export default function initCompareAutocomplete() { ...@@ -32,9 +46,15 @@ export default function initCompareAutocomplete() {
renderRow: function(ref) { renderRow: function(ref) {
var link; var link;
if (ref.header != null) { if (ref.header != null) {
return $('<li />').addClass('dropdown-header').text(ref.header); return $('<li />')
.addClass('dropdown-header')
.text(ref.header);
} else { } else {
link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref)); link = $('<a />')
.attr('href', '#')
.addClass(ref === selected ? 'is-active' : '')
.text(ref)
.attr('data-ref', escape(ref));
return $('<li />').append(link); return $('<li />').append(link);
} }
}, },
...@@ -43,9 +63,10 @@ export default function initCompareAutocomplete() { ...@@ -43,9 +63,10 @@ export default function initCompareAutocomplete() {
}, },
toggleLabel: function(obj, $el) { toggleLabel: function(obj, $el) {
return $el.text().trim(); return $el.text().trim();
} },
clicked: () => clickHandler($dropdown),
}); });
$filterInput.on('keyup', (e) => { $filterInput.on('keyup', e => {
const keyCode = e.keyCode || e.which; const keyCode = e.keyCode || e.which;
if (keyCode !== 13) return; if (keyCode !== 13) return;
const text = $filterInput.val(); const text = $filterInput.val();
...@@ -54,7 +75,7 @@ export default function initCompareAutocomplete() { ...@@ -54,7 +75,7 @@ export default function initCompareAutocomplete() {
$dropdownContainer.removeClass('open'); $dropdownContainer.removeClass('open');
}); });
$dropdownContainer.on('click', '.dropdown-content a', (e) => { $dropdownContainer.on('click', '.dropdown-content a', e => {
$dropdown.prop('title', e.target.text.replace(/_+?/g, '-')); $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) { if ($dropdown.hasClass('has-tooltip')) {
$dropdown.tooltip('fixTitle'); $dropdown.tooltip('fixTitle');
......
...@@ -34,7 +34,7 @@ export function getEmojiCategoryMap() { ...@@ -34,7 +34,7 @@ export function getEmojiCategoryMap() {
symbols: [], symbols: [],
flags: [], flags: [],
}; };
Object.keys(emojiMap).forEach((name) => { Object.keys(emojiMap).forEach(name => {
const emoji = emojiMap[name]; const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.category]) { if (emojiCategoryMap[emoji.category]) {
emojiCategoryMap[emoji.category].push(name); emojiCategoryMap[emoji.category].push(name);
...@@ -79,7 +79,9 @@ export function glEmojiTag(inputName, options) { ...@@ -79,7 +79,9 @@ export function glEmojiTag(inputName, options) {
classList.push(fallbackSpriteClass); classList.push(fallbackSpriteClass);
} }
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : ''; const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : ''; const fallbackSpriteAttribute = opts.sprite
? `data-fallback-sprite-class="${fallbackSpriteClass}"`
: '';
let contents = emojiInfo.moji; let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) { if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc); contents = emojiImageTag(name, fallbackImageSrc);
......
...@@ -54,7 +54,8 @@ const unicodeSupportTestMap = { ...@@ -54,7 +54,8 @@ const unicodeSupportTestMap = {
function checkPixelInImageDataArray(pixelOffset, imageDataArray) { function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
// `4 *` because RGBA // `4 *` because RGBA
const indexOffset = 4 * pixelOffset; const indexOffset = 4 * pixelOffset;
const hasColor = imageDataArray[indexOffset + 0] || const hasColor =
imageDataArray[indexOffset + 0] ||
imageDataArray[indexOffset + 1] || imageDataArray[indexOffset + 1] ||
imageDataArray[indexOffset + 2]; imageDataArray[indexOffset + 2];
const isVisible = imageDataArray[indexOffset + 3]; const isVisible = imageDataArray[indexOffset + 3];
...@@ -75,23 +76,23 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche ...@@ -75,23 +76,23 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche
const fontSize = 16; const fontSize = 16;
function generateUnicodeSupportMap(testMap) { function generateUnicodeSupportMap(testMap) {
const testMapKeys = Object.keys(testMap); const testMapKeys = Object.keys(testMap);
const numTestEntries = testMapKeys const numTestEntries = testMapKeys.reduce((list, testKey) => list.concat(testMap[testKey]), [])
.reduce((list, testKey) => list.concat(testMap[testKey]), []).length; .length;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
(window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas; (window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
canvas.width = (2 * fontSize); canvas.width = 2 * fontSize;
canvas.height = (numTestEntries * fontSize); canvas.height = numTestEntries * fontSize;
ctx.fillStyle = '#000000'; ctx.fillStyle = '#000000';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`; ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
// Write each emoji to the canvas vertically // Write each emoji to the canvas vertically
let writeIndex = 0; let writeIndex = 0;
testMapKeys.forEach((testKey) => { testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey]; const testEntry = testMap[testKey];
[].concat(testEntry).forEach((emojiUnicode) => { [].concat(testEntry).forEach(emojiUnicode => {
ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2)); ctx.fillText(emojiUnicode, 0, writeIndex * fontSize + fontSize / 2);
writeIndex += 1; writeIndex += 1;
}); });
}); });
...@@ -99,29 +100,25 @@ function generateUnicodeSupportMap(testMap) { ...@@ -99,29 +100,25 @@ function generateUnicodeSupportMap(testMap) {
// Read from the canvas // Read from the canvas
const resultMap = {}; const resultMap = {};
let readIndex = 0; let readIndex = 0;
testMapKeys.forEach((testKey) => { testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey]; const testEntry = testMap[testKey];
// This needs to be a `reduce` instead of `every` because we need to // This needs to be a `reduce` instead of `every` because we need to
// keep the `readIndex` in sync from the writes by running all entries // keep the `readIndex` in sync from the writes by running all entries
const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => { const isTestSatisfied = [].concat(testEntry).reduce(isSatisfied => {
// Sample along the vertical-middle for a couple of characters // Sample along the vertical-middle for a couple of characters
const imageData = ctx.getImageData( const imageData = ctx.getImageData(0, readIndex * fontSize + fontSize / 2, 2 * fontSize, 1)
0, .data;
(readIndex * fontSize) + (fontSize / 2),
2 * fontSize,
1,
).data;
let isValidEmoji = false; let isValidEmoji = false;
for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) { for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
const isLookingAtFirstChar = currentPixel < fontSize; const isLookingAtFirstChar = currentPixel < fontSize;
const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2)); const isLookingAtSecondChar = currentPixel >= fontSize + fontSize / 2;
// Check for the emoji somewhere along the row // Check for the emoji somewhere along the row
if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) { if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = true; isValidEmoji = true;
// Check to see that nothing is rendered next to the first character // Check to see that nothing is rendered next to the first character
// to ensure that the ZWJ sequence rendered as one piece // to ensure that the ZWJ sequence rendered as one piece
} else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) { } else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = false; isValidEmoji = false;
break; break;
...@@ -170,7 +167,10 @@ export default function getUnicodeSupportMap() { ...@@ -170,7 +167,10 @@ export default function getUnicodeSupportMap() {
if (isLocalStorageAvailable) { if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION); window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent); window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap)); window.localStorage.setItem(
'gl-emoji-unicode-support-map',
JSON.stringify(unicodeSupportMap),
);
} }
} }
......
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
<div class="environments-container"> <div class="environments-container">
<loading-icon <loading-icon
class="prepend-top-default"
label="Loading environments" label="Loading environments"
v-if="isLoading" v-if="isLoading"
size="3" size="3"
......
<script> <script>
import playIconSvg from 'icons/_icon_play.svg'; import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
...@@ -8,9 +8,9 @@ ...@@ -8,9 +8,9 @@
directives: { directives: {
tooltip, tooltip,
}, },
components: { components: {
loadingIcon, loadingIcon,
Icon,
}, },
props: { props: {
actions: { actions: {
...@@ -19,20 +19,16 @@ ...@@ -19,20 +19,16 @@
default: () => [], default: () => [],
}, },
}, },
data() { data() {
return { return {
playIconSvg,
isLoading: false, isLoading: false,
}; };
}, },
computed: { computed: {
title() { title() {
return 'Deploy to...'; return 'Deploy to...';
}, },
}, },
methods: { methods: {
onClickAction(endpoint) { onClickAction(endpoint) {
this.isLoading = true; this.isLoading = true;
...@@ -65,7 +61,10 @@ ...@@ -65,7 +61,10 @@
:disabled="isLoading" :disabled="isLoading"
> >
<span> <span>
<span v-html="playIconSvg"></span> <icon
name="play"
:size="12"
/>
<i <i
class="fa fa-caret-down" class="fa fa-caret-down"
aria-hidden="true" aria-hidden="true"
...@@ -86,7 +85,10 @@ ...@@ -86,7 +85,10 @@
:class="{ disabled: isActionDisabled(action) }" :class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)" :disabled="isActionDisabled(action)"
> >
<span v-html="playIconSvg"></span> <icon
name="play"
:size="12"
/>
<span> <span>
{{ action.name }} {{ action.name }}
</span> </span>
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
...@@ -6,6 +7,9 @@ ...@@ -6,6 +7,9 @@
* Renders the external url link in environments table. * Renders the external url link in environments table.
*/ */
export default { export default {
components: {
Icon,
},
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -15,7 +19,6 @@ ...@@ -15,7 +19,6 @@
required: true, required: true,
}, },
}, },
computed: { computed: {
title() { title() {
return s__('Environments|Open'); return s__('Environments|Open');
...@@ -34,10 +37,9 @@ ...@@ -34,10 +37,9 @@
:aria-label="title" :aria-label="title"
:href="externalUrl" :href="externalUrl"
> >
<i <icon
class="fa fa-external-link" name="external-link"
aria-hidden="true" :size="12"
> />
</i>
</a> </a>
</template> </template>
...@@ -2,20 +2,22 @@ ...@@ -2,20 +2,22 @@
/** /**
* Renders the Monitoring (Metrics) link in environments table. * Renders the Monitoring (Metrics) link in environments table.
*/ */
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
components: {
Icon,
},
directives: { directives: {
tooltip, tooltip,
}, },
props: { props: {
monitoringUrl: { monitoringUrl: {
type: String, type: String,
required: true, required: true,
}, },
}, },
computed: { computed: {
title() { title() {
return 'Monitoring'; return 'Monitoring';
...@@ -33,10 +35,9 @@ ...@@ -33,10 +35,9 @@
:title="title" :title="title"
:aria-label="title" :aria-label="title"
> >
<i <icon
class="fa fa-area-chart" name="chart"
aria-hidden="true" :size="12"
> />
</i>
</a> </a>
</template> </template>
...@@ -12,7 +12,6 @@ ...@@ -12,7 +12,6 @@
components: { components: {
loadingIcon, loadingIcon,
}, },
props: { props: {
retryUrl: { retryUrl: {
type: String, type: String,
...@@ -24,13 +23,11 @@ ...@@ -24,13 +23,11 @@
default: true, default: true,
}, },
}, },
data() { data() {
return { return {
isLoading: false, isLoading: false,
}; };
}, },
methods: { methods: {
onClick() { onClick() {
this.isLoading = true; this.isLoading = true;
......
...@@ -3,14 +3,16 @@ ...@@ -3,14 +3,16 @@
* Renders a terminal button to open a web terminal. * Renders a terminal button to open a web terminal.
* Used in environments table. * Used in environments table.
*/ */
import terminalIconSvg from 'icons/_icon_terminal.svg'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
components: {
Icon,
},
directives: { directives: {
tooltip, tooltip,
}, },
props: { props: {
terminalPath: { terminalPath: {
type: String, type: String,
...@@ -18,13 +20,6 @@ ...@@ -18,13 +20,6 @@
default: '', default: '',
}, },
}, },
data() {
return {
terminalIconSvg,
};
},
computed: { computed: {
title() { title() {
return 'Terminal'; return 'Terminal';
...@@ -40,7 +35,10 @@ ...@@ -40,7 +35,10 @@
:title="title" :title="title"
:aria-label="title" :aria-label="title"
:href="terminalPath" :href="terminalPath"
v-html="terminalIconSvg"
> >
<icon
name="terminal"
:size="12"
/>
</a> </a>
</template> </template>
...@@ -408,7 +408,10 @@ class GfmAutoComplete { ...@@ -408,7 +408,10 @@ class GfmAutoComplete {
fetchData($input, at) { fetchData($input, at) {
if (this.isLoadingData[at]) return; if (this.isLoadingData[at]) return;
this.isLoadingData[at] = true; this.isLoadingData[at] = true;
const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]];
if (this.cachedData[at]) { if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]); this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
...@@ -418,12 +421,14 @@ class GfmAutoComplete { ...@@ -418,12 +421,14 @@ class GfmAutoComplete {
GfmAutoComplete.glEmojiTag = glEmojiTag; GfmAutoComplete.glEmojiTag = glEmojiTag;
}) })
.catch(() => { this.isLoadingData[at] = false; }); .catch(() => { this.isLoadingData[at] = false; });
} else { } else if (dataSource) {
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true) AjaxCache.retrieve(dataSource, true)
.then((data) => { .then((data) => {
this.loadData($input, at, data); this.loadData($input, at, data);
}) })
.catch(() => { this.isLoadingData[at] = false; }); .catch(() => { this.isLoadingData[at] = false; });
} else {
this.isLoadingData[at] = false;
} }
} }
......
...@@ -7,12 +7,12 @@ import { __ } from '~/locale'; ...@@ -7,12 +7,12 @@ import { __ } from '~/locale';
export default class GpgBadges { export default class GpgBadges {
static fetch() { static fetch() {
const badges = $('.js-loading-gpg-badge'); const badges = $('.js-loading-gpg-badge');
const form = $('.commits-search-form'); const tag = $('.js-signature-container');
badges.html('<i class="fa fa-spinner fa-spin"></i>'); badges.html('<i class="fa fa-spinner fa-spin"></i>');
const params = parseQueryStringIntoObject(form.serialize()); const params = parseQueryStringIntoObject(tag.serialize());
return axios.get(form.data('signaturesPath'), { params }) return axios.get(tag.data('signaturesPath'), { params })
.then(({ data }) => { .then(({ data }) => {
data.signatures.forEach((signature) => { data.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
......
...@@ -43,7 +43,7 @@ export default { ...@@ -43,7 +43,7 @@ export default {
return `${this.changedIcon}-solid`; return `${this.changedIcon}-solid`;
}, },
changedIconClass() { changedIconClass() {
return `multi-${this.changedIcon} prepend-left-5 pull-left`; return `multi-${this.changedIcon} pull-left`;
}, },
tooltipTitle() { tooltipTitle() {
if (!this.showTooltip) return undefined; if (!this.showTooltip) return undefined;
...@@ -79,13 +79,7 @@ export default { ...@@ -79,13 +79,7 @@ export default {
class="ide-file-changed-icon" class="ide-file-changed-icon"
> >
<icon <icon
v-if="file.staged && showStagedIcon" v-if="file.changed || file.tempFile || file.staged"
:name="stagedIcon"
:size="12"
:css-classes="changedIconClass"
/>
<icon
v-if="file.changed || file.tempFile || (file.staged && !showStagedIcon)"
:name="changedIcon" :name="changedIcon"
:size="12" :size="12"
:css-classes="changedIconClass" :css-classes="changedIconClass"
......
...@@ -15,17 +15,10 @@ export default { ...@@ -15,17 +15,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
committedStateSvgPath: {
type: String,
required: true,
},
}, },
computed: { computed: {
...mapState(['lastCommitMsg', 'rightPanelCollapsed']), ...mapState(['lastCommitMsg', 'rightPanelCollapsed', 'changedFiles', 'stagedFiles']),
...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']), ...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
statusSvg() {
return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
},
}, },
methods: { methods: {
...mapActions(['toggleRightPanelCollapsed']), ...mapActions(['toggleRightPanelCollapsed']),
...@@ -35,6 +28,7 @@ export default { ...@@ -35,6 +28,7 @@ export default {
<template> <template>
<div <div
v-if="!lastCommitMsg"
class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state" class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
> >
<header <header
...@@ -64,12 +58,11 @@ export default { ...@@ -64,12 +58,11 @@ export default {
v-if="!rightPanelCollapsed" v-if="!rightPanelCollapsed"
> >
<div class="svg-content svg-80"> <div class="svg-content svg-80">
<img :src="statusSvg" /> <img :src="noChangesStateSvgPath" />
</div> </div>
<div class="append-right-default prepend-left-default"> <div class="append-right-default prepend-left-default">
<div <div
class="text-content text-center" class="text-content text-center"
v-if="!lastCommitMsg"
> >
<h4> <h4>
{{ __('No changes') }} {{ __('No changes') }}
...@@ -78,15 +71,6 @@ export default { ...@@ -78,15 +71,6 @@ export default {
{{ __('Edit files in the editor and commit changes here') }} {{ __('Edit files in the editor and commit changes here') }}
</p> </p>
</div> </div>
<div
class="text-content text-center"
v-else
>
<h4>
{{ __('All changes are committed') }}
</h4>
<p v-html="lastCommitMsg"></p>
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -36,7 +36,7 @@ export default { ...@@ -36,7 +36,7 @@ export default {
return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`; return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
}, },
iconClass() { iconClass() {
return `multi-file-${this.file.tempFile ? 'additions' : 'modified'} append-right-8`; return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
}, },
}, },
methods: { methods: {
......
<script>
import { mapState } from 'vuex';
export default {
props: {
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState(['lastCommitMsg']),
},
};
</script>
<template>
<div
class="multi-file-commit-panel-success-message"
aria-live="assertive"
>
<div class="svg-content svg-80">
<img
:src="committedStateSvgPath"
alt=""
/>
</div>
<div class="append-right-default prepend-left-default">
<div
class="text-content text-center"
>
<h4>
{{ __('All changes are committed') }}
</h4>
<p v-html="lastCommitMsg"></p>
</div>
</div>
</div>
</template>
...@@ -7,6 +7,7 @@ import LoadingButton from '~/vue_shared/components/loading_button.vue'; ...@@ -7,6 +7,7 @@ import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitFilesList from './commit_sidebar/list.vue'; import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue'; import EmptyState from './commit_sidebar/empty_state.vue';
import CommitMessageField from './commit_sidebar/message_field.vue'; import CommitMessageField from './commit_sidebar/message_field.vue';
import SuccessMessage from './commit_sidebar/success_message.vue';
import * as consts from '../stores/modules/commit/constants'; import * as consts from '../stores/modules/commit/constants';
import Actions from './commit_sidebar/actions.vue'; import Actions from './commit_sidebar/actions.vue';
...@@ -16,6 +17,7 @@ export default { ...@@ -16,6 +17,7 @@ export default {
Icon, Icon,
CommitFilesList, CommitFilesList,
EmptyState, EmptyState,
SuccessMessage,
Actions, Actions,
LoadingButton, LoadingButton,
CommitMessageField, CommitMessageField,
...@@ -34,9 +36,15 @@ export default { ...@@ -34,9 +36,15 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['changedFiles', 'stagedFiles', 'rightPanelCollapsed']), showStageUnstageArea() {
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
},
someUncommitedChanges() {
return !!(this.changedFiles.length || this.stagedFiles.length);
},
...mapState(['changedFiles', 'stagedFiles', 'rightPanelCollapsed', 'lastCommitMsg', 'unusedSeal']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']), ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled', 'branchName']), ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
}, },
methods: { methods: {
...mapActions('commit', [ ...mapActions('commit', [
...@@ -69,7 +77,7 @@ export default { ...@@ -69,7 +77,7 @@ export default {
</template> </template>
</deprecated-modal> </deprecated-modal>
<template <template
v-if="changedFiles.length || stagedFiles.length" v-if="showStageUnstageArea"
> >
<commit-files-list <commit-files-list
icon-name="unstaged" icon-name="unstaged"
...@@ -89,11 +97,23 @@ export default { ...@@ -89,11 +97,23 @@ export default {
:show-toggle="false" :show-toggle="false"
:staged-list="true" :staged-list="true"
/> />
</template>
<empty-state
v-if="unusedSeal"
:no-changes-state-svg-path="noChangesStateSvgPath"
/>
<div
class="multi-file-commit-panel-bottom"
>
<form <form
class="form-horizontal multi-file-commit-form" class="form-horizontal multi-file-commit-form"
@submit.prevent.stop="commitChanges" @submit.prevent.stop="commitChanges"
v-if="!rightPanelCollapsed" v-if="!rightPanelCollapsed"
> >
<success-message
v-if="lastCommitMsg && !someUncommitedChanges"
:committed-state-svg-path="committedStateSvgPath"
/>
<commit-message-field <commit-message-field
:text="commitMessage" :text="commitMessage"
@input="updateCommitMessage" @input="updateCommitMessage"
...@@ -117,11 +137,6 @@ export default { ...@@ -117,11 +137,6 @@ export default {
</button> </button>
</div> </div>
</form> </form>
</template> </div>
<empty-state
v-else
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
/>
</div> </div>
</template> </template>
<script> <script>
import { mapActions } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import { n__, __, sprintf } from '~/locale';
import fileIcon from '~/vue_shared/components/file_icon.vue'; import tooltip from '~/vue_shared/directives/tooltip';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import router from '../ide_router'; import router from '../ide_router';
import newDropdown from './new_dropdown/index.vue'; import NewDropdown from './new_dropdown/index.vue';
import fileStatusIcon from './repo_file_status_icon.vue'; import FileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue'; import ChangedFileIcon from './changed_file_icon.vue';
import mrFileIcon from './mr_file_icon.vue'; import MrFileIcon from './mr_file_icon.vue';
export default { export default {
name: 'RepoFile', name: 'RepoFile',
directives: {
tooltip,
},
components: { components: {
skeletonLoadingContainer, SkeletonLoadingContainer,
newDropdown, NewDropdown,
fileStatusIcon, FileStatusIcon,
fileIcon, FileIcon,
changedFileIcon, ChangedFileIcon,
mrFileIcon, MrFileIcon,
Icon,
}, },
props: { props: {
file: { file: {
...@@ -29,6 +36,34 @@ export default { ...@@ -29,6 +36,34 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters([
'getChangesInFolder',
'getUnstagedFilesCountForPath',
'getStagedFilesCountForPath',
]),
folderUnstagedCount() {
return this.getUnstagedFilesCountForPath(this.file.path);
},
folderStagedCount() {
return this.getStagedFilesCountForPath(this.file.path);
},
changesCount() {
return this.getChangesInFolder(this.file.path);
},
folderChangesTooltip() {
if (this.changesCount === 0) return undefined;
if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) {
return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount);
} else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) {
return n__('%d staged change', '%d staged changes', this.folderStagedCount);
}
return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
unstaged: this.folderUnstagedCount,
staged: this.folderStagedCount,
});
},
isTree() { isTree() {
return this.file.type === 'tree'; return this.file.type === 'tree';
}, },
...@@ -48,10 +83,19 @@ export default { ...@@ -48,10 +83,19 @@ export default {
'is-open': this.file.opened, 'is-open': this.file.opened,
}; };
}, },
showTreeChangesCount() {
return this.isTree && this.changesCount > 0 && !this.file.opened;
},
showChangedFileIcon() {
return this.file.changed || this.file.tempFile || this.file.staged;
},
}, },
updated() { updated() {
if (this.file.type === 'blob' && this.file.active) { if (this.file.type === 'blob' && this.file.active) {
this.$el.scrollIntoView(); this.$el.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
} }
}, },
methods: { methods: {
...@@ -101,8 +145,23 @@ export default { ...@@ -101,8 +145,23 @@ export default {
<mr-file-icon <mr-file-icon
v-if="file.mrChange" v-if="file.mrChange"
/> />
<span
v-if="showTreeChangesCount"
class="ide-tree-changes"
>
{{ changesCount }}
<icon
v-tooltip
:title="folderChangesTooltip"
data-container="body"
data-placement="right"
name="file-modified"
:size="12"
css-classes="prepend-left-5 multi-file-modified"
/>
</span>
<changed-file-icon <changed-file-icon
v-if="file.changed || file.tempFile || file.staged" v-else-if="showChangedFileIcon"
:file="file" :file="file"
:show-tooltip="true" :show-tooltip="true"
:show-staged-icon="true" :show-staged-icon="true"
......
...@@ -149,6 +149,12 @@ export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, temp ...@@ -149,6 +149,12 @@ export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, temp
export const toggleFileFinder = ({ commit }, fileFindVisible) => export const toggleFileFinder = ({ commit }, fileFindVisible) =>
commit(types.TOGGLE_FILE_FINDER, fileFindVisible); commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
export const burstUnusedSeal = ({ state, commit }) => {
if (state.unusedSeal) {
commit(types.BURST_UNUSED_SEAL);
}
};
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
......
...@@ -117,7 +117,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = ...@@ -117,7 +117,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
}); });
}; };
export const changeFileContent = ({ state, commit }, { path, content }) => { export const changeFileContent = ({ commit, dispatch, state }, { path, content }) => {
const file = state.entries[path]; const file = state.entries[path];
commit(types.UPDATE_FILE_CONTENT, { path, content }); commit(types.UPDATE_FILE_CONTENT, { path, content });
...@@ -128,6 +128,8 @@ export const changeFileContent = ({ state, commit }, { path, content }) => { ...@@ -128,6 +128,8 @@ export const changeFileContent = ({ state, commit }, { path, content }) => {
} else if (!file.changed && indexOfChangedFile !== -1) { } else if (!file.changed && indexOfChangedFile !== -1) {
commit(types.REMOVE_FILE_FROM_CHANGED, path); commit(types.REMOVE_FILE_FROM_CHANGED, path);
} }
dispatch('burstUnusedSeal', {}, { root: true });
}; };
export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => { export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => {
......
import { __ } from '~/locale'; import { __ } from '~/locale';
import { getChangesCountForFiles, filePathMatches } from './utils';
export const activeFile = state => state.openFiles.find(file => file.active) || null; export const activeFile = state => state.openFiles.find(file => file.active) || null;
...@@ -55,7 +56,23 @@ export const allBlobs = state => ...@@ -55,7 +56,23 @@ export const allBlobs = state =>
}, []) }, [])
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt); .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path);
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path); export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
export const getChangesInFolder = state => path => {
const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length;
const stagedFilesCount = state.stagedFiles.filter(
f => filePathMatches(f, path) && !getChangedFile(state)(f.path),
).length;
return changedFilesCount + stagedFilesCount;
};
export const getUnstagedFilesCountForPath = state => path =>
getChangesCountForFiles(state.changedFiles, path);
export const getStagedFilesCountForPath = state => path =>
getChangesCountForFiles(state.stagedFiles, path);
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -182,6 +182,10 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) = ...@@ -182,6 +182,10 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
} }
commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true }); commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
setTimeout(() => {
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000);
}) })
.then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)); .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH));
}) })
......
...@@ -61,3 +61,4 @@ export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB'; ...@@ -61,3 +61,4 @@ export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG'; export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
...@@ -128,6 +128,11 @@ export default { ...@@ -128,6 +128,11 @@ export default {
}), }),
}); });
}, },
[types.BURST_UNUSED_SEAL](state) {
Object.assign(state, {
unusedSeal: false,
});
},
...projectMutations, ...projectMutations,
...mergeRequestMutation, ...mergeRequestMutation,
...fileMutations, ...fileMutations,
......
...@@ -18,5 +18,6 @@ export default () => ({ ...@@ -18,5 +18,6 @@ export default () => ({
entries: {}, entries: {},
viewer: 'editor', viewer: 'editor',
delayViewerUpdated: false, delayViewerUpdated: false,
unusedSeal: true,
fileFindVisible: false, fileFindVisible: false,
}); });
...@@ -33,7 +33,6 @@ export const dataStructure = () => ({ ...@@ -33,7 +33,6 @@ export const dataStructure = () => ({
raw: '', raw: '',
content: '', content: '',
parentTreeUrl: '', parentTreeUrl: '',
parentPath: '',
renderError: false, renderError: false,
base64: false, base64: false,
editorRow: 1, editorRow: 1,
...@@ -43,6 +42,7 @@ export const dataStructure = () => ({ ...@@ -43,6 +42,7 @@ export const dataStructure = () => ({
viewMode: 'edit', viewMode: 'edit',
previewMode: null, previewMode: null,
size: 0, size: 0,
parentPath: null,
lastOpenedAt: 0, lastOpenedAt: 0,
}); });
...@@ -83,7 +83,6 @@ export const decorateData = entity => { ...@@ -83,7 +83,6 @@ export const decorateData = entity => {
opened, opened,
active, active,
parentTreeUrl, parentTreeUrl,
parentPath,
changed, changed,
renderError, renderError,
content, content,
...@@ -91,6 +90,7 @@ export const decorateData = entity => { ...@@ -91,6 +90,7 @@ export const decorateData = entity => {
previewMode, previewMode,
file_lock, file_lock,
html, html,
parentPath,
}; };
}; };
...@@ -137,3 +137,9 @@ export const sortTree = sortedTree => ...@@ -137,3 +137,9 @@ export const sortTree = sortedTree =>
}), }),
) )
.sort(sortTreesByTypeAndName); .sort(sortTreesByTypeAndName);
export const filePathMatches = (f, path) =>
f.path.replace(new RegExp(`${f.name}$`), '').indexOf(`${path}/`) === 0;
export const getChangesCountForFiles = (files, path) =>
files.filter(f => filePathMatches(f, path)).length;
...@@ -30,7 +30,7 @@ export default class IssuableForm { ...@@ -30,7 +30,7 @@ export default class IssuableForm {
} }
this.initAutosave(); this.initAutosave();
this.form.on('submit', this.handleSubmit); this.form.on('submit:success', this.handleSubmit);
this.form.on('click', '.btn-cancel', this.resetAutosave); this.form.on('click', '.btn-cancel', this.resetAutosave);
this.initWip(); this.initWip();
......
...@@ -74,7 +74,11 @@ export function capitalizeFirstCharacter(text) { ...@@ -74,7 +74,11 @@ export function capitalizeFirstCharacter(text) {
* @param {*} replace * @param {*} replace
* @returns {String} * @returns {String}
*/ */
export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace); export const stripHtml = (string, replace = '') => {
if (!string) return string;
return string.replace(/<[^>]*>/g, replace);
};
/** /**
* Converts snake_case string to camelCase * Converts snake_case string to camelCase
......
/* eslint-disable no-new */
import $ from 'jquery'; import $ from 'jquery';
import flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
...@@ -62,7 +60,7 @@ export default class MiniPipelineGraph { ...@@ -62,7 +60,7 @@ export default class MiniPipelineGraph {
*/ */
renderBuildsList(stageContainer, data) { renderBuildsList(stageContainer, data) {
const dropdownContainer = stageContainer.parentElement.querySelector( const dropdownContainer = stageContainer.parentElement.querySelector(
`${this.dropdownListSelector} .js-builds-dropdown-list`, `${this.dropdownListSelector} .js-builds-dropdown-list ul`,
); );
dropdownContainer.innerHTML = data; dropdownContainer.innerHTML = data;
......
...@@ -99,10 +99,6 @@ export default { ...@@ -99,10 +99,6 @@ export default {
'js-note-target-reopen': !this.isOpen, 'js-note-target-reopen': !this.isOpen,
}; };
}, },
supportQuickActions() {
// Disable quick actions support for Epics
return this.noteableType !== constants.EPIC_NOTEABLE_TYPE;
},
markdownDocsPath() { markdownDocsPath() {
return this.getNotesData.markdownDocsPath; return this.getNotesData.markdownDocsPath;
}, },
...@@ -359,7 +355,7 @@ Please check your network connection and try again.`; ...@@ -359,7 +355,7 @@ Please check your network connection and try again.`;
name="note[note]" name="note[note]"
class="note-textarea js-vue-comment-form class="note-textarea js-vue-comment-form
js-gfm-input js-autosize markdown-area js-vue-textarea" js-gfm-input js-autosize markdown-area js-vue-textarea"
:data-supports-quick-actions="supportQuickActions" data-supports-quick-actions="true"
aria-label="Description" aria-label="Description"
v-model="note" v-model="note"
ref="textarea" ref="textarea"
......
import initCompareAutocomplete from '~/compare_autocomplete'; import initCompareAutocomplete from '~/compare_autocomplete';
document.addEventListener('DOMContentLoaded', initCompareAutocomplete); document.addEventListener('DOMContentLoaded', () => initCompareAutocomplete());
import Diff from '~/diff'; import Diff from '~/diff';
import initChangesDropdown from '~/init_changes_dropdown'; import initChangesDropdown from '~/init_changes_dropdown';
import GpgBadges from '~/gpg_badges';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new new Diff(); // eslint-disable-line no-new
const paddingTop = 16; const paddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop); initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
GpgBadges.fetch();
}); });
import $ from 'jquery';
import { localTimeAgo } from '~/lib/utils/datetime_utility';
import axios from '~/lib/utils/axios_utils';
import initCompareAutocomplete from '~/compare_autocomplete';
import initTargetProjectDropdown from './target_project_dropdown';
const updateCommitList = (url, $loadingIndicator, $commitList, params) => {
$loadingIndicator.show();
$commitList.empty();
return axios
.get(url, {
params,
})
.then(({ data }) => {
$loadingIndicator.hide();
$commitList.html(data);
localTimeAgo($('.js-timeago', $commitList));
});
};
export default mrNewCompareNode => {
const { sourceBranchUrl, targetBranchUrl } = mrNewCompareNode.dataset;
initTargetProjectDropdown();
const updateSourceBranchCommitList = () =>
updateCommitList(
sourceBranchUrl,
$(mrNewCompareNode).find('.js-source-loading'),
$(mrNewCompareNode).find('.mr_source_commit'),
{
ref: $(mrNewCompareNode)
.find("input[name='merge_request[source_branch]']")
.val(),
},
);
const updateTargetBranchCommitList = () =>
updateCommitList(
targetBranchUrl,
$(mrNewCompareNode).find('.js-target-loading'),
$(mrNewCompareNode).find('.mr_target_commit'),
{
target_project_id: $(mrNewCompareNode)
.find("input[name='merge_request[target_project_id]']")
.val(),
ref: $(mrNewCompareNode)
.find("input[name='merge_request[target_branch]']")
.val(),
},
);
initCompareAutocomplete('branches', $dropdown => {
if ($dropdown.is('.js-target-branch')) {
updateTargetBranchCommitList();
} else if ($dropdown.is('.js-source-branch')) {
updateSourceBranchCommitList();
}
});
updateSourceBranchCommitList();
updateTargetBranchCommitList();
};
import Compare from '~/compare';
import MergeRequest from '~/merge_request'; import MergeRequest from '~/merge_request';
import initPipelines from '~/commit/pipelines/pipelines_bundle'; import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initCompare from './compare';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
if (mrNewCompareNode) { if (mrNewCompareNode) {
new Compare({ // eslint-disable-line no-new initCompare(mrNewCompareNode);
targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl,
sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl,
targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl,
});
} else { } else {
const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit'); const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
new MergeRequest({ // eslint-disable-line no-new // eslint-disable-next-line no-new
new MergeRequest({
action: mrNewSubmitNode.dataset.mrSubmitAction, action: mrNewSubmitNode.dataset.mrSubmitAction,
}); });
initPipelines(); initPipelines();
......
import $ from 'jquery';
export default () => {
const $targetProjectDropdown = $('.js-target-project');
$targetProjectDropdown.glDropdown({
selectable: true,
fieldName: $targetProjectDropdown.data('fieldName'),
filterable: true,
id(obj, $el) {
return $el.data('id');
},
toggleLabel(obj, $el) {
return $el.text().trim();
},
clicked({ $el }) {
$('.mr_target_commit').empty();
const $targetBranchDropdown = $('.js-target-branch');
$targetBranchDropdown.data('refsUrl', $el.data('refsUrl'));
$targetBranchDropdown.data('glDropdown').clearMenu();
},
});
};
import $ from 'jquery'; import $ from 'jquery';
import NewBranchForm from '~/new_branch_form'; import NewBranchForm from '~/new_branch_form';
import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new
setupNativeFormVariableList({
container: $('.js-ci-variable-list-section'),
formField: 'variables_attributes',
});
}); });
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
methods: { methods: {
onClickAction() { onClickAction() {
$(this.$el).tooltip('hide'); $(this.$el).tooltip('hide');
eventHub.$emit('graphAction', this.link); eventHub.$emit('postAction', this.link);
this.linkRequested = this.link; this.linkRequested = this.link;
this.isDisabled = true; this.isDisabled = true;
}, },
......
...@@ -87,7 +87,8 @@ export default { ...@@ -87,7 +87,8 @@ export default {
data-toggle="dropdown" data-toggle="dropdown"
data-container="body" data-container="body"
class="dropdown-menu-toggle build-content" class="dropdown-menu-toggle build-content"
:title="tooltipText"> :title="tooltipText"
>
<job-name-component <job-name-component
:name="job.name" :name="job.name"
...@@ -104,7 +105,8 @@ export default { ...@@ -104,7 +105,8 @@ export default {
<ul> <ul>
<li <li
v-for="(item, i) in job.jobs" v-for="(item, i) in job.jobs"
:key="i"> :key="i"
>
<job-component <job-component
:job="item" :job="item"
css-class-job-name="mini-pipeline-graph-dropdown-item" css-class-job-name="mini-pipeline-graph-dropdown-item"
......
...@@ -108,7 +108,7 @@ export default { ...@@ -108,7 +108,7 @@ export default {
<div <div
v-else v-else
v-tooltip v-tooltip
class="js-job-component-tooltip" class="js-job-component-tooltip non-details-job-component"
:title="tooltipText" :title="tooltipText"
:class="cssClassJobName" :class="cssClassJobName"
data-html="true" data-html="true"
......
<script> <script>
/**
/** * Renders each stage of the pipeline mini graph.
* Renders each stage of the pipeline mini graph. *
* * Given the provided endpoint will make a request to
* Given the provided endpoint will make a request to * fetch the dropdown data when the stage is clicked.
* fetch the dropdown data when the stage is clicked. *
* * Request is made inside this component to make it reusable between:
* Request is made inside this component to make it reusable between: * 1. Pipelines main table
* 1. Pipelines main table * 2. Pipelines table in commit and Merge request views
* 2. Pipelines table in commit and Merge request views * 3. Merge request widget
* 3. Merge request widget * 4. Commit widget
* 4. Commit widget */
*/
import $ from 'jquery';
import $ from 'jquery'; import { __ } from '../../locale';
import Flash from '../../flash'; import Flash from '../../flash';
import axios from '../../lib/utils/axios_utils'; import axios from '../../lib/utils/axios_utils';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import Icon from '../../vue_shared/components/icon.vue'; import Icon from '../../vue_shared/components/icon.vue';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import JobComponent from './graph/job_component.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: { export default {
LoadingIcon, components: {
Icon, LoadingIcon,
Icon,
JobComponent,
},
directives: {
tooltip,
},
props: {
stage: {
type: Object,
required: true,
}, },
directives: { updateDropdown: {
tooltip, type: Boolean,
required: false,
default: false,
}, },
},
props: {
stage: { data() {
type: Object, return {
required: true, isLoading: false,
}, dropdownContent: '',
};
updateDropdown: { },
type: Boolean,
required: false, computed: {
default: false, dropdownClass() {
}, return this.dropdownContent.length > 0
? 'js-builds-dropdown-container'
: 'js-builds-dropdown-loading';
}, },
data() { triggerButtonClass() {
return { return `ci-status-icon-${this.stage.status.group}`;
isLoading: false,
dropdownContent: '',
};
}, },
computed: { borderlessIcon() {
dropdownClass() { return `${this.stage.status.icon}_borderless`;
return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading'; },
}, },
triggerButtonClass() { watch: {
return `ci-status-icon-${this.stage.status.group}`; updateDropdown() {
}, if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
this.fetchJobs();
}
},
},
updated() {
if (this.dropdownContent.length > 0) {
this.stopDropdownClickPropagation();
}
},
methods: {
onClickStage() {
if (!this.isDropdownOpen()) {
eventHub.$emit('clickedDropdown');
this.isLoading = true;
this.fetchJobs();
}
},
borderlessIcon() { fetchJobs() {
return `${this.stage.status.icon}_borderless`; axios
}, .get(this.stage.dropdown_path)
.then(({ data }) => {
this.dropdownContent = data.latest_statuses;
this.isLoading = false;
})
.catch(() => {
this.closeDropdown();
this.isLoading = false;
Flash(__('Something went wrong on our end.'));
});
}, },
watch: { /**
updateDropdown() { * When the user right clicks or cmd/ctrl + click in the job name
if (this.updateDropdown && * the dropdown should not be closed and the link should open in another tab,
this.isDropdownOpen() && * so we stop propagation of the click event inside the dropdown.
!this.isLoading) { *
this.fetchJobs(); * Since this component is rendered multiple times per page we need to guarantee we only
} * target the click event of this component.
}, */
stopDropdownClickPropagation() {
$(
'.js-builds-dropdown-list button, .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item',
this.$el,
).on('click', e => {
e.stopPropagation();
});
}, },
updated() { closeDropdown() {
if (this.dropdownContent.length > 0) { if (this.isDropdownOpen()) {
this.stopDropdownClickPropagation(); $(this.$refs.dropdown).dropdown('toggle');
} }
}, },
methods: { isDropdownOpen() {
onClickStage() { return this.$el.classList.contains('open');
if (!this.isDropdownOpen()) {
eventHub.$emit('clickedDropdown');
this.isLoading = true;
this.fetchJobs();
}
},
fetchJobs() {
axios.get(this.stage.dropdown_path)
.then(({ data }) => {
this.dropdownContent = data.html;
this.isLoading = false;
})
.catch(() => {
this.closeDropdown();
this.isLoading = false;
Flash('Something went wrong on our end.');
});
},
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item'))
.on('click', (e) => {
e.stopPropagation();
});
},
closeDropdown() {
if (this.isDropdownOpen()) {
$(this.$refs.dropdown).dropdown('toggle');
}
},
isDropdownOpen() {
return this.$el.classList.contains('open');
},
}, },
}; },
};
</script> </script>
<template> <template>
...@@ -168,7 +173,6 @@ ...@@ -168,7 +173,6 @@
> >
<li <li
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu" class="js-builds-dropdown-list scrollable-menu"
> >
...@@ -176,8 +180,16 @@ ...@@ -176,8 +180,16 @@
<ul <ul
v-else v-else
v-html="dropdownContent"
> >
<li
v-for="job in dropdownContent"
:key="job.id"
>
<job-component
:job="job"
css-class-job-name="mini-pipeline-graph-dropdown-item"
/>
</li>
</ul> </ul>
</li> </li>
</ul> </ul>
......
...@@ -29,10 +29,10 @@ export default () => { ...@@ -29,10 +29,10 @@ export default () => {
}; };
}, },
created() { created() {
eventHub.$on('graphAction', this.postAction); eventHub.$on('postAction', this.postAction);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('graphAction', this.postAction); eventHub.$off('postAction', this.postAction);
}, },
methods: { methods: {
postAction(action) { postAction(action) {
......
...@@ -100,9 +100,10 @@ export default { ...@@ -100,9 +100,10 @@ export default {
fetchSearchedProjects(searchQuery) { fetchSearchedProjects(searchQuery) {
this.searchQuery = searchQuery; this.searchQuery = searchQuery;
this.toggleLoader(true); this.toggleLoader(true);
this.service.getSearchedProjects(this.searchQuery) this.service
.getSearchedProjects(this.searchQuery)
.then(res => res.json()) .then(res => res.json())
.then((results) => { .then(results => {
this.toggleSearchProjectsList(true); this.toggleSearchProjectsList(true);
this.store.setSearchedProjects(results); this.store.setSearchedProjects(results);
}) })
......
...@@ -50,7 +50,7 @@ export default class ProjectsService { ...@@ -50,7 +50,7 @@ export default class ProjectsService {
} else { } else {
// Check if project is already present in frequents list // Check if project is already present in frequents list
// When found, update metadata of it. // When found, update metadata of it.
storedFrequentProjects = JSON.parse(storedRawProjects).map((projectItem) => { storedFrequentProjects = JSON.parse(storedRawProjects).map(projectItem => {
if (projectItem.id === project.id) { if (projectItem.id === project.id) {
matchFound = true; matchFound = true;
const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS; const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS;
...@@ -104,13 +104,17 @@ export default class ProjectsService { ...@@ -104,13 +104,17 @@ export default class ProjectsService {
return []; return [];
} }
if (bp.getBreakpointSize() === 'sm' || if (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'xs') {
bp.getBreakpointSize() === 'xs') {
frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE; frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE;
} }
const frequentProjects = storedFrequentProjects const frequentProjects = storedFrequentProjects.filter(
.filter(project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY); project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY,
);
if (!frequentProjects || frequentProjects.length === 0) {
return [];
}
// Sort all frequent projects in decending order of frequency // Sort all frequent projects in decending order of frequency
// and then by lastAccessedOn with recent most first // and then by lastAccessedOn with recent most first
......
...@@ -70,6 +70,9 @@ ...@@ -70,6 +70,9 @@
toggleMoreParticipants() { toggleMoreParticipants() {
this.isShowingMoreParticipants = !this.isShowingMoreParticipants; this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
}, },
onClickCollapsedIcon() {
this.$emit('toggleSidebar');
},
}, },
}; };
</script> </script>
...@@ -82,6 +85,7 @@ ...@@ -82,6 +85,7 @@
data-container="body" data-container="body"
data-placement="left" data-placement="left"
:title="participantLabel" :title="participantLabel"
@click="onClickCollapsedIcon"
> >
<i <i
class="fa fa-users" class="fa fa-users"
......
<script> <script>
import Store from '../../stores/sidebar_store'; import Store from '../../stores/sidebar_store';
import eventHub from '../../event_hub';
import Flash from '../../../flash'; import Flash from '../../../flash';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue'; import subscriptions from './subscriptions.vue';
...@@ -20,12 +19,6 @@ export default { ...@@ -20,12 +19,6 @@ export default {
store: new Store(), store: new Store(),
}; };
}, },
created() {
eventHub.$on('toggleSubscription', this.onToggleSubscription);
},
beforeDestroy() {
eventHub.$off('toggleSubscription', this.onToggleSubscription);
},
methods: { methods: {
onToggleSubscription() { onToggleSubscription() {
this.mediator.toggleSubscription() this.mediator.toggleSubscription()
...@@ -42,6 +35,7 @@ export default { ...@@ -42,6 +35,7 @@ export default {
<subscriptions <subscriptions
:loading="store.isFetching.subscriptions" :loading="store.isFetching.subscriptions"
:subscribed="store.subscribed" :subscribed="store.subscribed"
@toggleSubscription="onToggleSubscription"
/> />
</div> </div>
</template> </template>
...@@ -47,8 +47,25 @@ ...@@ -47,8 +47,25 @@
}, },
}, },
methods: { methods: {
/**
* We need to emit this event on both component & eventHub
* for 2 dependencies;
*
* 1. eventHub: This component is used in Issue Boards sidebar
* where component template is part of HAML
* and event listeners are tied to app's eventHub.
* 2. Component: This compone is also used in Epics in EE
* where listeners are tied to component event.
*/
toggleSubscription() { toggleSubscription() {
// App's eventHub event emission.
eventHub.$emit('toggleSubscription', this.id); eventHub.$emit('toggleSubscription', this.id);
// Component event emission.
this.$emit('toggleSubscription', this.id);
},
onClickCollapsedIcon() {
this.$emit('toggleSidebar');
}, },
}, },
}; };
...@@ -56,7 +73,10 @@ ...@@ -56,7 +73,10 @@
<template> <template>
<div> <div>
<div class="sidebar-collapsed-icon"> <div
class="sidebar-collapsed-icon"
@click="onClickCollapsedIcon"
>
<span <span
v-tooltip v-tooltip
:title="notificationTooltip" :title="notificationTooltip"
......
export default {
name: 'time-tracking-no-tracking-pane',
template: `
<div class="time-tracking-no-tracking-pane">
<span class="no-value">
{{ __('No estimate or time spent') }}
</span>
</div>
`,
};
<script>
export default {
name: 'TimeTrackingNoTrackingPane',
};
</script>
<template>
<div class="time-tracking-no-tracking-pane">
<span class="no-value">
{{ __('No estimate or time spent') }}
</span>
</div>
</template>
<script>
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
...@@ -10,14 +11,17 @@ import Mediator from '../../sidebar_mediator'; ...@@ -10,14 +11,17 @@ import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
export default { export default {
components: {
IssuableTimeTracker,
},
data() { data() {
return { return {
mediator: new Mediator(), mediator: new Mediator(),
store: new Store(), store: new Store(),
}; };
}, },
components: { mounted() {
IssuableTimeTracker, this.listenForQuickActions();
}, },
methods: { methods: {
listenForQuickActions() { listenForQuickActions() {
...@@ -41,18 +45,17 @@ export default { ...@@ -41,18 +45,17 @@ export default {
} }
}, },
}, },
mounted() {
this.listenForQuickActions();
},
template: `
<div class="block">
<issuable-time-tracker
:time_estimate="store.timeEstimate"
:time_spent="store.totalTimeSpent"
:human_time_estimate="store.humanTimeEstimate"
:human_time_spent="store.humanTotalTimeSpent"
:rootPath="store.rootPath"
/>
</div>
`,
}; };
</script>
<template>
<div class="block">
<issuable-time-tracker
:time_estimate="store.timeEstimate"
:time_spent="store.totalTimeSpent"
:human_time_estimate="store.humanTimeEstimate"
:human_time_spent="store.humanTotalTimeSpent"
:root-path="store.rootPath"
/>
</div>
</template>
<script>
export default { export default {
name: 'time-tracking-spent-only-pane', name: 'TimeTrackingSpentOnlyPane',
props: { props: {
timeSpentHumanReadable: { timeSpentHumanReadable: {
type: String, type: String,
required: true, required: true,
}, },
}, },
template: `
<div class="time-tracking-spend-only-pane">
<span class="bold">Spent:</span>
{{ timeSpentHumanReadable }}
</div>
`,
}; };
</script>
<template>
<div class="time-tracking-spend-only-pane">
<span class="bold">Spent:</span>
{{ timeSpentHumanReadable }}
</div>
</template>
<script> <script>
import TimeTrackingHelpState from './help_state.vue'; import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingCollapsedState from './collapsed_state.vue'; import TimeTrackingCollapsedState from './collapsed_state.vue';
import timeTrackingSpentOnlyPane from './spent_only_pane'; import TimeTrackingSpentOnlyPane from './spent_only_pane.vue';
import timeTrackingNoTrackingPane from './no_tracking_pane'; import TimeTrackingNoTrackingPane from './no_tracking_pane.vue';
import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue'; import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue'; import TimeTrackingComparisonPane from './comparison_pane.vue';
...@@ -13,8 +13,8 @@ export default { ...@@ -13,8 +13,8 @@ export default {
components: { components: {
TimeTrackingCollapsedState, TimeTrackingCollapsedState,
TimeTrackingEstimateOnlyPane, TimeTrackingEstimateOnlyPane,
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane, TimeTrackingSpentOnlyPane,
'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane, TimeTrackingNoTrackingPane,
TimeTrackingComparisonPane, TimeTrackingComparisonPane,
TimeTrackingHelpState, TimeTrackingHelpState,
}, },
......
import $ from 'jquery'; import $ from 'jquery';
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.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue'; import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue'; import SidebarMoveIssue from './lib/sidebar_move_issue';
......
<script>
import $ from 'jquery'; import $ from 'jquery';
import statusIcon from '../mr_widget_status_icon.vue'; import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
export default { export default {
name: 'MRWidgetWIP', name: 'WorkInProgress',
props: { components: {
mr: { type: Object, required: true }, statusIcon,
service: { type: Object, required: true },
}, },
directives: { directives: {
tooltip, tooltip,
}, },
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
data() { data() {
return { return {
isMakingRequest: false, isMakingRequest: false,
}; };
}, },
components: {
statusIcon,
},
methods: { methods: {
removeWIP() { removeWIP() {
this.isMakingRequest = true; this.isMakingRequest = true;
...@@ -36,32 +37,40 @@ export default { ...@@ -36,32 +37,40 @@ export default {
}); });
}, },
}, },
template: `
<div class="mr-widget-body media">
<status-icon status="warning" :show-disabled-button="Boolean(mr.removeWIPPath)" />
<div class="media-body space-children">
<span class="bold">
This is a Work in Progress
<i
v-tooltip
class="fa fa-question-circle"
title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged"
aria-label="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged">
</i>
</span>
<button
v-if="mr.removeWIPPath"
@click="removeWIP"
:disabled="isMakingRequest"
type="button"
class="btn btn-default btn-xs js-remove-wip">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
Resolve WIP status
</button>
</div>
</div>
`,
}; };
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="warning"
:show-disabled-button="Boolean(mr.removeWIPPath)"
/>
<div class="media-body space-children">
<span class="bold">
This is a Work in Progress
<i
v-tooltip
class="fa fa-question-circle"
title="When this merge request is ready,
remove the WIP: prefix from the title to allow it to be merged"
aria-label="When this merge request is ready,
remove the WIP: prefix from the title to allow it to be merged">
</i>
</span>
<button
v-if="mr.removeWIPPath"
@click="removeWIP"
:disabled="isMakingRequest"
type="button"
class="btn btn-default btn-xs js-remove-wip">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
aria-hidden="true">
</i>
Resolve WIP status
</button>
</div>
</div>
</template>
...@@ -21,7 +21,7 @@ export { default as MergedState } from './components/states/mr_widget_merged.vue ...@@ -21,7 +21,7 @@ export { default as MergedState } from './components/states/mr_widget_merged.vue
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue'; export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
export { default as ClosedState } from './components/states/mr_widget_closed.vue'; export { default as ClosedState } from './components/states/mr_widget_closed.vue';
export { default as MergingState } from './components/states/mr_widget_merging.vue'; export { default as MergingState } from './components/states/mr_widget_merging.vue';
export { default as WipState } from './components/states/mr_widget_wip'; export { default as WorkInProgressState } from './components/states/work_in_progress.vue';
export { default as ArchivedState } from './components/states/mr_widget_archived.vue'; export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue'; export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue'; export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue';
......
...@@ -12,7 +12,7 @@ import { ...@@ -12,7 +12,7 @@ import {
ClosedState, ClosedState,
MergingState, MergingState,
RebaseState, RebaseState,
WipState, WorkInProgressState,
ArchivedState, ArchivedState,
ConflictsState, ConflictsState,
NothingToMergeState, NothingToMergeState,
...@@ -220,7 +220,7 @@ export default { ...@@ -220,7 +220,7 @@ export default {
'mr-widget-closed': ClosedState, 'mr-widget-closed': ClosedState,
'mr-widget-merging': MergingState, 'mr-widget-merging': MergingState,
'mr-widget-failed-to-merge': FailedToMerge, 'mr-widget-failed-to-merge': FailedToMerge,
'mr-widget-wip': WipState, 'mr-widget-wip': WorkInProgressState,
'mr-widget-archived': ArchivedState, 'mr-widget-archived': ArchivedState,
'mr-widget-conflicts': ConflictsState, 'mr-widget-conflicts': ConflictsState,
'mr-widget-nothing-to-merge': NothingToMergeState, 'mr-widget-nothing-to-merge': NothingToMergeState,
......
This diff is collapsed.
@import "framework/variables"; @import 'framework/variables';
@import "framework/mixins"; @import 'framework/mixins';
@import 'framework/tw_bootstrap_variables'; @import 'framework/tw_bootstrap_variables';
@import 'framework/tw_bootstrap'; @import 'framework/tw_bootstrap';
@import "framework/layout"; @import 'framework/layout';
@import "framework/animations"; @import 'framework/animations';
@import "framework/vue_transitions"; @import 'framework/vue_transitions';
@import "framework/avatar"; @import 'framework/avatar';
@import "framework/asciidoctor"; @import 'framework/asciidoctor';
@import "framework/banner"; @import 'framework/banner';
@import "framework/blocks"; @import 'framework/blocks';
@import "framework/buttons"; @import 'framework/buttons';
@import "framework/badges"; @import 'framework/badges';
@import "framework/calendar"; @import 'framework/calendar';
@import "framework/callout"; @import 'framework/callout';
@import "framework/common"; @import 'framework/common';
@import "framework/dropdowns"; @import 'framework/dropdowns';
@import "framework/files"; @import 'framework/files';
@import "framework/filters"; @import 'framework/filters';
@import "framework/flash"; @import 'framework/flash';
@import "framework/forms"; @import 'framework/forms';
@import "framework/gfm"; @import 'framework/gfm';
@import "framework/gitlab_theme"; @import 'framework/gitlab_theme';
@import "framework/header"; @import 'framework/header';
@import "framework/highlight"; @import 'framework/highlight';
@import "framework/issue_box"; @import 'framework/issue_box';
@import "framework/jquery"; @import 'framework/jquery';
@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/media_object';
@import "framework/mobile"; @import 'framework/mobile';
@import "framework/modal"; @import 'framework/modal';
@import "framework/pagination"; @import 'framework/pagination';
@import "framework/panels"; @import 'framework/panels';
@import "framework/popup"; @import 'framework/popup';
@import "framework/secondary_navigation_elements"; @import 'framework/secondary_navigation_elements';
@import "framework/selects"; @import 'framework/selects';
@import "framework/sidebar"; @import 'framework/sidebar';
@import "framework/contextual_sidebar"; @import 'framework/contextual_sidebar';
@import "framework/tables"; @import 'framework/tables';
@import "framework/notes"; @import 'framework/notes';
@import "framework/tabs"; @import 'framework/tabs';
@import "framework/timeline"; @import 'framework/timeline';
@import "framework/tooltips"; @import 'framework/tooltips';
@import "framework/toggle"; @import 'framework/toggle';
@import "framework/typography"; @import 'framework/typography';
@import "framework/zen"; @import 'framework/zen';
@import "framework/blank"; @import 'framework/blank';
@import "framework/wells"; @import 'framework/wells';
@import "framework/page_header"; @import 'framework/page_header';
@import "framework/awards"; @import 'framework/awards';
@import "framework/images"; @import 'framework/images';
@import "framework/broadcast_messages"; @import 'framework/broadcast_messages';
@import "framework/emojis"; @import 'framework/emojis';
@import "framework/emoji_sprites"; @import 'framework/icons';
@import "framework/icons"; @import 'framework/snippets';
@import "framework/snippets"; @import 'framework/memory_graph';
@import "framework/memory_graph"; @import 'framework/responsive_tables';
@import "framework/responsive_tables"; @import 'framework/stacked_progress_bar';
@import "framework/stacked_progress_bar"; @import 'framework/ci_variable_list';
@import "framework/ci_variable_list"; @import 'framework/feature_highlight';
@import "framework/feature_highlight"; @import 'framework/terms';
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
} }
&.middle-block { &.middle-block {
margin-top: 0; margin-top: $gl-padding-24;
margin-bottom: 0; margin-bottom: 0;
} }
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
} }
&.footer-block { &.footer-block {
margin-top: 0; margin-top: $gl-padding-24;
border-bottom: 0; border-bottom: 0;
margin-bottom: -$gl-padding; margin-bottom: -$gl-padding;
} }
......
...@@ -452,6 +452,7 @@ img.emoji { ...@@ -452,6 +452,7 @@ img.emoji {
/** COMMON CLASSES **/ /** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; } .prepend-top-0 { margin-top: 0; }
.prepend-top-2 { margin-top: 2px; }
.prepend-top-5 { margin-top: 5px; } .prepend-top-5 { margin-top: 5px; }
.prepend-top-8 { margin-top: $grid-size; } .prepend-top-8 { margin-top: $grid-size; }
.prepend-top-10 { margin-top: 10px; } .prepend-top-10 { margin-top: 10px; }
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -39,35 +39,10 @@ ...@@ -39,35 +39,10 @@
svg { svg {
fill: currentColor; fill: currentColor;
&.s8 { $svg-sizes: 8 12 16 18 24 32 48 72;
@include svg-size(8px); @each $svg-size in $svg-sizes {
} &.s#{$svg-size} {
@include svg-size(#{$svg-size}px);
&.s12 { }
@include svg-size(12px);
}
&.s16 {
@include svg-size(16px);
}
&.s18 {
@include svg-size(18px);
}
&.s24 {
@include svg-size(24px);
}
&.s32 {
@include svg-size(32px);
}
&.s48 {
@include svg-size(48px);
}
&.s72 {
@include svg-size(72px);
} }
} }
...@@ -107,6 +107,16 @@ ...@@ -107,6 +107,16 @@
padding-top: 10px; padding-top: 10px;
} }
.referenced-commands {
background: $blue-50;
padding: $gl-padding-8 $gl-padding;
border-radius: $border-radius-default;
p {
margin: 0;
}
}
.md-preview-holder { .md-preview-holder {
min-height: 167px; min-height: 167px;
padding: 10px 0; padding: 10px 0;
......
.terms {
.with-performance-bar & {
margin-top: 0;
}
.alert-wrapper {
min-height: $header-height + $gl-padding;
}
.content {
padding-top: $gl-padding;
}
.panel {
.panel-heading {
display: -webkit-flex;
display: flex;
align-items: center;
justify-content: space-between;
.title {
display: flex;
align-items: center;
.logo-text {
width: 55px;
height: 24px;
display: flex;
flex-direction: column;
justify-content: center;
}
}
.navbar-collapse {
padding-right: 0;
}
.nav li a {
color: $theme-gray-700;
}
}
.panel-content {
padding: $gl-padding;
*:first-child {
margin-top: 0;
}
*:last-child {
margin-bottom: 0;
}
}
.footer-block {
margin: 0;
}
}
}
...@@ -212,6 +212,7 @@ $tooltip-font-size: 12px; ...@@ -212,6 +212,7 @@ $tooltip-font-size: 12px;
/* /*
* Padding * Padding
*/ */
$gl-padding-24: 24px;
$gl-padding: 16px; $gl-padding: 16px;
$gl-padding-8: 8px; $gl-padding-8: 8px;
$gl-padding-4: 4px; $gl-padding-4: 4px;
...@@ -689,6 +690,8 @@ $stage-hover-bg: $gray-darker; ...@@ -689,6 +690,8 @@ $stage-hover-bg: $gray-darker;
$ci-action-icon-size: 22px; $ci-action-icon-size: 22px;
$pipeline-dropdown-line-height: 20px; $pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px; $pipeline-dropdown-status-icon-size: 18px;
$ci-action-dropdown-button-size: 24px;
$ci-action-dropdown-svg-size: 12px;
/* /*
CI variable lists CI variable lists
......
...@@ -317,6 +317,7 @@ ...@@ -317,6 +317,7 @@
a { a {
color: $gl-text-color; color: $gl-text-color;
word-wrap: break-word; word-wrap: break-word;
word-break: break-word;
margin-right: 2px; margin-right: 2px;
} }
} }
...@@ -462,6 +463,7 @@ ...@@ -462,6 +463,7 @@
.issuable-header-text { .issuable-header-text {
padding-right: 35px; padding-right: 35px;
word-break: break-word;
> strong { > strong {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
......
...@@ -44,6 +44,12 @@ ...@@ -44,6 +44,12 @@
} }
} }
.note-text {
table {
font-family: $font-family-sans-serif;
}
}
table { table {
width: 100%; width: 100%;
font-family: $monospace_font; font-family: $monospace_font;
......
...@@ -156,10 +156,6 @@ ...@@ -156,10 +156,6 @@
.dropdown-menu { .dropdown-menu {
z-index: 300; z-index: 300;
} }
.ci-action-icon-wrapper {
line-height: 16px;
}
} }
.mini-pipeline-graph-dropdown-toggle { .mini-pipeline-graph-dropdown-toggle {
......
...@@ -22,7 +22,6 @@ ...@@ -22,7 +22,6 @@
} }
.ci-table { .ci-table {
.label { .label {
margin-bottom: 3px; margin-bottom: 3px;
} }
...@@ -123,7 +122,6 @@ ...@@ -123,7 +122,6 @@
} }
.branch-commit { .branch-commit {
.ref-name { .ref-name {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
max-width: 100px; max-width: 100px;
...@@ -481,43 +479,6 @@ ...@@ -481,43 +479,6 @@
@extend .build-content:hover; @extend .build-content:hover;
} }
.ci-action-icon-container {
position: absolute;
right: 5px;
top: 5px;
// Action Icons in big pipeline-graph nodes
&.ci-action-icon-wrapper {
height: 30px;
width: 30px;
background: $white-light;
border: 1px solid $border-color;
border-radius: 100%;
display: block;
&:hover {
background-color: $stage-hover-bg;
border: 1px solid $dropdown-toggle-active-border-color;
svg {
fill: $gl-text-color;
}
}
svg {
fill: $gl-text-color-secondary;
position: relative;
top: -1px;
}
&.play {
svg {
left: 2px;
}
}
}
}
.ci-status-icon svg { .ci-status-icon svg {
height: 20px; height: 20px;
width: 20px; width: 20px;
...@@ -548,7 +509,6 @@ ...@@ -548,7 +509,6 @@
border: 1px solid $dropdown-toggle-active-border-color; border: 1px solid $dropdown-toggle-active-border-color;
} }
// Connect first build in each stage with right horizontal line // Connect first build in each stage with right horizontal line
&:first-child { &:first-child {
&::after { &::after {
...@@ -602,6 +562,43 @@ ...@@ -602,6 +562,43 @@
} }
} }
} }
.ci-action-icon-container {
position: absolute;
right: 5px;
top: 5px;
// Action Icons in big pipeline-graph nodes
&.ci-action-icon-wrapper {
height: 30px;
width: 30px;
background: $white-light;
border: 1px solid $border-color;
border-radius: 100%;
display: block;
&:hover {
background-color: $stage-hover-bg;
border: 1px solid $dropdown-toggle-active-border-color;
svg {
fill: $gl-text-color;
}
}
svg {
fill: $gl-text-color-secondary;
position: relative;
top: -1px;
}
&.play {
svg {
left: 2px;
}
}
}
}
} }
// Triggers the dropdown in the big pipeline graph // Triggers the dropdown in the big pipeline graph
...@@ -710,93 +707,77 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -710,93 +707,77 @@ button.mini-pipeline-graph-dropdown-toggle {
} }
} }
// dropdown content for big and mini pipeline /**
Action icons inside dropdowns:
- mini graph in pipelines table
- dropdown in big graph
- mini graph in MR widget pipeline
- mini graph in Commit widget pipeline
*/
.big-pipeline-graph-dropdown-menu, .big-pipeline-graph-dropdown-menu,
.mini-pipeline-graph-dropdown-menu { .mini-pipeline-graph-dropdown-menu {
width: 240px; width: 240px;
max-width: 240px; max-width: 240px;
.scrollable-menu { // override dropdown.scss
&.dropdown-menu li button,
&.dropdown-menu li a.ci-action-icon-container {
padding: 0; padding: 0;
max-height: 245px; text-align: center;
overflow: auto;
} }
li { .ci-action-icon-container {
position: relative; position: absolute;
right: 8px;
top: 8px;
// ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered &.ci-action-icon-wrapper {
&:hover > .mini-pipeline-graph-dropdown-item, height: $ci-action-dropdown-button-size;
&:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item { width: $ci-action-dropdown-button-size;
@extend .mini-pipeline-graph-dropdown-item:hover;
}
// Action icon on the right background: $white-light;
a.ci-action-icon-wrapper {
border-radius: 50%;
border: 1px solid $border-color; border: 1px solid $border-color;
width: $ci-action-icon-size; border-radius: 50%;
height: $ci-action-icon-size; display: block;
padding: 2px 0 0 5px;
font-size: 12px;
background-color: $white-light;
position: absolute;
top: 50%;
right: $gl-padding;
margin-top: -#{$ci-action-icon-size / 2};
&:hover, &:hover {
&:focus {
background-color: $stage-hover-bg; background-color: $stage-hover-bg;
border: 1px solid $dropdown-toggle-active-border-color; border: 1px solid $dropdown-toggle-active-border-color;
svg {
fill: $gl-text-color;
}
} }
svg { svg {
width: $ci-action-dropdown-svg-size;
height: $ci-action-dropdown-svg-size;
fill: $gl-text-color-secondary; fill: $gl-text-color-secondary;
width: #{$ci-action-icon-size - 6};
height: #{$ci-action-icon-size - 6};
left: -3px;
position: relative; position: relative;
top: -1px; top: 0;
vertical-align: initial;
&.icon-action-stop,
&.icon-action-cancel {
width: 12px;
height: 12px;
top: 1px;
left: -1px;
}
&.icon-action-play {
width: 11px;
height: 11px;
top: 1px;
left: 1px;
}
&.icon-action-retry {
width: 16px;
height: 16px;
top: 0;
left: -3px;
}
} }
}
}
&:hover svg, // SVGs in the commit widget and mr widget
&:focus svg { a.ci-action-icon-container.ci-action-icon-wrapper svg {
fill: $gl-text-color; top: 2px;
} }
&.icon-action-retry, .scrollable-menu {
&.icon-action-play { padding: 0;
svg { max-height: 245px;
width: #{$ci-action-icon-size - 6}; overflow: auto;
height: #{$ci-action-icon-size - 6}; }
left: 8px;
}
}
li {
position: relative;
// ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
&:hover > .mini-pipeline-graph-dropdown-item,
&:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item {
@extend .mini-pipeline-graph-dropdown-item:hover;
} }
// link to the build // link to the build
...@@ -808,6 +789,11 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -808,6 +789,11 @@ button.mini-pipeline-graph-dropdown-toggle {
line-height: $line-height-base; line-height: $line-height-base;
white-space: nowrap; white-space: nowrap;
// Match dropdown.scss for all `a` tags
&.non-details-job-component {
padding: 8px 16px;
}
.ci-job-name-component { .ci-job-name-component {
align-items: center; align-items: center;
display: flex; display: flex;
...@@ -939,7 +925,7 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -939,7 +925,7 @@ button.mini-pipeline-graph-dropdown-toggle {
&.dropdown-menu { &.dropdown-menu {
transform: translate(-80%, 0); transform: translate(-80%, 0);
@media(min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
transform: translate(-50%, 0); transform: translate(-50%, 0);
right: auto; right: auto;
left: 50%; left: 50%;
......
...@@ -440,6 +440,7 @@ ...@@ -440,6 +440,7 @@
padding-right: 3px; padding-right: 3px;
.projects-sidebar { .projects-sidebar {
min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
...@@ -549,6 +550,7 @@ ...@@ -549,6 +550,7 @@
margin-bottom: 0; margin-bottom: 0;
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding 0; padding: $gl-btn-padding 0;
min-height: 56px;
} }
.multi-file-commit-panel-header-title { .multi-file-commit-panel-header-title {
...@@ -602,14 +604,14 @@ ...@@ -602,14 +604,14 @@
} }
} }
.multi-file-additions, .multi-file-addition,
.multi-file-additions-solid { .multi-file-addition-solid {
fill: $green-500; color: $green-500;
} }
.multi-file-modified, .multi-file-modified,
.multi-file-modified-solid { .multi-file-modified-solid {
fill: $orange-500; color: $orange-500;
} }
.multi-file-commit-list-collapsed { .multi-file-commit-list-collapsed {
...@@ -673,6 +675,24 @@ ...@@ -673,6 +675,24 @@
} }
} }
.multi-file-commit-panel-bottom {
position: relative;
.multi-file-commit-panel-success-message {
position: absolute;
top: 1px;
left: 3px;
bottom: 0;
right: 0;
z-index: 10;
background: $gray-light;
overflow: auto;
display: flex;
flex-direction: column;
justify-content: center;
}
}
.dirty-diff { .dirty-diff {
// !important need to override monaco inline style // !important need to override monaco inline style
width: 4px !important; width: 4px !important;
...@@ -972,6 +992,12 @@ ...@@ -972,6 +992,12 @@
resize: none; resize: none;
} }
.ide-tree-changes {
display: flex;
align-items: center;
font-size: 12px;
}
.ide-new-modal-label { .ide-new-modal-label {
line-height: 34px; line-height: 34px;
} }
This diff is collapsed.
...@@ -13,12 +13,14 @@ class ApplicationController < ActionController::Base ...@@ -13,12 +13,14 @@ class ApplicationController < ActionController::Base
before_action :authenticate_sessionless_user! before_action :authenticate_sessionless_user!
before_action :authenticate_user! before_action :authenticate_user!
before_action :enforce_terms!, if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms },
unless: :peek_request?
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
before_action :check_password_expiration before_action :check_password_expiration
before_action :ldap_security_check before_action :ldap_security_check
before_action :sentry_context before_action :sentry_context
before_action :default_headers before_action :default_headers
before_action :add_gon_variables, unless: -> { request.path.start_with?('/-/peek') } before_action :add_gon_variables, unless: :peek_request?
before_action :configure_permitted_parameters, if: :devise_controller? before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller? before_action :require_email, unless: :devise_controller?
...@@ -110,7 +112,8 @@ class ApplicationController < ActionController::Base ...@@ -110,7 +112,8 @@ class ApplicationController < ActionController::Base
def log_exception(exception) def log_exception(exception)
Raven.capture_exception(exception) if sentry_enabled? Raven.capture_exception(exception) if sentry_enabled?
application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace backtrace_cleaner = Gitlab.rails5? ? env["action_dispatch.backtrace_cleaner"] : env
application_trace = ActionDispatch::ExceptionWrapper.new(backtrace_cleaner, exception).application_trace
application_trace.map! { |t| " #{t}\n" } application_trace.map! { |t| " #{t}\n" }
logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}" logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}"
end end
...@@ -268,6 +271,27 @@ class ApplicationController < ActionController::Base ...@@ -268,6 +271,27 @@ class ApplicationController < ActionController::Base
end end
end end
def enforce_terms!
return unless current_user
return if current_user.terms_accepted?
if sessionless_user?
render_403
else
# Redirect to the destination if the request is a get.
# Redirect to the source if it was a post, so the user can re-submit after
# accepting the terms.
redirect_path = if request.get?
request.fullpath
else
URI(request.referer).path if request.referer
end
flash[:notice] = _("Please accept the Terms of Service before continuing.")
redirect_to terms_path(redirect: redirect_path), status: :found
end
end
def import_sources_enabled? def import_sources_enabled?
!Gitlab::CurrentSettings.import_sources.empty? !Gitlab::CurrentSettings.import_sources.empty?
end end
...@@ -341,4 +365,12 @@ class ApplicationController < ActionController::Base ...@@ -341,4 +365,12 @@ class ApplicationController < ActionController::Base
# Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8 # Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8
response.headers['Page-Title'] = URI.escape(page_title('GitLab')) response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
end end
def sessionless_user?
current_user && !session.keys.include?('warden.user.user.key')
end
def peek_request?
request.path.start_with?('/-/peek')
end
end end
module ContinueParams module ContinueParams
include InternalRedirect
extend ActiveSupport::Concern extend ActiveSupport::Concern
def continue_params def continue_params
...@@ -6,8 +7,7 @@ module ContinueParams ...@@ -6,8 +7,7 @@ module ContinueParams
return nil unless continue_params return nil unless continue_params
continue_params = continue_params.permit(:to, :notice, :notice_now) continue_params = continue_params.permit(:to, :notice, :notice_now)
return unless continue_params[:to] && continue_params[:to].start_with?('/') continue_params[:to] = safe_redirect_path(continue_params[:to])
return if continue_params[:to].start_with?('//')
continue_params continue_params
end end
......
module InternalRedirect
extend ActiveSupport::Concern
def safe_redirect_path(path)
return unless path
# Verify that the string starts with a `/` but not a double `/`.
return unless path =~ %r{^/\w.*$}
uri = URI(path)
# Ignore anything path of the redirect except for the path, querystring and,
# fragment, forcing the redirect within the same host.
full_path_for_uri(uri)
rescue URI::InvalidURIError
nil
end
def safe_redirect_path_for_url(url)
return unless url
uri = URI(url)
safe_redirect_path(full_path_for_uri(uri)) if host_allowed?(uri)
rescue URI::InvalidURIError
nil
end
def host_allowed?(uri)
uri.host == request.host &&
uri.port == request.port
end
def full_path_for_uri(uri)
path_with_query = [uri.path, uri.query].compact.join('?')
[path_with_query, uri.fragment].compact.join("#")
end
end
...@@ -57,7 +57,7 @@ module IssuableCollections ...@@ -57,7 +57,7 @@ module IssuableCollections
out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables
if out_of_range if out_of_range
redirect_to(url_for(params.merge(page: total_pages, only_path: true))) redirect_to(url_for(safe_params.merge(page: total_pages, only_path: true)))
end end
out_of_range out_of_range
......
...@@ -33,6 +33,6 @@ class Groups::ApplicationController < ApplicationController ...@@ -33,6 +33,6 @@ class Groups::ApplicationController < ApplicationController
def build_canonical_path(group) def build_canonical_path(group)
params[:group_id] = group.to_param params[:group_id] = group.to_param
url_for(params) url_for(safe_params)
end end
end end
class Import::BaseController < ApplicationController class Import::BaseController < ApplicationController
private private
def find_already_added_projects(import_type)
current_user.created_projects.where(import_type: import_type).includes(:import_state)
end
def find_jobs(import_type)
current_user.created_projects
.includes(:import_state)
.where(import_type: import_type)
.to_json(only: [:id], methods: [:import_status])
end
def find_or_create_namespace(names, owner) def find_or_create_namespace(names, owner)
names = params[:target_namespace].presence || names names = params[:target_namespace].presence || names
......
...@@ -22,16 +22,14 @@ class Import::BitbucketController < Import::BaseController ...@@ -22,16 +22,14 @@ class Import::BitbucketController < Import::BaseController
@repos, @incompatible_repos = repos.partition { |repo| repo.valid? } @repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
@already_added_projects = current_user.created_projects.where(import_type: 'bitbucket') @already_added_projects = find_already_added_projects('bitbucket')
already_added_projects_names = @already_added_projects.pluck(:import_source) already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) } @repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) }
end end
def jobs def jobs
render json: current_user.created_projects render json: find_jobs('bitbucket')
.where(import_type: 'bitbucket')
.to_json(only: [:id, :import_status])
end end
def create def create
......
...@@ -46,15 +46,14 @@ class Import::FogbugzController < Import::BaseController ...@@ -46,15 +46,14 @@ class Import::FogbugzController < Import::BaseController
@repos = client.repos @repos = client.repos
@already_added_projects = current_user.created_projects.where(import_type: 'fogbugz') @already_added_projects = find_already_added_projects('fogbugz')
already_added_projects_names = @already_added_projects.pluck(:import_source) already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.reject! { |repo| already_added_projects_names.include? repo.name } @repos.reject! { |repo| already_added_projects_names.include? repo.name }
end end
def jobs def jobs
jobs = current_user.created_projects.where(import_type: 'fogbugz').to_json(only: [:id, :import_status]) render json: find_jobs('fogbugz')
render json: jobs
end end
def create def create
......
...@@ -24,15 +24,14 @@ class Import::GithubController < Import::BaseController ...@@ -24,15 +24,14 @@ class Import::GithubController < Import::BaseController
def status def status
@repos = client.repos @repos = client.repos
@already_added_projects = current_user.created_projects.where(import_type: provider) @already_added_projects = find_already_added_projects(provider)
already_added_projects_names = @already_added_projects.pluck(:import_source) already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.reject! { |repo| already_added_projects_names.include? repo.full_name } @repos.reject! { |repo| already_added_projects_names.include? repo.full_name }
end end
def jobs def jobs
jobs = current_user.created_projects.where(import_type: provider).to_json(only: [:id, :import_status]) render json: find_jobs(provider)
render json: jobs
end end
def create def create
......
...@@ -12,15 +12,14 @@ class Import::GitlabController < Import::BaseController ...@@ -12,15 +12,14 @@ class Import::GitlabController < Import::BaseController
def status def status
@repos = client.projects @repos = client.projects
@already_added_projects = current_user.created_projects.where(import_type: "gitlab") @already_added_projects = find_already_added_projects('gitlab')
already_added_projects_names = @already_added_projects.pluck(:import_source) already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos = @repos.to_a.reject { |repo| already_added_projects_names.include? repo["path_with_namespace"] } @repos = @repos.to_a.reject { |repo| already_added_projects_names.include? repo["path_with_namespace"] }
end end
def jobs def jobs
jobs = current_user.created_projects.where(import_type: "gitlab").to_json(only: [:id, :import_status]) render json: find_jobs('gitlab')
render json: jobs
end end
def create def create
......
...@@ -73,15 +73,14 @@ class Import::GoogleCodeController < Import::BaseController ...@@ -73,15 +73,14 @@ class Import::GoogleCodeController < Import::BaseController
@repos = client.repos @repos = client.repos
@incompatible_repos = client.incompatible_repos @incompatible_repos = client.incompatible_repos
@already_added_projects = current_user.created_projects.where(import_type: "google_code") @already_added_projects = find_already_added_projects('google_code')
already_added_projects_names = @already_added_projects.pluck(:import_source) already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.reject! { |repo| already_added_projects_names.include? repo.name } @repos.reject! { |repo| already_added_projects_names.include? repo.name }
end end
def jobs def jobs
jobs = current_user.created_projects.where(import_type: "google_code").to_json(only: [:id, :import_status]) render json: find_jobs('google_code')
render json: jobs
end end
def create def create
......
...@@ -82,7 +82,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -82,7 +82,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if identity_linker.changed? if identity_linker.changed?
redirect_identity_linked redirect_identity_linked
elsif identity_linker.error_message.present? elsif identity_linker.failed?
redirect_identity_link_failed(identity_linker.error_message) redirect_identity_link_failed(identity_linker.error_message)
else else
redirect_identity_exists redirect_identity_exists
......
class Profiles::ActiveSessionsController < Profiles::ApplicationController
def index
@sessions = ActiveSession.list(current_user)
end
def destroy
ActiveSession.destroy(current_user, params[:id])
respond_to do |format|
format.html { redirect_to profile_active_sessions_url, status: 302 }
format.js { head :ok }
end
end
end
...@@ -25,7 +25,7 @@ class Projects::ApplicationController < ApplicationController ...@@ -25,7 +25,7 @@ class Projects::ApplicationController < ApplicationController
params[:namespace_id] = project.namespace.to_param params[:namespace_id] = project.namespace.to_param
params[:project_id] = project.to_param params[:project_id] = project.to_param
url_for(params) url_for(safe_params)
end end
def repository def repository
......
...@@ -8,8 +8,11 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -8,8 +8,11 @@ class Projects::CompareController < Projects::ApplicationController
# Authorize # Authorize
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :authorize_download_code! before_action :authorize_download_code!
before_action :define_ref_vars, only: [:index, :show, :diff_for_path] # Defining ivars
before_action :define_diff_vars, only: [:show, :diff_for_path] before_action :define_diffs, only: [:show, :diff_for_path]
before_action :define_environment, only: [:show]
before_action :define_diff_notes_disabled, only: [:show, :diff_for_path]
before_action :define_commits, only: [:show, :diff_for_path, :signatures]
before_action :merge_request, only: [:index, :show] before_action :merge_request, only: [:index, :show]
def index def index
...@@ -22,9 +25,9 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -22,9 +25,9 @@ class Projects::CompareController < Projects::ApplicationController
end end
def diff_for_path def diff_for_path
return render_404 unless @compare return render_404 unless compare
render_diff_for_path(@compare.diffs(diff_options)) render_diff_for_path(compare.diffs(diff_options))
end end
def create def create
...@@ -41,30 +44,60 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -41,30 +44,60 @@ class Projects::CompareController < Projects::ApplicationController
end end
end end
def signatures
respond_to do |format|
format.json do
render json: {
signatures: @commits.select(&:has_signature?).map do |commit|
{
commit_sha: commit.sha,
html: view_to_html_string('projects/commit/_signature', signature: commit.signature)
}
end
}
end
end
end
private private
def define_ref_vars def compare
@start_ref = Addressable::URI.unescape(params[:from]) return @compare if defined?(@compare)
@compare = CompareService.new(@project, head_ref).execute(@project, start_ref)
end
def start_ref
@start_ref ||= Addressable::URI.unescape(params[:from])
end
def head_ref
return @ref if defined?(@ref)
@ref = @head_ref = Addressable::URI.unescape(params[:to]) @ref = @head_ref = Addressable::URI.unescape(params[:to])
end end
def define_diff_vars def define_commits
@compare = CompareService.new(@project, @head_ref) @commits = compare.present? ? prepare_commits_for_rendering(compare.commits) : []
.execute(@project, @start_ref) end
if @compare def define_diffs
@commits = prepare_commits_for_rendering(@compare.commits) @diffs = compare.present? ? compare.diffs(diff_options) : []
@diffs = @compare.diffs(diff_options) end
environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @compare.commit } def define_environment
if compare
environment_params = @repository.branch_exists?(head_ref) ? { ref: head_ref } : { commit: compare.commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@diff_notes_disabled = true
end end
end end
def define_diff_notes_disabled
@diff_notes_disabled = compare.present?
end
def merge_request def merge_request
@merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
.find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) .find_by(source_project: @project, source_branch: head_ref, target_branch: start_ref)
end end
end end
...@@ -77,8 +77,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController ...@@ -77,8 +77,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
def link_to_project!(object) def link_to_project!(object)
if object && !object.projects.exists?(storage_project.id) if object && !object.projects.exists?(storage_project.id)
object.projects << storage_project object.lfs_objects_projects.create!(project: storage_project)
object.save!
end end
end end
end end
...@@ -83,13 +83,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -83,13 +83,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
render layout: false render layout: false
end end
def update_branches
@target_project = selected_target_project
@target_branches = @target_project ? @target_project.repository.branch_names : []
render layout: false
end
private private
def build_merge_request def build_merge_request
......
...@@ -104,9 +104,18 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -104,9 +104,18 @@ class Projects::PipelinesController < Projects::ApplicationController
@stage = pipeline.legacy_stage(params[:stage]) @stage = pipeline.legacy_stage(params[:stage])
return not_found unless @stage return not_found unless @stage
respond_to do |format| render json: StageSerializer
format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } } .new(project: @project, current_user: @current_user)
end .represent(@stage, details: true)
end
# TODO: This endpoint is used by mini-pipeline-graph
# TODO: This endpoint should be migrated to `stage.json`
def stage_ajax
@stage = pipeline.legacy_stage(params[:stage])
return not_found unless @stage
render json: { html: view_to_html_string('projects/pipelines/_stage') }
end end
def retry def retry
...@@ -157,7 +166,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -157,7 +166,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end end
def create_params def create_params
params.require(:pipeline).permit(:ref) params.require(:pipeline).permit(:ref, variables_attributes: %i[key secret_value])
end end
def pipeline def pipeline
......
...@@ -52,6 +52,12 @@ class Projects::RunnersController < Projects::ApplicationController ...@@ -52,6 +52,12 @@ class Projects::RunnersController < Projects::ApplicationController
redirect_to project_settings_ci_cd_path(@project) redirect_to project_settings_ci_cd_path(@project)
end end
def toggle_group_runners
project.toggle_ci_cd_settings!(:group_runners_enabled)
redirect_to project_settings_ci_cd_path(@project)
end
protected protected
def set_runner def set_runner
......
...@@ -67,10 +67,18 @@ module Projects ...@@ -67,10 +67,18 @@ module Projects
def define_runners_variables def define_runners_variables
@project_runners = @project.runners.ordered @project_runners = @project.runners.ordered
@assignable_runners = current_user.ci_authorized_runners
.assignable_for(project).ordered.page(params[:page]).per(20) @assignable_runners = current_user
.ci_authorized_runners
.assignable_for(project)
.ordered
.page(params[:page]).per(20)
@shared_runners = ::Ci::Runner.shared.active @shared_runners = ::Ci::Runner.shared.active
@shared_runners_count = @shared_runners.count(:all) @shared_runners_count = @shared_runners.count(:all)
@group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id)
end end
def define_secret_variables def define_secret_variables
......
class SessionsController < Devise::SessionsController class SessionsController < Devise::SessionsController
include InternalRedirect
include AuthenticatesWithTwoFactor include AuthenticatesWithTwoFactor
include Devise::Controllers::Rememberable include Devise::Controllers::Rememberable
include Recaptcha::ClientHelper include Recaptcha::ClientHelper
...@@ -102,18 +103,12 @@ class SessionsController < Devise::SessionsController ...@@ -102,18 +103,12 @@ class SessionsController < Devise::SessionsController
# we should never redirect to '/users/sign_in' after signing in successfully. # we should never redirect to '/users/sign_in' after signing in successfully.
return true if redirect_uri.path == new_user_session_path return true if redirect_uri.path == new_user_session_path
redirect_to = redirect_uri.to_s if redirect_allowed_to?(redirect_uri) redirect_to = redirect_uri.to_s if host_allowed?(redirect_uri)
@redirect_to = redirect_to @redirect_to = redirect_to
store_location_for(:redirect, redirect_to) store_location_for(:redirect, redirect_to)
end end
# Overridden in EE
def redirect_allowed_to?(uri)
uri.host == Gitlab.config.gitlab.host &&
uri.port == Gitlab.config.gitlab.port
end
def two_factor_enabled? def two_factor_enabled?
find_user&.two_factor_enabled? find_user&.two_factor_enabled?
end end
......
module Users
class TermsController < ApplicationController
include InternalRedirect
skip_before_action :enforce_terms!
before_action :terms
layout 'terms'
def index
@redirect = redirect_path
end
def accept
agreement = Users::RespondToTermsService.new(current_user, viewed_term)
.execute(accepted: true)
if agreement.persisted?
redirect_to redirect_path
else
flash[:alert] = agreement.errors.full_messages.join(', ')
redirect_to terms_path, redirect: redirect_path
end
end
def decline
agreement = Users::RespondToTermsService.new(current_user, viewed_term)
.execute(accepted: false)
if agreement.persisted?
sign_out(current_user)
redirect_to root_path
else
flash[:alert] = agreement.errors.full_messages.join(', ')
redirect_to terms_path, redirect: redirect_path
end
end
private
def viewed_term
@viewed_term ||= ApplicationSetting::Term.find(params[:id])
end
def terms
unless @term = Gitlab::CurrentSettings.current_application_settings.latest_terms
redirect_to redirect_path
end
end
def redirect_path
redirect_to_path = safe_redirect_path(params[:redirect]) || safe_redirect_path_for_url(request.referer)
if redirect_to_path &&
excluded_redirect_paths.none? { |excluded| redirect_to_path.include?(excluded) }
redirect_to_path
else
root_path
end
end
def excluded_redirect_paths
[terms_path, new_user_session_path]
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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