Commit 5ee20b63 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'master' into '37970-ci-sections-tracking'

# Conflicts:
#   db/schema.rb
parents c0cfc9eb d4f3963e
...@@ -27,7 +27,7 @@ variables: ...@@ -27,7 +27,7 @@ variables:
GET_SOURCES_ATTEMPTS: "3" GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json
FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/${CI_PROJECT_NAME}/report-master.json FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/report-suite.json
before_script: before_script:
- bundle --version - bundle --version
...@@ -87,12 +87,13 @@ stages: ...@@ -87,12 +87,13 @@ stages:
- export CI_NODE_TOTAL=${JOB_NAME[-1]} - export CI_NODE_TOTAL=${JOB_NAME[-1]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true - export KNAPSACK_GENERATE_REPORT=true
- export ALL_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/${CI_PROJECT_NAME}/all_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export SUITE_FLAKY_RSPEC_REPORT_PATH=${FLAKY_RSPEC_SUITE_REPORT_PATH}
- export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/${CI_PROJECT_NAME}/new_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export FLAKY_RSPEC_REPORT_PATH=rspec_flaky/all_${JOB_NAME[0]}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/new_${JOB_NAME[0]}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export FLAKY_RSPEC_GENERATE_REPORT=true - export FLAKY_RSPEC_GENERATE_REPORT=true
- export CACHE_CLASSES=true - export CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- cp ${FLAKY_RSPEC_SUITE_REPORT_PATH} ${ALL_FLAKY_RSPEC_REPORT_PATH} - '[[ -f $FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_REPORT_PATH}'
- '[[ -f $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}' - '[[ -f $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}'
- scripts/gitaly-test-spawn - scripts/gitaly-test-spawn
- knapsack rspec "--color --format documentation" - knapsack rspec "--color --format documentation"
...@@ -233,7 +234,7 @@ retrieve-tests-metadata: ...@@ -233,7 +234,7 @@ retrieve-tests-metadata:
- wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH - wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH
- '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}' - '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
- '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}' - '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}'
- mkdir -p rspec_flaky/${CI_PROJECT_NAME}/ - mkdir -p rspec_flaky/
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH - wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}' - '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
...@@ -252,22 +253,21 @@ update-tests-metadata: ...@@ -252,22 +253,21 @@ update-tests-metadata:
- retry gem install fog-aws mime-types - retry gem install fog-aws mime-types
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
- scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/${CI_PROJECT_NAME}/all_node_*.json - scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH' - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH' - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
- rm -f rspec_flaky/${CI_PROJECT_NAME}/*_node_*.json - rm -f rspec_flaky/all_*.json rspec_flaky/new_*.json
flaky-examples-check: flaky-examples-check:
<<: *dedicated-runner <<: *dedicated-runner
image: ruby:2.3-alpine image: ruby:2.3-alpine
services: [] services: []
before_script: [] before_script: []
cache: {}
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false" USE_BUNDLE_INSTALL: "false"
NEW_FLAKY_SPECS_REPORT: rspec_flaky/${CI_PROJECT_NAME}/new_rspec_flaky_examples.json NEW_FLAKY_SPECS_REPORT: rspec_flaky/report-new.json
stage: post-test stage: post-test
allow_failure: yes allow_failure: yes
only: only:
...@@ -281,7 +281,7 @@ flaky-examples-check: ...@@ -281,7 +281,7 @@ flaky-examples-check:
- rspec_flaky/ - rspec_flaky/
script: script:
- '[[ -f $NEW_FLAKY_SPECS_REPORT ]] || echo "{}" > ${NEW_FLAKY_SPECS_REPORT}' - '[[ -f $NEW_FLAKY_SPECS_REPORT ]] || echo "{}" > ${NEW_FLAKY_SPECS_REPORT}'
- scripts/merge-reports $NEW_FLAKY_SPECS_REPORT rspec_flaky/${CI_PROJECT_NAME}/new_node_*.json - scripts/merge-reports ${NEW_FLAKY_SPECS_REPORT} rspec_flaky/new_*_*.json
- scripts/detect-new-flaky-examples $NEW_FLAKY_SPECS_REPORT - scripts/detect-new-flaky-examples $NEW_FLAKY_SPECS_REPORT
setup-test-env: setup-test-env:
......
...@@ -2,6 +2,23 @@ ...@@ -2,6 +2,23 @@
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.0.3 (2017-10-05)
- [FIXED] find_user Users helper method no longer overrides find_user API helper method. !14418
- [FIXED] Fix CSRF validation issue when closing/opening merge requests from the UI. !14555
- [FIXED] Kubernetes integration: ensure v1.8.0 compatibility. !14635
- [FIXED] Fixes data parameter not being sent in ajax request for jobs log.
- [FIXED] Improves UX of autodevops popover to match gpg one.
- [FIXED] Fixed commenting on side-by-side commit diff.
- [FIXED] Make sure API responds with 401 when invalid authentication info is provided.
- [FIXED] Fix merge request counter updates after merge.
- [FIXED] Fix gitlab-rake gitlab:import:repos task failing.
- [FIXED] Fix pushes to an empty repository not invalidating has_visible_content? cache.
- [FIXED] Ensure all refs are restored on a restore from backup.
- [FIXED] Gitaly RepositoryExists remains opt-in for all method calls.
- [FIXED] Fix 500 error on merged merge requests when GitLab is restored from a backup.
- [FIXED] Adjust MRs being stuck on "process of being merged" for more than 2 hours.
## 10.0.2 (2017-09-27) ## 10.0.2 (2017-09-27)
- [FIXED] Notes will not show an empty bubble when the author isn't a member. !14450 - [FIXED] Notes will not show an empty bubble when the author isn't a member. !14450
...@@ -195,6 +212,10 @@ entry. ...@@ -195,6 +212,10 @@ entry.
- Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi) - Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi)
- [BUGIFX] Improves subgroup creation permissions. !13418 - [BUGIFX] Improves subgroup creation permissions. !13418
## 9.5.8 (2017-10-04)
- [FIXED] Fixed fork button being disabled for users who can fork to a group.
## 9.5.7 (2017-10-03) ## 9.5.7 (2017-10-03)
- Fix gitlab rake:import:repos task. - Fix gitlab rake:import:repos task.
......
...@@ -23,7 +23,7 @@ gem 'faraday', '~> 0.12' ...@@ -23,7 +23,7 @@ gem 'faraday', '~> 0.12'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0' gem 'doorkeeper', '~> 4.2.0'
gem 'doorkeeper-openid_connect', '~> 1.1.0' gem 'doorkeeper-openid_connect', '~> 1.2.0'
gem 'omniauth', '~> 1.4.2' gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.9' gem 'omniauth-azure-oauth2', '~> 0.0.9'
......
...@@ -80,7 +80,7 @@ GEM ...@@ -80,7 +80,7 @@ GEM
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubis (>= 2.6.6) erubis (>= 2.6.6)
rack (>= 0.9.0) rack (>= 0.9.0)
bindata (2.3.5) bindata (2.4.1)
binding_of_caller (0.7.2) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootstrap-sass (3.3.6) bootstrap-sass (3.3.6)
...@@ -166,9 +166,9 @@ GEM ...@@ -166,9 +166,9 @@ GEM
docile (1.1.5) docile (1.1.5)
domain_name (0.5.20161021) domain_name (0.5.20161021)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.0) doorkeeper (4.2.6)
railties (>= 4.2) railties (>= 4.2)
doorkeeper-openid_connect (1.1.2) doorkeeper-openid_connect (1.2.0)
doorkeeper (~> 4.0) doorkeeper (~> 4.0)
json-jwt (~> 1.6) json-jwt (~> 1.6)
dropzonejs-rails (0.7.2) dropzonejs-rails (0.7.2)
...@@ -410,7 +410,7 @@ GEM ...@@ -410,7 +410,7 @@ GEM
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
json (1.8.6) json (1.8.6)
json-jwt (1.7.1) json-jwt (1.7.2)
activesupport activesupport
bindata bindata
multi_json (>= 1.3) multi_json (>= 1.3)
...@@ -681,7 +681,7 @@ GEM ...@@ -681,7 +681,7 @@ GEM
rainbow (2.2.2) rainbow (2.2.2)
rake rake
raindrops (0.18.0) raindrops (0.18.0)
rake (12.0.0) rake (12.1.0)
rblineprof (0.3.6) rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3) debugger-ruby_core_source (~> 1.3)
rbnacl (4.0.2) rbnacl (4.0.2)
...@@ -910,7 +910,7 @@ GEM ...@@ -910,7 +910,7 @@ GEM
json (>= 1.8.0) json (>= 1.8.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.2) unf_ext (0.0.7.4)
unicode-display_width (1.3.0) unicode-display_width (1.3.0)
unicorn (5.1.0) unicorn (5.1.0)
kgio (~> 2.6) kgio (~> 2.6)
...@@ -1002,7 +1002,7 @@ DEPENDENCIES ...@@ -1002,7 +1002,7 @@ DEPENDENCIES
devise-two-factor (~> 3.0.0) devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0) diffy (~> 3.1.0)
doorkeeper (~> 4.2.0) doorkeeper (~> 4.2.0)
doorkeeper-openid_connect (~> 1.1.0) doorkeeper-openid_connect (~> 1.2.0)
dropzonejs-rails (~> 0.7.1) dropzonejs-rails (~> 0.7.1)
email_reply_trimmer (~> 0.1) email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0) email_spec (~> 1.6.0)
......
<svg width="24" height="30" viewBox="0 0 24 30" xmlns="http://www.w3.org/2000/svg"><title>cursor</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#1F78D1" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#FFF"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787c-.91 0-1.763.156-2.558.469-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068 0-.009.01-.031.033-.067a.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26.094-.126.168-.24.221-.342.054-.103.114-.235.181-.395.067-.161.125-.33.174-.51-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#1F78D1" fill-rule="nonzero"/></g></svg>
<svg width="48" height="60" viewBox="0 0 48 60" xmlns="http://www.w3.org/2000/svg"><title>cursor_2x</title><g fill="none" fill-rule="evenodd"><path d="M48 24.21C48 37.583 36.522 47.369 24 60 11.478 47.368 0 37.582 0 24.21 0 10.84 10.745 0 24 0s24 10.84 24 24.21z" fill="#1F78D1" fill-rule="nonzero"/><path d="M30.56 50.497c2.915-2.95 5.078-5.268 6.947-7.493 5.703-6.788 8.406-12.53 8.406-18.793 0-12.223-9.815-22.124-21.913-22.124S2.087 11.988 2.087 24.211c0 6.263 2.703 12.005 8.406 18.793 1.87 2.225 4.032 4.544 6.947 7.493 1.022 1.035 4.432 4.426 6.56 6.55 2.128-2.124 5.538-5.515 6.56-6.55z" fill="#FFF"/><path d="M29.103 16.512c-1.58-.625-3.282-.938-5.103-.938-1.821 0-3.527.313-5.116.938-1.58.616-2.84 1.45-3.777 2.504-.928 1.054-1.393 2.192-1.393 3.415 0 1 .317 1.956.951 2.866.643.902 1.545 1.684 2.706 2.344l1.165.67-.362 1.286a9.603 9.603 0 0 1-.937 2.303 13.208 13.208 0 0 0 3.683-2.29l.576-.509.763.08c.616.072 1.196.108 1.741.108 1.821 0 3.522-.308 5.103-.925 1.589-.625 2.848-1.464 3.776-2.517.938-1.054 1.407-2.192 1.407-3.416 0-1.223-.469-2.361-1.407-3.415-.928-1.053-2.187-1.888-3.776-2.504zm5.29 1.62c1.071 1.313 1.607 2.746 1.607 4.3 0 1.553-.536 2.99-1.607 4.312-1.072 1.312-2.527 2.353-4.366 3.12-1.84.76-3.848 1.139-6.027 1.139a18.32 18.32 0 0 1-1.942-.107c-1.768 1.562-3.821 2.643-6.16 3.24-.438.126-.947.224-1.527.295h-.067a.521.521 0 0 1-.362-.147.649.649 0 0 1-.214-.362v-.013c-.027-.036-.032-.09-.014-.16.027-.072.036-.117.027-.135 0-.017.022-.062.067-.133a1.29 1.29 0 0 0 .08-.121c.01-.009.04-.045.094-.107a106.068 106.068 0 0 1 .522-.59c.215-.232.367-.401.456-.508.098-.099.236-.273.415-.523.188-.25.335-.477.442-.683.107-.205.228-.468.362-.79.134-.321.25-.66.348-1.018-1.402-.794-2.51-1.777-3.322-2.946C12.402 25.025 12 23.77 12 22.43c0-1.553.536-2.986 1.607-4.299 1.072-1.321 2.527-2.361 4.366-3.12 1.84-.768 3.848-1.152 6.027-1.152 2.179 0 4.188.384 6.027 1.152 1.84.759 3.294 1.799 4.366 3.12z" fill="#1F78D1" fill-rule="nonzero"/></g></svg>
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */
import { visitUrl } from './lib/utils/url_utility';
import { convertPermissionToBoolean } from './lib/utils/common_utils';
window.BuildArtifacts = (function() { window.BuildArtifacts = (function() {
function BuildArtifacts() { function BuildArtifacts() {
this.disablePropagation(); this.disablePropagation();
this.setupEntryClick(); this.setupEntryClick();
this.setupTooltips();
} }
BuildArtifacts.prototype.disablePropagation = function() { BuildArtifacts.prototype.disablePropagation = function() {
...@@ -17,9 +20,28 @@ window.BuildArtifacts = (function() { ...@@ -17,9 +20,28 @@ window.BuildArtifacts = (function() {
BuildArtifacts.prototype.setupEntryClick = function() { BuildArtifacts.prototype.setupEntryClick = function() {
return $('.tree-holder').on('click', 'tr[data-link]', function(e) { return $('.tree-holder').on('click', 'tr[data-link]', function(e) {
return window.location = this.dataset.link; visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink));
}); });
}; };
BuildArtifacts.prototype.setupTooltips = function() {
$('.js-artifact-tree-tooltip').tooltip({
placement: 'bottom',
// Stop the tooltip from hiding when we stop hovering the element directly
// We handle all the showing/hiding below
trigger: 'manual',
});
// We want the tooltip to show if you hover anywhere on the row
// But be placed below and in the middle of the file name
$('.js-artifact-tree-row')
.on('mouseenter', (e) => {
$(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('show');
})
.on('mouseleave', (e) => {
$(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('hide');
});
};
return BuildArtifacts; return BuildArtifacts;
})(); })();
/* globals Flash */
import Visibility from 'visibilityjs';
import axios from 'axios';
import Poll from './lib/utils/poll';
import { s__ } from './locale';
import './flash';
/**
* Cluster page has 2 separate parts:
* Toggle button
*
* - Polling status while creating or scheduled
* -- Update status area with the response result
*/
class ClusterService {
constructor(options = {}) {
this.options = options;
}
fetchData() {
return axios.get(this.options.endpoint);
}
}
export default class Clusters {
constructor() {
const dataset = document.querySelector('.js-edit-cluster-form').dataset;
this.state = {
statusPath: dataset.statusPath,
clusterStatus: dataset.clusterStatus,
clusterStatusReason: dataset.clusterStatusReason,
toggleStatus: dataset.toggleStatus,
};
this.service = new ClusterService({ endpoint: this.state.statusPath });
this.toggleButton = document.querySelector('.js-toggle-cluster');
this.toggleInput = document.querySelector('.js-toggle-input');
this.errorContainer = document.querySelector('.js-cluster-error');
this.successContainer = document.querySelector('.js-cluster-success');
this.creatingContainer = document.querySelector('.js-cluster-creating');
this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
this.toggleButton.addEventListener('click', this.toggle.bind(this));
if (this.state.clusterStatus !== 'created') {
this.updateContainer(this.state.clusterStatus, this.state.clusterStatusReason);
}
if (this.state.statusPath) {
this.initPolling();
}
}
toggle() {
this.toggleButton.classList.toggle('checked');
this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
}
initPolling() {
this.poll = new Poll({
resource: this.service,
method: 'fetchData',
successCallback: (data) => {
const { status, status_reason } = data.data;
this.updateContainer(status, status_reason);
},
errorCallback: () => {
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
},
});
if (!Visibility.hidden()) {
this.poll.makeRequest();
} else {
this.service.fetchData();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
}
hideAll() {
this.errorContainer.classList.add('hidden');
this.successContainer.classList.add('hidden');
this.creatingContainer.classList.add('hidden');
}
updateContainer(status, error) {
this.hideAll();
switch (status) {
case 'created':
this.successContainer.classList.remove('hidden');
break;
case 'errored':
this.errorContainer.classList.remove('hidden');
this.errorReasonContainer.textContent = error;
break;
case 'scheduled':
case 'creating':
this.creatingContainer.classList.remove('hidden');
break;
default:
this.hideAll();
}
}
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife */
/* global CommitFile */
window.Commit = (function() {
function Commit() {
$('.files .diff-file').each(function() {
return new CommitFile(this);
});
}
return Commit;
})();
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new */
/* global ImageFile */
(function() {
this.CommitFile = (function() {
function CommitFile(file) {
if ($('.image', file).length) {
new gl.ImageFile(file);
}
}
return CommitFile;
})();
}).call(window);
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
import 'vendor/jquery.waitforimages';
(function() { (function() {
gl.ImageFile = (function() { gl.ImageFile = (function() {
var prepareFrames; var prepareFrames;
...@@ -17,15 +19,10 @@ ...@@ -17,15 +19,10 @@
// Load two-up view after images are loaded // Load two-up view after images are loaded
// so that we can display the correct width and height information // so that we can display the correct width and height information
const images = $('.two-up.view img', _this.file); const $images = $('.two-up.view img', _this.file);
let loadedCount = 0;
images.on('load', () => {
loadedCount += 1;
if (loadedCount === images.length) { $images.waitForImages(function() {
_this.initView('two-up'); _this.initView('two-up');
}
}); });
}); });
}; };
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
<template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template> <template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template> <template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template> <template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template>
<template v-if="time.seconds && hasDa === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template> <template v-if="time.seconds && hasData === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template>
</template> </template>
<template v-else> <template v-else>
-- --
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import './lib/utils/url_utility'; import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button'; import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff'; import SingleFileDiff from './single_file_diff';
import imageDiffHelper from './image_diff/helpers/index';
const UNFOLD_COUNT = 20; const UNFOLD_COUNT = 20;
let isBound = false; let isBound = false;
...@@ -17,9 +18,12 @@ class Diff { ...@@ -17,9 +18,12 @@ class Diff {
} }
}); });
FilesCommentButton.init($diffFile); const tab = document.getElementById('diffs');
if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== '')) FilesCommentButton.init($diffFile);
$diffFile.each((index, file) => new gl.ImageFile(file)); const firstFile = $('.files').first().get(0);
const canCreateNote = firstFile && firstFile.hasAttribute('data-can-create-note');
$diffFile.each((index, file) => imageDiffHelper.initImageDiff(file, canCreateNote));
if (!isBound) { if (!isBound) {
$(document) $(document)
......
...@@ -171,7 +171,14 @@ const JumpToDiscussion = Vue.extend({ ...@@ -171,7 +171,14 @@ const JumpToDiscussion = Vue.extend({
// When jumping between unresolved discussions on the diffs tab, we show them. // When jumping between unresolved discussions on the diffs tab, we show them.
$target.closest(".content").show(); $target.closest(".content").show();
$target = $target.closest("tr.notes_holder"); const $notesHolder = $target.closest("tr.notes_holder");
// Image diff discussions does not use notes_holder
// so we should keep original $target value in those cases
if ($notesHolder.length > 0) {
$target = $notesHolder;
}
$target.show(); $target.show();
// If we are on the diffs tab, we don't scroll to the discussion itself, but to // If we are on the diffs tab, we don't scroll to the discussion itself, but to
......
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
/* global IssuableForm */ /* global IssuableForm */
/* global LabelsSelect */ /* global LabelsSelect */
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global Commit */
/* global CommitsList */ /* global CommitsList */
/* global NewBranchForm */ /* global NewBranchForm */
/* global NotificationsForm */ /* global NotificationsForm */
...@@ -316,7 +315,6 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; ...@@ -316,7 +315,6 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
new gl.Activities(); new gl.Activities();
break; break;
case 'projects:commit:show': case 'projects:commit:show':
new Commit();
new gl.Diff(); new gl.Diff();
new ZenMode(); new ZenMode();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
...@@ -525,6 +523,11 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; ...@@ -525,6 +523,11 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
case 'admin:impersonation_tokens:index': case 'admin:impersonation_tokens:index':
new gl.DueDateSelectors(); new gl.DueDateSelectors();
break; break;
case 'projects:clusters:show':
import(/* webpackChunkName: "clusters" */ './clusters')
.then(cluster => new cluster.default()) // eslint-disable-line new-cap
.catch(() => {});
break;
} }
switch (path[0]) { switch (path[0]) {
case 'sessions': case 'sessions':
......
...@@ -738,7 +738,7 @@ GitLabDropdown = (function() { ...@@ -738,7 +738,7 @@ GitLabDropdown = (function() {
: selectedObject.id; : selectedObject.id;
if (isInput) { if (isInput) {
field = $(this.el); field = $(this.el);
} else if (value) { } else if (value != null) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
} }
...@@ -746,7 +746,7 @@ GitLabDropdown = (function() { ...@@ -746,7 +746,7 @@ GitLabDropdown = (function() {
return; return;
} }
if (el.hasClass(ACTIVE_CLASS)) { if (el.hasClass(ACTIVE_CLASS) && value !== 0) {
isMarking = false; isMarking = false;
el.removeClass(ACTIVE_CLASS); el.removeClass(ACTIVE_CLASS);
if (field && field.length) { if (field && field.length) {
...@@ -852,7 +852,7 @@ GitLabDropdown = (function() { ...@@ -852,7 +852,7 @@ GitLabDropdown = (function() {
if (href && href !== '#') { if (href && href !== '#') {
gl.utils.visitUrl(href); gl.utils.visitUrl(href);
} else { } else {
$el.first().trigger('click'); $el.trigger('click');
} }
} }
}; };
......
export function createImageBadge(noteId, { x, y }, classNames = []) {
const buttonEl = document.createElement('button');
const classList = classNames.concat(['js-image-badge']);
classList.forEach(className => buttonEl.classList.add(className));
buttonEl.setAttribute('type', 'button');
buttonEl.setAttribute('disabled', true);
buttonEl.dataset.noteId = noteId;
buttonEl.style.left = `${x}px`;
buttonEl.style.top = `${y}px`;
return buttonEl;
}
export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) {
const buttonEl = createImageBadge(noteId, coordinate, ['badge']);
buttonEl.innerText = badgeText;
containerEl.appendChild(buttonEl);
}
export function addImageCommentBadge(containerEl, { coordinate, noteId }) {
const buttonEl = createImageBadge(noteId, coordinate, ['image-comment-badge', 'inverted']);
const iconEl = document.createElement('i');
iconEl.className = 'fa fa-comment-o';
iconEl.setAttribute('aria-label', 'comment');
buttonEl.appendChild(iconEl);
containerEl.appendChild(buttonEl);
}
export function addAvatarBadge(el, event) {
const { noteId, badgeNumber } = event.detail;
// Add badge to new comment
const avatarBadgeEl = el.querySelector(`#${noteId} .badge`);
avatarBadgeEl.innerText = badgeNumber;
avatarBadgeEl.classList.remove('hidden');
}
export function addCommentIndicator(containerEl, { x, y }) {
const buttonEl = document.createElement('button');
buttonEl.classList.add('btn-transparent');
buttonEl.classList.add('comment-indicator');
buttonEl.setAttribute('type', 'button');
buttonEl.style.left = `${x}px`;
buttonEl.style.top = `${y}px`;
buttonEl.innerHTML = gl.utils.spriteIcon('image-comment-dark');
containerEl.appendChild(buttonEl);
}
export function removeCommentIndicator(imageFrameEl) {
const commentIndicatorEl = imageFrameEl.querySelector('.comment-indicator');
const imageEl = imageFrameEl.querySelector('img');
const willRemove = !!commentIndicatorEl;
let meta = {};
if (willRemove) {
meta = {
x: parseInt(commentIndicatorEl.style.left, 10),
y: parseInt(commentIndicatorEl.style.top, 10),
image: {
width: imageEl.width,
height: imageEl.height,
},
};
commentIndicatorEl.remove();
}
return Object.assign({}, meta, {
removed: willRemove,
});
}
export function showCommentIndicator(imageFrameEl, coordinate) {
const { x, y } = coordinate;
const commentIndicatorEl = imageFrameEl.querySelector('.comment-indicator');
if (commentIndicatorEl) {
commentIndicatorEl.style.left = `${x}px`;
commentIndicatorEl.style.top = `${y}px`;
} else {
addCommentIndicator(imageFrameEl, coordinate);
}
}
export function commentIndicatorOnClick(event) {
// Prevent from triggering onAddImageDiffNote in notes.js
event.stopPropagation();
const buttonEl = event.currentTarget;
const diffViewerEl = buttonEl.closest('.diff-viewer');
const textareaEl = diffViewerEl.querySelector('.note-container .note-textarea');
textareaEl.focus();
}
export function setPositionDataAttribute(el, options) {
// Update position data attribute so that the
// new comment form can use this data for ajax request
const { x, y, width, height } = options;
const position = el.dataset.position;
const positionObject = Object.assign({}, JSON.parse(position), {
x,
y,
width,
height,
});
el.setAttribute('data-position', JSON.stringify(positionObject));
}
export function updateDiscussionAvatarBadgeNumber(discussionEl, newBadgeNumber) {
const avatarBadgeEl = discussionEl.querySelector('.image-diff-avatar-link .badge');
avatarBadgeEl.innerText = newBadgeNumber;
}
export function updateDiscussionBadgeNumber(discussionEl, newBadgeNumber) {
const discussionBadgeEl = discussionEl.querySelector('.badge');
discussionBadgeEl.innerText = newBadgeNumber;
}
export function toggleCollapsed(event) {
const toggleButtonEl = event.currentTarget;
const discussionNotesEl = toggleButtonEl.closest('.discussion-notes');
const formEl = discussionNotesEl.querySelector('.discussion-form');
const isCollapsed = discussionNotesEl.classList.contains('collapsed');
if (isCollapsed) {
discussionNotesEl.classList.remove('collapsed');
} else {
discussionNotesEl.classList.add('collapsed');
}
// Override the inline display style set in notes.js
if (formEl && !isCollapsed) {
formEl.style.display = 'none';
} else if (formEl && isCollapsed) {
formEl.style.display = 'block';
}
}
import * as badgeHelper from './badge_helper';
import * as commentIndicatorHelper from './comment_indicator_helper';
import * as domHelper from './dom_helper';
import * as utilsHelper from './utils_helper';
export default {
addCommentIndicator: commentIndicatorHelper.addCommentIndicator,
removeCommentIndicator: commentIndicatorHelper.removeCommentIndicator,
showCommentIndicator: commentIndicatorHelper.showCommentIndicator,
commentIndicatorOnClick: commentIndicatorHelper.commentIndicatorOnClick,
addImageBadge: badgeHelper.addImageBadge,
addImageCommentBadge: badgeHelper.addImageCommentBadge,
addAvatarBadge: badgeHelper.addAvatarBadge,
setPositionDataAttribute: domHelper.setPositionDataAttribute,
updateDiscussionAvatarBadgeNumber: domHelper.updateDiscussionAvatarBadgeNumber,
updateDiscussionBadgeNumber: domHelper.updateDiscussionBadgeNumber,
toggleCollapsed: domHelper.toggleCollapsed,
resizeCoordinatesToImageElement: utilsHelper.resizeCoordinatesToImageElement,
generateBadgeFromDiscussionDOM: utilsHelper.generateBadgeFromDiscussionDOM,
getTargetSelection: utilsHelper.getTargetSelection,
initImageDiff: utilsHelper.initImageDiff,
};
import ImageBadge from '../image_badge';
import ImageDiff from '../image_diff';
import ReplacedImageDiff from '../replaced_image_diff';
import '../../commit/image_file';
export function resizeCoordinatesToImageElement(imageEl, meta) {
const { x, y, width, height } = meta;
const imageWidth = imageEl.width;
const imageHeight = imageEl.height;
const widthRatio = imageWidth / width;
const heightRatio = imageHeight / height;
return {
x: Math.round(x * widthRatio),
y: Math.round(y * heightRatio),
width: imageWidth,
height: imageHeight,
};
}
export function generateBadgeFromDiscussionDOM(imageFrameEl, discussionEl) {
const position = JSON.parse(discussionEl.dataset.position);
const firstNoteEl = discussionEl.querySelector('.note');
const badge = new ImageBadge({
actual: position,
imageEl: imageFrameEl.querySelector('img'),
noteId: firstNoteEl.id,
discussionId: discussionEl.dataset.discussionId,
});
return badge;
}
export function getTargetSelection(event) {
const containerEl = event.currentTarget;
const imageEl = containerEl.querySelector('img');
const x = event.offsetX;
const y = event.offsetY;
const width = imageEl.width;
const height = imageEl.height;
const actualWidth = imageEl.naturalWidth;
const actualHeight = imageEl.naturalHeight;
const widthRatio = actualWidth / width;
const heightRatio = actualHeight / height;
// Browser will include the frame as a clickable target,
// which would result in potential 1px out of bounds value
// This bound the coordinates to inside the frame
const normalizedX = Math.max(0, x) && Math.min(x, width);
const normalizedY = Math.max(0, y) && Math.min(y, height);
return {
browser: {
x: normalizedX,
y: normalizedY,
width,
height,
},
actual: {
// Round x, y so that we don't need to deal with decimals
x: Math.round(normalizedX * widthRatio),
y: Math.round(normalizedY * heightRatio),
width: actualWidth,
height: actualHeight,
},
};
}
export function initImageDiff(fileEl, canCreateNote, renderCommentBadge) {
const options = {
canCreateNote,
renderCommentBadge,
};
let diff;
// ImageFile needs to be invoked before initImageDiff so that badges
// can mount to the correct location
new gl.ImageFile(fileEl); // eslint-disable-line no-new
if (fileEl.querySelector('.diff-file .js-single-image')) {
diff = new ImageDiff(fileEl, options);
diff.init();
} else if (fileEl.querySelector('.diff-file .js-replaced-image')) {
diff = new ReplacedImageDiff(fileEl, options);
diff.init();
}
return diff;
}
import imageDiffHelper from './helpers/index';
const defaultMeta = {
x: 0,
y: 0,
width: 0,
height: 0,
};
export default class ImageBadge {
constructor(options) {
const { noteId, discussionId } = options;
this.actual = options.actual || defaultMeta;
this.browser = options.browser || defaultMeta;
this.noteId = noteId;
this.discussionId = discussionId;
if (options.imageEl && !options.browser) {
this.browser = imageDiffHelper.resizeCoordinatesToImageElement(options.imageEl, this.actual);
}
}
}
import imageDiffHelper from './helpers/index';
import ImageBadge from './image_badge';
import { isImageLoaded } from '../lib/utils/image_utility';
export default class ImageDiff {
constructor(el, options) {
this.el = el;
this.canCreateNote = !!(options && options.canCreateNote);
this.renderCommentBadge = !!(options && options.renderCommentBadge);
this.$noteContainer = $('.note-container', this.el);
this.imageBadges = [];
}
init() {
this.imageFrameEl = this.el.querySelector('.diff-file .js-image-frame');
this.imageEl = this.imageFrameEl.querySelector('img');
this.bindEvents();
}
bindEvents() {
this.imageClickedWrapper = this.imageClicked.bind(this);
this.imageBlurredWrapper = imageDiffHelper.removeCommentIndicator.bind(null, this.imageFrameEl);
this.addBadgeWrapper = this.addBadge.bind(this);
this.removeBadgeWrapper = this.removeBadge.bind(this);
this.renderBadgesWrapper = this.renderBadges.bind(this);
// Render badges
if (isImageLoaded(this.imageEl)) {
this.renderBadges();
} else {
this.imageEl.addEventListener('load', this.renderBadgesWrapper);
}
// jquery makes the event delegation here much simpler
this.$noteContainer.on('click', '.js-diff-notes-toggle', imageDiffHelper.toggleCollapsed);
$(this.el).on('click', '.comment-indicator', imageDiffHelper.commentIndicatorOnClick);
if (this.canCreateNote) {
this.el.addEventListener('click.imageDiff', this.imageClickedWrapper);
this.el.addEventListener('blur.imageDiff', this.imageBlurredWrapper);
this.el.addEventListener('addBadge.imageDiff', this.addBadgeWrapper);
this.el.addEventListener('removeBadge.imageDiff', this.removeBadgeWrapper);
}
}
imageClicked(event) {
const customEvent = event.detail;
const selection = imageDiffHelper.getTargetSelection(customEvent);
const el = customEvent.currentTarget;
imageDiffHelper.setPositionDataAttribute(el, selection.actual);
imageDiffHelper.showCommentIndicator(this.imageFrameEl, selection.browser);
}
renderBadges() {
const discussionsEls = this.el.querySelectorAll('.note-container .discussion-notes .notes');
[...discussionsEls].forEach(this.renderBadge.bind(this));
}
renderBadge(discussionEl, index) {
const imageBadge = imageDiffHelper
.generateBadgeFromDiscussionDOM(this.imageFrameEl, discussionEl);
this.imageBadges.push(imageBadge);
const options = {
coordinate: imageBadge.browser,
noteId: imageBadge.noteId,
};
if (this.renderCommentBadge) {
imageDiffHelper.addImageCommentBadge(this.imageFrameEl, options);
} else {
const numberBadgeOptions = Object.assign({}, options, {
badgeText: index + 1,
});
imageDiffHelper.addImageBadge(this.imageFrameEl, numberBadgeOptions);
}
}
addBadge(event) {
const { x, y, width, height, noteId, discussionId } = event.detail;
const badgeText = this.imageBadges.length + 1;
const imageBadge = new ImageBadge({
actual: {
x,
y,
width,
height,
},
imageEl: this.imageFrameEl.querySelector('img'),
noteId,
discussionId,
});
this.imageBadges.push(imageBadge);
imageDiffHelper.addImageBadge(this.imageFrameEl, {
coordinate: imageBadge.browser,
badgeText,
noteId,
});
imageDiffHelper.addAvatarBadge(this.el, {
detail: {
noteId,
badgeNumber: badgeText,
},
});
const discussionEl = this.el.querySelector(`#discussion_${discussionId}`);
imageDiffHelper.updateDiscussionBadgeNumber(discussionEl, badgeText);
}
removeBadge(event) {
const { badgeNumber } = event.detail;
const indexToRemove = badgeNumber - 1;
const imageBadgeEls = this.imageFrameEl.querySelectorAll('.badge');
if (this.imageBadges.length !== badgeNumber) {
// Cascade badges count numbers for (avatar badges + image badges)
this.imageBadges.forEach((badge, index) => {
if (index > indexToRemove) {
const { discussionId } = badge;
const updatedBadgeNumber = index;
const discussionEl = this.el.querySelector(`#discussion_${discussionId}`);
imageBadgeEls[index].innerText = updatedBadgeNumber;
imageDiffHelper.updateDiscussionBadgeNumber(discussionEl, updatedBadgeNumber);
imageDiffHelper.updateDiscussionAvatarBadgeNumber(discussionEl, updatedBadgeNumber);
}
});
}
this.imageBadges.splice(indexToRemove, 1);
const imageBadgeEl = imageBadgeEls[indexToRemove];
imageBadgeEl.remove();
}
}
import imageDiffHelper from './helpers/index';
export default () => {
// Always pass can-create-note as false because a user
// cannot place new badge markers on discussion tab
const canCreateNote = false;
const renderCommentBadge = true;
const diffFileEls = document.querySelectorAll('.timeline-content .diff-file.js-image-file');
[...diffFileEls].forEach(diffFileEl =>
imageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge));
};
import imageDiffHelper from './helpers/index';
import { viewTypes, isValidViewType } from './view_types';
import ImageDiff from './image_diff';
export default class ReplacedImageDiff extends ImageDiff {
init(defaultViewType = viewTypes.TWO_UP) {
this.imageFrameEls = {
[viewTypes.TWO_UP]: this.el.querySelector('.two-up .js-image-frame'),
[viewTypes.SWIPE]: this.el.querySelector('.swipe .js-image-frame'),
[viewTypes.ONION_SKIN]: this.el.querySelector('.onion-skin .js-image-frame'),
};
const viewModesEl = this.el.querySelector('.view-modes-menu');
this.viewModesEls = {
[viewTypes.TWO_UP]: viewModesEl.querySelector('.two-up'),
[viewTypes.SWIPE]: viewModesEl.querySelector('.swipe'),
[viewTypes.ONION_SKIN]: viewModesEl.querySelector('.onion-skin'),
};
this.currentView = defaultViewType;
this.generateImageEls();
this.bindEvents();
}
generateImageEls() {
this.imageEls = {};
const viewTypeNames = Object.getOwnPropertyNames(viewTypes);
viewTypeNames.forEach((viewType) => {
this.imageEls[viewType] = this.imageFrameEls[viewType].querySelector('img');
});
}
bindEvents() {
super.bindEvents();
this.changeToViewTwoUp = this.changeView.bind(this, viewTypes.TWO_UP);
this.changeToViewSwipe = this.changeView.bind(this, viewTypes.SWIPE);
this.changeToViewOnionSkin = this.changeView.bind(this, viewTypes.ONION_SKIN);
this.viewModesEls[viewTypes.TWO_UP].addEventListener('click', this.changeToViewTwoUp);
this.viewModesEls[viewTypes.SWIPE].addEventListener('click', this.changeToViewSwipe);
this.viewModesEls[viewTypes.ONION_SKIN].addEventListener('click', this.changeToViewOnionSkin);
}
get imageEl() {
return this.imageEls[this.currentView];
}
get imageFrameEl() {
return this.imageFrameEls[this.currentView];
}
changeView(newView) {
if (!isValidViewType(newView)) {
return;
}
const indicator = imageDiffHelper.removeCommentIndicator(this.imageFrameEl);
this.currentView = newView;
// Clear existing badges on new view
const existingBadges = this.imageFrameEl.querySelectorAll('.badge');
[...existingBadges].map(badge => badge.remove());
// Remove existing references to old view image badges
this.imageBadges = [];
// Image_file.js has a fade animation of 200ms for loading the view
// Need to wait an additional 250ms for the images to be displayed
// on window in order to re-normalize their dimensions
setTimeout(this.renderNewView.bind(this, indicator), 250);
}
renderNewView(indicator) {
// Generate badge coordinates on new view
this.renderBadges();
// Re-render indicator in new view
if (indicator.removed) {
const normalizedIndicator = imageDiffHelper
.resizeCoordinatesToImageElement(this.imageEl, {
x: indicator.x,
y: indicator.y,
width: indicator.image.width,
height: indicator.image.height,
});
imageDiffHelper.showCommentIndicator(this.imageFrameEl, normalizedIndicator);
}
}
}
export const viewTypes = {
TWO_UP: 'TWO_UP',
SWIPE: 'SWIPE',
ONION_SKIN: 'ONION_SKIN',
};
export function isValidViewType(validate) {
return !!Object.getOwnPropertyNames(viewTypes).find(viewType => viewType === validate);
}
...@@ -14,6 +14,9 @@ If you need to compose a headers object, use the spread operator: ...@@ -14,6 +14,9 @@ If you need to compose a headers object, use the spread operator:
someOtherHeader: '12345', someOtherHeader: '12345',
} }
``` ```
see also http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf
and https://github.com/rails/jquery-rails/blob/v4.3.1/vendor/assets/javascripts/jquery_ujs.js#L59-L62
*/ */
const csrf = { const csrf = {
...@@ -53,4 +56,3 @@ if ($.rails) { ...@@ -53,4 +56,3 @@ if ($.rails) {
} }
export default csrf; export default csrf;
/* eslint-disable import/prefer-default-export */
export function isImageLoaded(element) {
return element.complete && element.naturalHeight !== 0;
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
var base; var base;
var w = window; var w = window;
if (w.gl == null) { if (w.gl == null) {
...@@ -86,6 +87,21 @@ w.gl.utils.getLocationHash = function(url) { ...@@ -86,6 +87,21 @@ w.gl.utils.getLocationHash = function(url) {
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href); w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
w.gl.utils.visitUrl = (url) => { // eslint-disable-next-line import/prefer-default-export
document.location.href = url; export function visitUrl(url, external = false) {
if (external) {
// Simulate `target="blank" rel="noopener noreferrer"`
// See https://mathiasbynens.github.io/rel-noopener/
const otherWindow = window.open();
otherWindow.opener = null;
otherWindow.location = url;
} else {
document.location.href = url;
}
}
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
visitUrl,
}; };
...@@ -54,12 +54,14 @@ LineHighlighter.prototype.bindEvents = function() { ...@@ -54,12 +54,14 @@ LineHighlighter.prototype.bindEvents = function() {
$fileHolder.on('highlight:line', this.highlightHash); $fileHolder.on('highlight:line', this.highlightHash);
}; };
LineHighlighter.prototype.highlightHash = function() { LineHighlighter.prototype.highlightHash = function(newHash) {
var range; let range;
if (newHash && typeof newHash === 'string') this._hash = newHash;
this.clearHighlight();
if (this._hash !== '') { if (this._hash !== '') {
range = this.hashToRange(this._hash); range = this.hashToRange(this._hash);
if (range[0]) { if (range[0]) {
this.highlightRange(range); this.highlightRange(range);
const lineSelector = `#L${range[0]}`; const lineSelector = `#L${range[0]}`;
......
...@@ -4,6 +4,7 @@ import sprintf from './sprintf'; ...@@ -4,6 +4,7 @@ import sprintf from './sprintf';
const langAttribute = document.querySelector('html').getAttribute('lang'); const langAttribute = document.querySelector('html').getAttribute('lang');
const lang = (langAttribute || 'en').replace(/-/g, '_'); const lang = (langAttribute || 'en').replace(/-/g, '_');
const locale = new Jed(window.translations || {}); const locale = new Jed(window.translations || {});
delete window.translations;
/** /**
Translates `text` Translates `text`
......
...@@ -35,12 +35,9 @@ import './shortcuts_network'; ...@@ -35,12 +35,9 @@ import './shortcuts_network';
import './templates/issuable_template_selector'; import './templates/issuable_template_selector';
import './templates/issuable_template_selectors'; import './templates/issuable_template_selectors';
// commit
import './commit/file';
import './commit/image_file'; import './commit/image_file';
// lib/utils // lib/utils
import './lib/utils/bootstrap_linked_tabs';
import { handleLocationHash } from './lib/utils/common_utils'; import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility'; import './lib/utils/datetime_utility';
import './lib/utils/pretty_time'; import './lib/utils/pretty_time';
...@@ -71,7 +68,6 @@ import './build'; ...@@ -71,7 +68,6 @@ import './build';
import './build_artifacts'; import './build_artifacts';
import './build_variables'; import './build_variables';
import './ci_lint_editor'; import './ci_lint_editor';
import './commit';
import './commits'; import './commits';
import './compare'; import './compare';
import './compare_autocomplete'; import './compare_autocomplete';
...@@ -111,7 +107,6 @@ import './merge_request'; ...@@ -111,7 +107,6 @@ import './merge_request';
import './merge_request_tabs'; import './merge_request_tabs';
import './milestone'; import './milestone';
import './milestone_select'; import './milestone_select';
import './mini_pipeline_graph_dropdown';
import './namespace_select'; import './namespace_select';
import './new_branch_form'; import './new_branch_form';
import './new_commit_form'; import './new_commit_form';
...@@ -119,7 +114,6 @@ import './notes'; ...@@ -119,7 +114,6 @@ import './notes';
import './notifications_dropdown'; import './notifications_dropdown';
import './notifications_form'; import './notifications_form';
import './pager'; import './pager';
import './pipelines';
import './preview_markdown'; import './preview_markdown';
import './project'; import './project';
import './project_avatar'; import './project_avatar';
......
...@@ -13,6 +13,8 @@ import { ...@@ -13,6 +13,8 @@ import {
isMetaClick, isMetaClick,
} from './lib/utils/common_utils'; } from './lib/utils/common_utils';
import initDiscussionTab from './image_diff/init_discussion_tab';
/* eslint-disable max-len */ /* eslint-disable max-len */
// MergeRequestTabs // MergeRequestTabs
// //
...@@ -154,6 +156,8 @@ import { ...@@ -154,6 +156,8 @@ import {
} }
this.resetViewContainer(); this.resetViewContainer();
this.destroyPipelinesView(); this.destroyPipelinesView();
initDiscussionTab();
} }
if (this.setUrl) { if (this.setUrl) {
this.setCurrentAction(action); this.setCurrentAction(action);
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
showEmptyState: true, showEmptyState: true,
updateAspectRatio: false, updateAspectRatio: false,
updatedAspectRatios: 0, updatedAspectRatios: 0,
hoverData: {},
resizeThrottled: {}, resizeThrottled: {},
}; };
}, },
...@@ -64,6 +65,10 @@ ...@@ -64,6 +65,10 @@
this.updatedAspectRatios = 0; this.updatedAspectRatios = 0;
} }
}, },
hoverChanged(data) {
this.hoverData = data;
},
}, },
created() { created() {
...@@ -72,10 +77,12 @@ ...@@ -72,10 +77,12 @@
deploymentEndpoint: this.deploymentEndpoint, deploymentEndpoint: this.deploymentEndpoint,
}); });
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$on('hoverChanged', this.hoverChanged);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('toggleAspectRatio', this.toggleAspectRatio); eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$off('hoverChanged', this.hoverChanged);
window.removeEventListener('resize', this.resizeThrottled, false); window.removeEventListener('resize', this.resizeThrottled, false);
}, },
...@@ -102,6 +109,7 @@ ...@@ -102,6 +109,7 @@
v-for="(graphData, index) in groupData.metrics" v-for="(graphData, index) in groupData.metrics"
:key="index" :key="index"
:graph-data="graphData" :graph-data="graphData"
:hover-data="hoverData"
:update-aspect-ratio="updateAspectRatio" :update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData" :deployment-data="store.deploymentData"
/> />
......
...@@ -73,34 +73,22 @@ ...@@ -73,34 +73,22 @@
<template> <template>
<div class="prometheus-state"> <div class="prometheus-state">
<div class="row"> <div class="state-svg svg-content">
<div class="col-md-4 col-md-offset-4 state-svg svg-content"> <img :src="currentState.svgUrl"/>
<img :src="currentState.svgUrl"/>
</div>
</div> </div>
<div class="row"> <h4 class="state-title">
<div class="col-md-6 col-md-offset-3"> {{currentState.title}}
<h4 class="text-center state-title"> </h4>
{{currentState.title}} <p class="state-description">
</h4> {{currentState.description}}
</div> <a v-if="showButtonDescription" :href="settingsPath">
</div> Prometheus server
<div class="row"> </a>
<div class="col-md-6 col-md-offset-3"> </p>
<div class="description-text text-center state-description"> <div class="state-button">
{{currentState.description}} <a class="btn btn-success" :href="buttonPath">
<a v-if="showButtonDescription" :href="settingsPath"> {{currentState.buttonText}}
Prometheus server </a>
</a>
</div>
</div>
</div>
<div class="row state-button-section">
<div class="col-md-4 col-md-offset-4 text-center state-button">
<a class="btn btn-success" :href="buttonPath">
{{currentState.buttonText}}
</a>
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -3,16 +3,14 @@ ...@@ -3,16 +3,14 @@
import GraphLegend from './graph/legend.vue'; import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue'; import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue'; import GraphDeployment from './graph/deployment.vue';
import GraphPath from './graph_path.vue'; import GraphPath from './graph/path.vue';
import MonitoringMixin from '../mixins/monitoring_mixins'; import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import measurements from '../utils/measurements'; import measurements from '../utils/measurements';
import { timeScaleFormat } from '../utils/date_time_formatters'; import { timeScaleFormat, bisectDate } from '../utils/date_time_formatters';
import createTimeSeries from '../utils/multiple_time_series'; import createTimeSeries from '../utils/multiple_time_series';
import bp from '../../breakpoints'; import bp from '../../breakpoints';
const bisectDate = d3.bisector(d => d.time).left;
export default { export default {
props: { props: {
graphData: { graphData: {
...@@ -27,6 +25,11 @@ ...@@ -27,6 +25,11 @@
type: Array, type: Array,
required: true, required: true,
}, },
hoverData: {
type: Object,
required: false,
default: () => ({}),
},
}, },
mixins: [MonitoringMixin], mixins: [MonitoringMixin],
...@@ -52,6 +55,7 @@ ...@@ -52,6 +55,7 @@
currentXCoordinate: 0, currentXCoordinate: 0,
currentFlagPosition: 0, currentFlagPosition: 0,
showFlag: false, showFlag: false,
showFlagContent: false,
showDeployInfo: true, showDeployInfo: true,
timeSeries: [], timeSeries: [],
}; };
...@@ -65,7 +69,7 @@ ...@@ -65,7 +69,7 @@
}, },
computed: { computed: {
outterViewBox() { outerViewBox() {
return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`; return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
}, },
...@@ -122,36 +126,30 @@ ...@@ -122,36 +126,30 @@
const d1 = firstTimeSeries.values[overlayIndex]; const d1 = firstTimeSeries.values[overlayIndex];
if (d0 === undefined || d1 === undefined) return; if (d0 === undefined || d1 === undefined) return;
const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay; const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
this.currentData = evalTime ? d1 : d0; const hoveredDataIndex = evalTime ? overlayIndex : (overlayIndex - 1);
this.currentDataIndex = evalTime ? overlayIndex : (overlayIndex - 1); const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time;
this.currentXCoordinate = Math.floor(firstTimeSeries.timeSeriesScaleX(this.currentData.time));
const currentDeployXPos = this.mouseOverDeployInfo(point.x); const currentDeployXPos = this.mouseOverDeployInfo(point.x);
if (this.currentXCoordinate > (this.graphWidth - 200)) { eventHub.$emit('hoverChanged', {
this.currentFlagPosition = this.currentXCoordinate - 103; hoveredDate,
} else { currentDeployXPos,
this.currentFlagPosition = this.currentXCoordinate; });
}
if (currentDeployXPos) {
this.showFlag = false;
} else {
this.showFlag = true;
}
}, },
renderAxesPaths() { renderAxesPaths() {
this.timeSeries = createTimeSeries(this.graphData.queries[0], this.timeSeries = createTimeSeries(
this.graphWidth, this.graphData.queries[0],
this.graphHeight, this.graphWidth,
this.graphHeightOffset); this.graphHeight,
this.graphHeightOffset,
);
if (this.timeSeries.length > 3) { if (this.timeSeries.length > 3) {
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20; this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
} }
const axisXScale = d3.time.scale() const axisXScale = d3.time.scale()
.range([0, this.graphWidth]); .range([0, this.graphWidth - 70]);
const axisYScale = d3.scale.linear() const axisYScale = d3.scale.linear()
.range([this.graphHeight - this.graphHeightOffset, 0]); .range([this.graphHeight - this.graphHeightOffset, 0]);
...@@ -194,6 +192,10 @@ ...@@ -194,6 +192,10 @@
eventHub.$emit('toggleAspectRatio'); eventHub.$emit('toggleAspectRatio');
} }
}, },
hoverData() {
this.positionFlag();
},
}, },
mounted() { mounted() {
...@@ -203,7 +205,10 @@ ...@@ -203,7 +205,10 @@
</script> </script>
<template> <template>
<div class="prometheus-graph"> <div
class="prometheus-graph"
@mouseover="showFlagContent = true"
@mouseleave="showFlagContent = false">
<h5 class="text-center graph-title"> <h5 class="text-center graph-title">
{{graphData.title}} {{graphData.title}}
</h5> </h5>
...@@ -211,7 +216,7 @@ ...@@ -211,7 +216,7 @@
class="prometheus-svg-container" class="prometheus-svg-container"
:style="paddingBottomRootSvg"> :style="paddingBottomRootSvg">
<svg <svg
:viewBox="outterViewBox" :viewBox="outerViewBox"
ref="baseSvg"> ref="baseSvg">
<g <g
class="x-axis" class="x-axis"
...@@ -247,6 +252,7 @@ ...@@ -247,6 +252,7 @@
<graph-deployment <graph-deployment
:show-deploy-info="showDeployInfo" :show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData" :deployment-data="reducedDeploymentData"
:graph-width="graphWidth"
:graph-height="graphHeight" :graph-height="graphHeight"
:graph-height-offset="graphHeightOffset" :graph-height-offset="graphHeightOffset"
/> />
...@@ -257,6 +263,7 @@ ...@@ -257,6 +263,7 @@
:current-flag-position="currentFlagPosition" :current-flag-position="currentFlagPosition"
:graph-height="graphHeight" :graph-height="graphHeight"
:graph-height-offset="graphHeightOffset" :graph-height-offset="graphHeightOffset"
:show-flag-content="showFlagContent"
/> />
<rect <rect
class="prometheus-graph-overlay" class="prometheus-graph-overlay"
......
...@@ -19,6 +19,10 @@ ...@@ -19,6 +19,10 @@
type: Number, type: Number,
required: true, required: true,
}, },
graphWidth: {
type: Number,
required: true,
},
}, },
computed: { computed: {
...@@ -47,6 +51,14 @@ ...@@ -47,6 +51,14 @@
transformDeploymentGroup(deployment) { transformDeploymentGroup(deployment) {
return `translate(${Math.floor(deployment.xPos) + 1}, 20)`; return `translate(${Math.floor(deployment.xPos) + 1}, 20)`;
}, },
positionFlag(deployment) {
let xPosition = 3;
if (deployment.xPos > (this.graphWidth - 200)) {
xPosition = -97;
}
return xPosition;
},
}, },
}; };
</script> </script>
...@@ -77,7 +89,7 @@ ...@@ -77,7 +89,7 @@
<svg <svg
v-if="deployment.showDeploymentFlag" v-if="deployment.showDeploymentFlag"
class="js-deploy-info-box" class="js-deploy-info-box"
x="3" :x="positionFlag(deployment)"
y="0" y="0"
width="92" width="92"
height="60"> height="60">
......
...@@ -23,6 +23,10 @@ ...@@ -23,6 +23,10 @@
type: Number, type: Number,
required: true, required: true,
}, },
showFlagContent: {
type: Boolean,
required: true,
},
}, },
data() { data() {
...@@ -57,6 +61,7 @@ ...@@ -57,6 +61,7 @@
transform="translate(-5, 20)"> transform="translate(-5, 20)">
</line> </line>
<svg <svg
v-if="showFlagContent"
class="rect-text-metric" class="rect-text-metric"
:x="currentFlagPosition" :x="currentFlagPosition"
y="0"> y="0">
......
...@@ -79,7 +79,11 @@ ...@@ -79,7 +79,11 @@
}, },
formatMetricUsage(series) { formatMetricUsage(series) {
return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`; const value = series.values[this.currentDataIndex].value;
if (isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`;
}, },
createSeriesString(index, series) { createSeriesString(index, series) {
......
import { bisectDate } from '../utils/date_time_formatters';
const mixins = { const mixins = {
methods: { methods: {
mouseOverDeployInfo(mouseXPos) { mouseOverDeployInfo(mouseXPos) {
...@@ -18,6 +20,7 @@ const mixins = { ...@@ -18,6 +20,7 @@ const mixins = {
return dataFound; return dataFound;
}, },
formatDeployments() { formatDeployments() {
this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => { this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at); const time = new Date(deployment.created_at);
...@@ -40,6 +43,25 @@ const mixins = { ...@@ -40,6 +43,25 @@ const mixins = {
return deploymentDataArray; return deploymentDataArray;
}, []); }, []);
}, },
positionFlag() {
const timeSeries = this.timeSeries[0];
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
this.currentData = timeSeries.values[hoveredDataIndex];
this.currentDataIndex = hoveredDataIndex;
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
if (this.currentXCoordinate > (this.graphWidth - 200)) {
this.currentFlagPosition = this.currentXCoordinate - 103;
} else {
this.currentFlagPosition = this.currentXCoordinate;
}
if (this.hoverData.currentDeployXPos) {
this.showFlag = false;
} else {
this.showFlag = true;
}
},
}, },
}; };
......
...@@ -3,8 +3,5 @@ import Dashboard from './components/dashboard.vue'; ...@@ -3,8 +3,5 @@ import Dashboard from './components/dashboard.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#prometheus-graphs', el: '#prometheus-graphs',
components: { render: createElement => createElement(Dashboard),
Dashboard,
},
render: createElement => createElement('dashboard'),
})); }));
...@@ -13,7 +13,7 @@ function normalizeMetrics(metrics) { ...@@ -13,7 +13,7 @@ function normalizeMetrics(metrics) {
...result, ...result,
values: result.values.map(([timestamp, value]) => ({ values: result.values.map(([timestamp, value]) => ({
time: new Date(timestamp * 1000), time: new Date(timestamp * 1000),
value, value: Number(value),
})), })),
})), })),
})), })),
......
...@@ -2,6 +2,7 @@ import d3 from 'd3'; ...@@ -2,6 +2,7 @@ import d3 from 'd3';
export const dateFormat = d3.time.format('%b %-d, %Y'); export const dateFormat = d3.time.format('%b %-d, %Y');
export const timeFormat = d3.time.format('%-I:%M%p'); export const timeFormat = d3.time.format('%-I:%M%p');
export const bisectDate = d3.bisector(d => d.time).left;
export const timeScaleFormat = d3.time.format.multi([ export const timeScaleFormat = d3.time.format.multi([
['.%L', d => d.getMilliseconds()], ['.%L', d => d.getMilliseconds()],
......
...@@ -56,12 +56,16 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra ...@@ -56,12 +56,16 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
timeSeriesScaleX.ticks(d3.time.minute, 60); timeSeriesScaleX.ticks(d3.time.minute, 60);
timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]); timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]);
const defined = d => !isNaN(d.value) && d.value != null;
const lineFunction = d3.svg.line() const lineFunction = d3.svg.line()
.defined(defined)
.interpolate('linear') .interpolate('linear')
.x(d => timeSeriesScaleX(d.time)) .x(d => timeSeriesScaleX(d.time))
.y(d => timeSeriesScaleY(d.value)); .y(d => timeSeriesScaleY(d.value));
const areaFunction = d3.svg.area() const areaFunction = d3.svg.area()
.defined(defined)
.interpolate('linear') .interpolate('linear')
.x(d => timeSeriesScaleX(d.time)) .x(d => timeSeriesScaleX(d.time))
.y0(graphHeight - graphHeightOffset) .y0(graphHeight - graphHeightOffset)
......
...@@ -24,6 +24,7 @@ import './autosave'; ...@@ -24,6 +24,7 @@ import './autosave';
import './dropzone_input'; import './dropzone_input';
import TaskList from './task_list'; import TaskList from './task_list';
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils'; import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
window.autosize = autosize; window.autosize = autosize;
window.Dropzone = Dropzone; window.Dropzone = Dropzone;
...@@ -42,6 +43,7 @@ export default class Notes { ...@@ -42,6 +43,7 @@ export default class Notes {
this.visibilityChange = this.visibilityChange.bind(this); this.visibilityChange = this.visibilityChange.bind(this);
this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this); this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this);
this.onAddDiffNote = this.onAddDiffNote.bind(this); this.onAddDiffNote = this.onAddDiffNote.bind(this);
this.onAddImageDiffNote = this.onAddImageDiffNote.bind(this);
this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this); this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this);
this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this); this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this);
this.removeNote = this.removeNote.bind(this); this.removeNote = this.removeNote.bind(this);
...@@ -114,6 +116,8 @@ export default class Notes { ...@@ -114,6 +116,8 @@ export default class Notes {
$(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote); $(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
// add diff note // add diff note
$(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote); $(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// add diff note for images
$(document).on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote);
// hide diff note form // hide diff note form
$(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm); $(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
// toggle commit list // toggle commit list
...@@ -140,6 +144,7 @@ export default class Notes { ...@@ -140,6 +144,7 @@ export default class Notes {
$(document).off('click', '.js-note-attachment-delete'); $(document).off('click', '.js-note-attachment-delete');
$(document).off('click', '.js-discussion-reply-button'); $(document).off('click', '.js-discussion-reply-button');
$(document).off('click', '.js-add-diff-note-button'); $(document).off('click', '.js-add-diff-note-button');
$(document).off('click', '.js-add-image-diff-note-button');
$(document).off('visibilitychange'); $(document).off('visibilitychange');
$(document).off('keyup input', '.js-note-text'); $(document).off('keyup input', '.js-note-text');
$(document).off('click', '.js-note-target-reopen'); $(document).off('click', '.js-note-target-reopen');
...@@ -412,6 +417,11 @@ export default class Notes { ...@@ -412,6 +417,11 @@ export default class Notes {
this.note_ids.push(noteEntity.id); this.note_ids.push(noteEntity.id);
form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`); form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
row = form.closest('tr'); row = form.closest('tr');
if (noteEntity.on_image) {
row = form;
}
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion? // is this the first note of discussion?
...@@ -423,7 +433,7 @@ export default class Notes { ...@@ -423,7 +433,7 @@ export default class Notes {
if (noteEntity.diff_discussion_html) { if (noteEntity.diff_discussion_html) {
var $discussion = $(noteEntity.diff_discussion_html).renderGFM(); var $discussion = $(noteEntity.diff_discussion_html).renderGFM();
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) { if (!this.isParallelView() || row.hasClass('js-temp-notes-holder') || noteEntity.on_image) {
// insert the note and the reply button after the temp row // insert the note and the reply button after the temp row
row.after($discussion); row.after($discussion);
} else { } else {
...@@ -449,6 +459,7 @@ export default class Notes { ...@@ -449,6 +459,7 @@ export default class Notes {
if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) { if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity); this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
} }
...@@ -561,7 +572,7 @@ export default class Notes { ...@@ -561,7 +572,7 @@ export default class Notes {
form.find('#note_line_code').val(), form.find('#note_line_code').val(),
// DiffNote // DiffNote
form.find('#note_position').val() form.find('#note_position').val(),
]; ];
return new Autosave(textarea, key); return new Autosave(textarea, key);
} }
...@@ -783,9 +794,22 @@ export default class Notes { ...@@ -783,9 +794,22 @@ export default class Notes {
$(`.js-diff-avatars-${discussionId}`).trigger('remove.vue'); $(`.js-diff-avatars-${discussionId}`).trigger('remove.vue');
// The notes tr can contain multiple lists of notes, like on the parallel diff // The notes tr can contain multiple lists of notes, like on the parallel diff
if (notesTr.find('.discussion-notes').length > 1) { // notesTr does not exist for image diffs
if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) {
const $diffFile = $notes.closest('.diff-file');
if ($diffFile.length > 0) {
const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', {
detail: {
// badgeNumber's start with 1 and index starts with 0
badgeNumber: $notes.index() + 1,
},
});
$diffFile[0].dispatchEvent(removeBadgeEvent);
}
$notes.remove(); $notes.remove();
} else { } else if (notesTr.length > 0) {
notesTr.remove(); notesTr.remove();
} }
} }
...@@ -841,7 +865,11 @@ export default class Notes { ...@@ -841,7 +865,11 @@ export default class Notes {
*/ */
setupDiscussionNoteForm(dataHolder, form) { setupDiscussionNoteForm(dataHolder, form) {
// setup note target // setup note target
const diffFileData = dataHolder.closest('.text-file'); let diffFileData = dataHolder.closest('.text-file');
if (diffFileData.length === 0) {
diffFileData = dataHolder.closest('.image');
}
var discussionID = dataHolder.data('discussionId'); var discussionID = dataHolder.data('discussionId');
...@@ -907,6 +935,31 @@ export default class Notes { ...@@ -907,6 +935,31 @@ export default class Notes {
}); });
} }
onAddImageDiffNote(e) {
const $link = $(e.currentTarget || e.target);
const $diffFile = $link.closest('.diff-file');
const clickEvent = new CustomEvent('click.imageDiff', {
detail: e,
});
$diffFile[0].dispatchEvent(clickEvent);
// Setup comment form
let newForm;
const $noteContainer = $link.closest('.diff-viewer').find('.note-container');
const $form = $noteContainer.find('> .discussion-form');
if ($form.length === 0) {
newForm = this.cleanForm(this.formClone.clone());
newForm.appendTo($noteContainer);
} else {
newForm = $form;
}
this.setupDiscussionNoteForm($link, newForm);
}
toggleDiffNote({ toggleDiffNote({
target, target,
lineType, lineType,
...@@ -999,10 +1052,25 @@ export default class Notes { ...@@ -999,10 +1052,25 @@ export default class Notes {
} }
cancelDiscussionForm(e) { cancelDiscussionForm(e) {
var form;
e.preventDefault(); e.preventDefault();
form = $(e.target).closest('.js-discussion-note-form'); const $form = $(e.target).closest('.js-discussion-note-form');
return this.removeDiscussionNoteForm(form); const $discussionNote = $(e.target).closest('.discussion-notes');
if ($discussionNote.length === 0) {
// Only send blur event when the discussion form
// is not part of a discussion note
const $diffFile = $form.closest('.diff-file');
if ($diffFile.length > 0) {
const blurEvent = new CustomEvent('blur.imageDiff', {
detail: e,
});
$diffFile[0].dispatchEvent(blurEvent);
}
}
return this.removeDiscussionNoteForm($form);
} }
/** /**
...@@ -1414,6 +1482,15 @@ export default class Notes { ...@@ -1414,6 +1482,15 @@ export default class Notes {
// Submission successful! remove placeholder // Submission successful! remove placeholder
$notesContainer.find(`#${noteUniqueId}`).remove(); $notesContainer.find(`#${noteUniqueId}`).remove();
const $diffFile = $form.closest('.diff-file');
if ($diffFile.length > 0) {
const blurEvent = new CustomEvent('blur.imageDiff', {
detail: e,
});
$diffFile[0].dispatchEvent(blurEvent);
}
// Reset cached commands list when command is applied // Reset cached commands list when command is applied
if (hasQuickActions) { if (hasQuickActions) {
$form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho'); $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho');
...@@ -1436,7 +1513,28 @@ export default class Notes { ...@@ -1436,7 +1513,28 @@ export default class Notes {
} }
// Show final note element on UI // Show final note element on UI
this.addDiscussionNote($form, note, $notesContainer.length === 0); const isNewDiffComment = $notesContainer.length === 0;
this.addDiscussionNote($form, note, isNewDiffComment);
if (isNewDiffComment) {
// Add image badge, avatar badge and toggle discussion badge for new image diffs
const notePosition = $form.find('#note_position').val();
if ($diffFile.length > 0 && notePosition.length > 0) {
const { x, y, width, height } = JSON.parse(notePosition);
const addBadgeEvent = new CustomEvent('addBadge.imageDiff', {
detail: {
x,
y,
width,
height,
noteId: `note_${note.id}`,
discussionId: note.discussion_id,
},
});
$diffFile[0].dispatchEvent(addBadgeEvent);
}
}
// append flash-container to the Notes list // append flash-container to the Notes list
if ($notesContainer.length) { if ($notesContainer.length) {
...@@ -1457,6 +1555,16 @@ export default class Notes { ...@@ -1457,6 +1555,16 @@ export default class Notes {
// Submission failed, remove placeholder note and show Flash error message // Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${noteUniqueId}`).remove(); $notesContainer.find(`#${noteUniqueId}`).remove();
const blurEvent = new CustomEvent('blur.imageDiff', {
detail: e,
});
const closestDiffFile = $form.closest('.diff-file');
if (closestDiffFile.length) {
closestDiffFile[0].dispatchEvent(blurEvent);
}
if (hasQuickActions) { if (hasQuickActions) {
$notesContainer.find(`#${systemNoteUniqueId}`).remove(); $notesContainer.find(`#${systemNoteUniqueId}`).remove();
} }
...@@ -1500,6 +1608,8 @@ export default class Notes { ...@@ -1500,6 +1608,8 @@ export default class Notes {
const $noteBody = $editingNote.find('.js-task-list-container'); const $noteBody = $editingNote.find('.js-task-list-container');
const $noteBodyText = $noteBody.find('.note-text'); const $noteBodyText = $noteBody.find('.note-text');
const { formData, formContent, formAction } = this.getFormData($form); const { formData, formContent, formAction } = this.getFormData($form);
const $diffFile = $form.closest('.diff-file');
const $notesContainer = $form.closest('.notes');
// Cache original comment content // Cache original comment content
const cachedNoteBodyText = $noteBodyText.html(); const cachedNoteBodyText = $noteBodyText.html();
......
...@@ -7,10 +7,12 @@ ...@@ -7,10 +7,12 @@
import TaskList from '../../task_list'; import TaskList from '../../task_list';
import * as constants from '../constants'; import * as constants from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
import issueDiscussionLockedWidget from './issue_discussion_locked_widget.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import issuableStateMixin from '../mixins/issuable_state';
export default { export default {
name: 'issueCommentForm', name: 'issueCommentForm',
...@@ -26,8 +28,9 @@ ...@@ -26,8 +28,9 @@
}; };
}, },
components: { components: {
confidentialIssue, issueWarning,
issueNoteSignedOutWidget, issueNoteSignedOutWidget,
issueDiscussionLockedWidget,
markdownField, markdownField,
userAvatarLink, userAvatarLink,
}, },
...@@ -55,6 +58,9 @@ ...@@ -55,6 +58,9 @@
isIssueOpen() { isIssueOpen() {
return this.issueState === constants.OPENED || this.issueState === constants.REOPENED; return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
}, },
canCreateNote() {
return this.getIssueData.current_user.can_create_note;
},
issueActionButtonTitle() { issueActionButtonTitle() {
if (this.note.length) { if (this.note.length) {
const actionText = this.isIssueOpen ? 'close' : 'reopen'; const actionText = this.isIssueOpen ? 'close' : 'reopen';
...@@ -90,9 +96,6 @@ ...@@ -90,9 +96,6 @@
endpoint() { endpoint() {
return this.getIssueData.create_note_path; return this.getIssueData.create_note_path;
}, },
isConfidentialIssue() {
return this.getIssueData.confidential;
},
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -220,6 +223,9 @@ ...@@ -220,6 +223,9 @@
}); });
}, },
}, },
mixins: [
issuableStateMixin,
],
mounted() { mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery. // jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => { $(document).on('issuable:change', (e, isClosed) => {
...@@ -235,6 +241,7 @@ ...@@ -235,6 +241,7 @@
<template> <template>
<div> <div>
<issue-note-signed-out-widget v-if="!isLoggedIn" /> <issue-note-signed-out-widget v-if="!isLoggedIn" />
<issue-discussion-locked-widget v-else-if="!canCreateNote" />
<ul <ul
v-else v-else
class="notes notes-form timeline"> class="notes notes-form timeline">
...@@ -253,15 +260,22 @@ ...@@ -253,15 +260,22 @@
<div class="timeline-content timeline-content-form"> <div class="timeline-content timeline-content-form">
<form <form
ref="commentForm" ref="commentForm"
class="new-note js-quick-submit common-note-form gfm-form js-main-target-form"> class="new-note js-quick-submit common-note-form gfm-form js-main-target-form"
<confidentialIssue v-if="isConfidentialIssue" /> >
<div class="error-alert"></div> <div class="error-alert"></div>
<issue-warning
v-if="hasWarning(getIssueData)"
:is-locked="isLocked(getIssueData)"
:is-confidential="isConfidential(getIssueData)"
/>
<markdown-field <markdown-field
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath" :quick-actions-docs-path="quickActionsDocsPath"
:add-spacing-classes="false" :add-spacing-classes="false"
:is-confidential-issue="isConfidentialIssue"
ref="markdownField"> ref="markdownField">
<textarea <textarea
id="note-body" id="note-body"
......
<script>
export default {
computed: {
lockIcon() {
return gl.utils.spriteIcon('lock');
},
},
};
</script>
<template>
<div class="disabled-comment text-center">
<span class="issuable-note-warning">
<span class="icon" v-html="lockIcon"></span>
<span>This issue is locked. Only <b>project members</b> can comment.</span>
</span>
</div>
</template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state';
export default { export default {
name: 'issueNoteForm', name: 'issueNoteForm',
...@@ -39,12 +40,13 @@ ...@@ -39,12 +40,13 @@
}; };
}, },
components: { components: {
confidentialIssue, issueWarning,
markdownField, markdownField,
}, },
computed: { computed: {
...mapGetters([ ...mapGetters([
'getDiscussionLastNote', 'getDiscussionLastNote',
'getIssueData',
'getIssueDataByProp', 'getIssueDataByProp',
'getNotesDataByProp', 'getNotesDataByProp',
'getUserDataByProp', 'getUserDataByProp',
...@@ -67,9 +69,6 @@ ...@@ -67,9 +69,6 @@
isDisabled() { isDisabled() {
return !this.note.length || this.isSubmitting; return !this.note.length || this.isSubmitting;
}, },
isConfidentialIssue() {
return this.getIssueDataByProp('confidential');
},
}, },
methods: { methods: {
handleUpdate() { handleUpdate() {
...@@ -95,6 +94,9 @@ ...@@ -95,6 +94,9 @@
this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note); this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note);
}, },
}, },
mixins: [
issuableStateMixin,
],
mounted() { mounted() {
this.$refs.textarea.focus(); this.$refs.textarea.focus();
}, },
...@@ -125,7 +127,13 @@ ...@@ -125,7 +127,13 @@
<div class="flash-container timeline-content"></div> <div class="flash-container timeline-content"></div>
<form <form
class="edit-note common-note-form js-quick-submit gfm-form"> class="edit-note common-note-form js-quick-submit gfm-form">
<confidentialIssue v-if="isConfidentialIssue" />
<issue-warning
v-if="hasWarning(getIssueData)"
:is-locked="isLocked(getIssueData)"
:is-confidential="isConfidential(getIssueData)"
/>
<markdown-field <markdown-field
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
......
export default {
methods: {
isConfidential(issue) {
return !!issue.confidential;
},
isLocked(issue) {
return !!issue.discussion_locked;
},
hasWarning(issue) {
return this.isConfidential(issue) || this.isLocked(issue);
},
},
};
...@@ -72,6 +72,13 @@ ...@@ -72,6 +72,13 @@
:title="pipeline.yaml_errors"> :title="pipeline.yaml_errors">
yaml invalid yaml invalid
</span> </span>
<span
v-if="pipeline.flags.failure_reason"
v-tooltip
class="js-pipeline-url-failure label label-danger"
:title="pipeline.failure_reason">
error
</span>
<a <a
v-if="pipeline.flags.auto_devops" v-if="pipeline.flags.auto_devops"
tabindex="0" tabindex="0"
......
<script>
import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
import { __, s__, sprintf } from '../../../locale';
import csrf from '../../../lib/utils/csrf';
export default {
props: {
actionUrl: {
type: String,
required: true,
},
confirmWithPassword: {
type: Boolean,
required: true,
},
username: {
type: String,
required: true,
},
},
data() {
return {
enteredPassword: '',
enteredUsername: '',
isOpen: false,
};
},
components: {
popupDialog,
},
computed: {
csrfToken() {
return csrf.token;
},
inputLabel() {
let confirmationValue;
if (this.confirmWithPassword) {
confirmationValue = __('password');
} else {
confirmationValue = __('username');
}
confirmationValue = `<code>${confirmationValue}</code>`;
return sprintf(
s__('Profiles|Type your %{confirmationValue} to confirm:'),
{ confirmationValue },
false,
);
},
text() {
return sprintf(
s__(`Profiles|
You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account.
Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
{
yourAccount: `<strong>${s__('Profiles|your account')}</strong>`,
deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`,
},
false,
);
},
},
methods: {
canSubmit() {
if (this.confirmWithPassword) {
return this.enteredPassword !== '';
}
return this.enteredUsername === this.username;
},
onSubmit(status) {
if (status) {
if (!this.canSubmit()) {
return;
}
this.$refs.form.submit();
}
this.toggleOpen(false);
},
toggleOpen(isOpen) {
this.isOpen = isOpen;
},
},
};
</script>
<template>
<div>
<popup-dialog
v-if="isOpen"
:title="s__('Profiles|Delete your account?')"
:text="text"
:kind="`danger ${!canSubmit() && 'disabled'}`"
:primary-button-label="s__('Profiles|Delete account')"
@toggle="toggleOpen"
@submit="onSubmit">
<template slot="body" scope="props">
<p v-html="props.text"></p>
<form
ref="form"
:action="actionUrl"
method="post">
<input
type="hidden"
name="_method"
value="delete" />
<input
type="hidden"
name="authenticity_token"
:value="csrfToken" />
<p id="input-label" v-html="inputLabel"></p>
<input
v-if="confirmWithPassword"
name="password"
class="form-control"
type="password"
v-model="enteredPassword"
aria-labelledby="input-label" />
<input
v-else
name="username"
class="form-control"
type="text"
v-model="enteredUsername"
aria-labelledby="input-label" />
</form>
</template>
</popup-dialog>
<button
type="button"
class="btn btn-danger"
@click="toggleOpen(true)">
{{ s__('Profiles|Delete account') }}
</button>
</div>
</template>
import Vue from 'vue';
import deleteAccountModal from './components/delete_account_modal.vue';
const deleteAccountModalEl = document.getElementById('delete-account-modal');
// eslint-disable-next-line no-new
new Vue({
el: deleteAccountModalEl,
components: {
deleteAccountModal,
},
render(createElement) {
return createElement('delete-account-modal', {
props: {
actionUrl: deleteAccountModalEl.dataset.actionUrl,
confirmWithPassword: !!deleteAccountModalEl.dataset.confirmWithPassword,
username: deleteAccountModalEl.dataset.username,
},
});
},
});
export default () => { export default () => {
$('.fork-thumbnail a').on('click', function forkThumbnailClicked() { $('.js-fork-thumbnail').on('click', function forkThumbnailClicked() {
if ($(this).hasClass('disabled')) return false; if ($(this).hasClass('disabled')) return false;
$('.fork-namespaces').hide(); return $('.js-fork-content').toggle();
return $('.save-project-loader').show();
}); });
}; };
import _ from 'underscore';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
import ProtectedBranchDropdown from './protected_branch_dropdown'; import ProtectedBranchDropdown from './protected_branch_dropdown';
import AccessorUtilities from '../lib/utils/accessor';
const PB_LOCAL_STORAGE_KEY = 'protected-branches-defaults';
export default class ProtectedBranchCreate { export default class ProtectedBranchCreate {
constructor() { constructor() {
this.$form = $('.js-new-protected-branch'); this.$form = $('.js-new-protected-branch');
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.currentProjectUserDefaults = {};
this.buildDropdowns(); this.buildDropdowns();
} }
buildDropdowns() { buildDropdowns() {
const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge'); const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge');
const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push'); const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push');
const $protectedBranchDropdown = this.$form.find('.js-protected-branch-select');
// Cache callback // Cache callback
this.onSelectCallback = this.onSelect.bind(this); this.onSelectCallback = this.onSelect.bind(this);
...@@ -28,15 +35,13 @@ export default class ProtectedBranchCreate { ...@@ -28,15 +35,13 @@ export default class ProtectedBranchCreate {
onSelect: this.onSelectCallback, onSelect: this.onSelectCallback,
}); });
// Select default
$allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0);
$allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0);
// Protected branch dropdown // Protected branch dropdown
this.protectedBranchDropdown = new ProtectedBranchDropdown({ this.protectedBranchDropdown = new ProtectedBranchDropdown({
$dropdown: this.$form.find('.js-protected-branch-select'), $dropdown: $protectedBranchDropdown,
onSelect: this.onSelectCallback, onSelect: this.onSelectCallback,
}); });
this.loadPreviousSelection($allowedToMergeDropdown.data('glDropdown'), $allowedToPushDropdown.data('glDropdown'));
} }
// This will run after clicked callback // This will run after clicked callback
...@@ -45,7 +50,41 @@ export default class ProtectedBranchCreate { ...@@ -45,7 +50,41 @@ export default class ProtectedBranchCreate {
const $branchInput = this.$form.find('input[name="protected_branch[name]"]'); const $branchInput = this.$form.find('input[name="protected_branch[name]"]');
const $allowedToMergeInput = this.$form.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]'); const $allowedToMergeInput = this.$form.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
const $allowedToPushInput = this.$form.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]'); const $allowedToPushInput = this.$form.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
const completedForm = !(
$branchInput.val() &&
$allowedToMergeInput.length &&
$allowedToPushInput.length
);
this.savePreviousSelection($allowedToMergeInput.val(), $allowedToPushInput.val());
this.$form.find('input[type="submit"]').attr('disabled', completedForm);
}
loadPreviousSelection(mergeDropdown, pushDropdown) {
let mergeIndex = 0;
let pushIndex = 0;
if (this.isLocalStorageAvailable) {
const savedDefaults = JSON.parse(window.localStorage.getItem(PB_LOCAL_STORAGE_KEY));
if (savedDefaults != null) {
mergeIndex = _.findLastIndex(mergeDropdown.fullData.roles, {
id: parseInt(savedDefaults.mergeSelection, 0),
});
pushIndex = _.findLastIndex(pushDropdown.fullData.roles, {
id: parseInt(savedDefaults.pushSelection, 0),
});
}
}
mergeDropdown.selectRowAtIndex(mergeIndex);
pushDropdown.selectRowAtIndex(pushIndex);
}
this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length)); savePreviousSelection(mergeSelection, pushSelection) {
if (this.isLocalStorageAvailable) {
const branchDefaults = {
mergeSelection,
pushSelection,
};
window.localStorage.setItem(PB_LOCAL_STORAGE_KEY, JSON.stringify(branchDefaults));
}
} }
} }
...@@ -62,7 +62,7 @@ export default { ...@@ -62,7 +62,7 @@ export default {
:primary-button-label="__('Discard changes')" :primary-button-label="__('Discard changes')"
kind="warning" kind="warning"
:title="__('Are you sure?')" :title="__('Are you sure?')"
:body="__('Are you sure you want to discard your changes?')" :text="__('Are you sure you want to discard your changes?')"
@toggle="toggleDialogOpen" @toggle="toggleDialogOpen"
@submit="dialogSubmitted" @submit="dialogSubmitted"
/> />
......
...@@ -63,12 +63,7 @@ const RepoEditor = { ...@@ -63,12 +63,7 @@ const RepoEditor = {
const lineNumber = e.target.position.lineNumber; const lineNumber = e.target.position.lineNumber;
if (e.target.element.classList.contains('line-numbers')) { if (e.target.element.classList.contains('line-numbers')) {
location.hash = `L${lineNumber}`; location.hash = `L${lineNumber}`;
Store.activeLine = lineNumber; Store.setActiveLine(lineNumber);
Helper.monacoInstance.setPosition({
lineNumber: this.activeLine,
column: 1,
});
} }
}, },
}, },
...@@ -101,6 +96,15 @@ const RepoEditor = { ...@@ -101,6 +96,15 @@ const RepoEditor = {
this.setupEditor(); this.setupEditor();
} }
}, },
activeLine() {
if (Helper.monacoInstance) {
Helper.monacoInstance.setPosition({
lineNumber: this.activeLine,
column: 1,
});
}
},
}, },
computed: { computed: {
shouldHideEditor() { shouldHideEditor() {
......
...@@ -14,6 +14,11 @@ export default { ...@@ -14,6 +14,11 @@ export default {
highlightFile() { highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight(); $(this.$el).find('.file-content').syntaxHighlight();
}, },
highlightLine() {
if (Store.activeLine > -1) {
this.lineHighlighter.highlightHash(`#L${Store.activeLine}`);
}
},
}, },
mounted() { mounted() {
this.highlightFile(); this.highlightFile();
...@@ -26,8 +31,12 @@ export default { ...@@ -26,8 +31,12 @@ export default {
html() { html() {
this.$nextTick(() => { this.$nextTick(() => {
this.highlightFile(); this.highlightFile();
this.highlightLine();
}); });
}, },
activeLine() {
this.highlightLine();
},
}, },
}; };
</script> </script>
......
...@@ -18,22 +18,40 @@ export default { ...@@ -18,22 +18,40 @@ export default {
}, },
created() { created() {
this.addPopEventListener(); window.addEventListener('popstate', this.checkHistory);
},
destroyed() {
window.removeEventListener('popstate', this.checkHistory);
}, },
data: () => Store, data: () => Store,
methods: { methods: {
addPopEventListener() { checkHistory() {
window.addEventListener('popstate', () => { let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1);
if (location.href.indexOf('#') > -1) return; if (!selectedFile) {
this.linkClicked({ // Maybe it is not in the current tree but in the opened tabs
selectedFile = Helper.getFileFromPath(location.pathname);
}
let lineNumber = null;
if (location.hash.indexOf('#L') > -1) lineNumber = Number(location.hash.substr(2));
if (selectedFile) {
if (selectedFile.url !== this.activeFile.url) {
this.fileClicked(selectedFile, lineNumber);
} else {
Store.setActiveLine(lineNumber);
}
} else {
// Not opened at all lets open new tab
this.fileClicked({
url: location.href, url: location.href,
}); }, lineNumber);
}); }
}, },
fileClicked(clickedFile) { fileClicked(clickedFile, lineNumber) {
let file = clickedFile; let file = clickedFile;
if (file.loading) return; if (file.loading) return;
file.loading = true; file.loading = true;
...@@ -41,17 +59,20 @@ export default { ...@@ -41,17 +59,20 @@ export default {
if (file.type === 'tree' && file.opened) { if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file); file = Store.removeChildFilesOfTree(file);
file.loading = false; file.loading = false;
Store.setActiveLine(lineNumber);
} else { } else {
const openFile = Helper.getFileFromPath(file.url); const openFile = Helper.getFileFromPath(file.url);
if (openFile) { if (openFile) {
file.loading = false; file.loading = false;
Store.setActiveFiles(openFile); Store.setActiveFiles(openFile);
Store.setActiveLine(lineNumber);
} else { } else {
Service.url = file.url; Service.url = file.url;
Helper.getContent(file) Helper.getContent(file)
.then(() => { .then(() => {
file.loading = false; file.loading = false;
Helper.scrollTabsRight(); Helper.scrollTabsRight();
Store.setActiveLine(lineNumber);
}) })
.catch(Helper.loadingError); .catch(Helper.loadingError);
} }
...@@ -74,8 +95,8 @@ export default { ...@@ -74,8 +95,8 @@ export default {
<thead v-if="!isMini"> <thead v-if="!isMini">
<tr> <tr>
<th class="name">Name</th> <th class="name">Name</th>
<th class="hidden-sm hidden-xs last-commit">Last Commit</th> <th class="hidden-sm hidden-xs last-commit">Last commit</th>
<th class="hidden-xs last-update text-right">Last Update</th> <th class="hidden-xs last-update text-right">Last update</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
......
...@@ -254,7 +254,9 @@ const RepoHelper = { ...@@ -254,7 +254,9 @@ const RepoHelper = {
RepoHelper.key = RepoHelper.genKey(); RepoHelper.key = RepoHelper.genKey();
history.pushState({ key: RepoHelper.key }, '', url); if (document.location.pathname !== url) {
history.pushState({ key: RepoHelper.key }, '', url);
}
if (title) { if (title) {
document.title = title; document.title = title;
......
...@@ -26,7 +26,7 @@ const RepoStore = { ...@@ -26,7 +26,7 @@ const RepoStore = {
}, },
activeFile: Helper.getDefaultActiveFile(), activeFile: Helper.getDefaultActiveFile(),
activeFileIndex: 0, activeFileIndex: 0,
activeLine: 0, activeLine: -1,
activeFileLabel: 'Raw', activeFileLabel: 'Raw',
files: [], files: [],
isCommitable: false, isCommitable: false,
...@@ -85,6 +85,7 @@ const RepoStore = { ...@@ -85,6 +85,7 @@ const RepoStore = {
if (!file.loading) Helper.updateHistoryEntry(file.url, file.pageTitle || file.name); if (!file.loading) Helper.updateHistoryEntry(file.url, file.pageTitle || file.name);
RepoStore.binary = file.binary; RepoStore.binary = file.binary;
RepoStore.setActiveLine(-1);
}, },
setFileActivity(file, openedFile, i) { setFileActivity(file, openedFile, i) {
...@@ -101,6 +102,10 @@ const RepoStore = { ...@@ -101,6 +102,10 @@ const RepoStore = {
RepoStore.activeFileIndex = i; RepoStore.activeFileIndex = i;
}, },
setActiveLine(activeLine) {
if (!isNaN(activeLine)) RepoStore.activeLine = activeLine;
},
setActiveToRaw() { setActiveToRaw() {
RepoStore.activeFile.raw = false; RepoStore.activeFile.raw = false;
// can't get vue to listen to raw for some reason so RepoStore for now. // can't get vue to listen to raw for some reason so RepoStore for now.
......
...@@ -47,9 +47,9 @@ export default { ...@@ -47,9 +47,9 @@ export default {
</script> </script>
<template> <template>
<div class="block confidentiality"> <div class="block issuable-sidebar-item confidentiality">
<div class="sidebar-collapsed-icon"> <div class="sidebar-collapsed-icon">
<i class="fa" :class="faEye" aria-hidden="true" data-hidden="true"></i> <i class="fa" :class="faEye" aria-hidden="true"></i>
</div> </div>
<div class="title hide-collapsed"> <div class="title hide-collapsed">
Confidentiality Confidentiality
...@@ -62,19 +62,19 @@ export default { ...@@ -62,19 +62,19 @@ export default {
Edit Edit
</a> </a>
</div> </div>
<div class="value confidential-value hide-collapsed"> <div class="value sidebar-item-value hide-collapsed">
<editForm <editForm
v-if="edit" v-if="edit"
:toggle-form="toggleForm" :toggle-form="toggleForm"
:is-confidential="isConfidential" :is-confidential="isConfidential"
:update-confidential-attribute="updateConfidentialAttribute" :update-confidential-attribute="updateConfidentialAttribute"
/> />
<div v-if="!isConfidential" class="no-value confidential-value"> <div v-if="!isConfidential" class="no-value sidebar-item-value">
<i class="fa fa-eye is-not-confidential"></i> <i class="fa fa-eye sidebar-item-icon"></i>
Not confidential Not confidential
</div> </div>
<div v-else class="value confidential-value hide-collapsed"> <div v-else class="value sidebar-item-value hide-collapsed">
<i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i> <i aria-hidden="true" class="fa fa-eye-slash sidebar-item-icon is-active"></i>
This issue is confidential This issue is confidential
</div> </div>
</div> </div>
......
...@@ -2,9 +2,6 @@ ...@@ -2,9 +2,6 @@
import editFormButtons from './edit_form_buttons.vue'; import editFormButtons from './edit_form_buttons.vue';
export default { export default {
components: {
editFormButtons,
},
props: { props: {
isConfidential: { isConfidential: {
required: true, required: true,
...@@ -19,12 +16,16 @@ export default { ...@@ -19,12 +16,16 @@ export default {
type: Function, type: Function,
}, },
}, },
components: {
editFormButtons,
},
}; };
</script> </script>
<template> <template>
<div class="dropdown open"> <div class="dropdown open">
<div class="dropdown-menu confidential-warning-message"> <div class="dropdown-menu sidebar-item-warning-message">
<div> <div>
<p v-if="!isConfidential"> <p v-if="!isConfidential">
You are going to turn on the confidentiality. This means that only team members with You are going to turn on the confidentiality. This means that only team members with
......
...@@ -15,7 +15,7 @@ export default { ...@@ -15,7 +15,7 @@ export default {
}, },
}, },
computed: { computed: {
onOrOff() { toggleButtonText() {
return this.isConfidential ? 'Turn Off' : 'Turn On'; return this.isConfidential ? 'Turn Off' : 'Turn On';
}, },
updateConfidentialBool() { updateConfidentialBool() {
...@@ -26,7 +26,7 @@ export default { ...@@ -26,7 +26,7 @@ export default {
</script> </script>
<template> <template>
<div class="confidential-warning-message-actions"> <div class="sidebar-item-warning-message-actions">
<button <button
type="button" type="button"
class="btn btn-default append-right-10" class="btn btn-default append-right-10"
...@@ -39,7 +39,7 @@ export default { ...@@ -39,7 +39,7 @@ export default {
class="btn btn-close" class="btn btn-close"
@click.prevent="updateConfidentialAttribute(updateConfidentialBool)" @click.prevent="updateConfidentialAttribute(updateConfidentialBool)"
> >
{{ onOrOff }} {{ toggleButtonText }}
</button> </button>
</div> </div>
</template> </template>
<script>
import editFormButtons from './edit_form_buttons.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
export default {
props: {
isLocked: {
required: true,
type: Boolean,
},
toggleForm: {
required: true,
type: Function,
},
updateLockedAttribute: {
required: true,
type: Function,
},
issuableType: {
required: true,
type: String,
},
},
mixins: [
issuableMixin,
],
components: {
editFormButtons,
},
};
</script>
<template>
<div class="dropdown open">
<div class="dropdown-menu sidebar-item-warning-message">
<p class="text" v-if="isLocked">
Unlock this {{ issuableDisplayName(issuableType) }}?
<strong>Everyone</strong>
will be able to comment.
</p>
<p class="text" v-else>
Lock this {{ issuableDisplayName(issuableType) }}?
Only
<strong>project members</strong>
will be able to comment.
</p>
<edit-form-buttons
:is-locked="isLocked"
:toggle-form="toggleForm"
:update-locked-attribute="updateLockedAttribute"
/>
</div>
</div>
</template>
<script>
export default {
props: {
isLocked: {
required: true,
type: Boolean,
},
toggleForm: {
required: true,
type: Function,
},
updateLockedAttribute: {
required: true,
type: Function,
},
},
computed: {
buttonText() {
return this.isLocked ? this.__('Unlock') : this.__('Lock');
},
toggleLock() {
return !this.isLocked;
},
},
};
</script>
<template>
<div class="sidebar-item-warning-message-actions">
<button
type="button"
class="btn btn-default append-right-10"
@click="toggleForm"
>
{{ __('Cancel') }}
</button>
<button
type="button"
class="btn btn-close"
@click.prevent="updateLockedAttribute(toggleLock)"
>
{{ buttonText }}
</button>
</div>
</template>
<script>
/* global Flash */
import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
export default {
props: {
isLocked: {
required: true,
type: Boolean,
},
isEditable: {
required: true,
type: Boolean,
},
mediator: {
required: true,
type: Object,
validator(mediatorObject) {
return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
},
},
issuableType: {
required: true,
type: String,
},
},
mixins: [
issuableMixin,
],
components: {
editForm,
},
computed: {
lockIconClass() {
return this.isLocked ? 'fa-lock' : 'fa-unlock';
},
isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen;
},
},
methods: {
toggleForm() {
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
},
updateLockedAttribute(locked) {
this.mediator.service.update(this.issuableType, {
discussion_locked: locked,
})
.then(() => location.reload())
.catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}`)));
},
},
};
</script>
<template>
<div class="block issuable-sidebar-item lock">
<div class="sidebar-collapsed-icon">
<i
class="fa"
:class="lockIconClass"
aria-hidden="true"
></i>
</div>
<div class="title hide-collapsed">
Lock {{issuableDisplayName(issuableType) }}
<button
v-if="isEditable"
class="pull-right lock-edit btn btn-blank"
type="button"
@click.prevent="toggleForm"
>
{{ __('Edit') }}
</button>
</div>
<div class="value sidebar-item-value hide-collapsed">
<edit-form
v-if="isLockDialogOpen"
:toggle-form="toggleForm"
:is-locked="isLocked"
:update-locked-attribute="updateLockedAttribute"
:issuable-type="issuableType"
/>
<div
v-if="isLocked"
class="value sidebar-item-value"
>
<i
aria-hidden="true"
class="fa fa-lock sidebar-item-icon is-active"
></i>
{{ __('Locked') }}
</div>
<div
v-else
class="no-value sidebar-item-value hide-collapsed"
>
<i
aria-hidden="true"
class="fa fa-unlock sidebar-item-icon"
></i>
{{ __('Unlocked') }}
</div>
</div>
</div>
</template>
import Vue from 'vue'; import Vue from 'vue';
import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking'; import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
import sidebarAssignees from './components/assignees/sidebar_assignees'; import SidebarAssignees from './components/assignees/sidebar_assignees';
import confidential from './components/confidential/confidential_issue_sidebar.vue'; import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue'; import SidebarMoveIssue from './lib/sidebar_move_issue';
import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
import Translate from '../vue_shared/translate';
import Mediator from './sidebar_mediator'; import Mediator from './sidebar_mediator';
Vue.use(Translate);
function mountConfidentialComponent(mediator) {
const el = document.getElementById('js-confidential-entry-point');
if (!el) return;
const dataNode = document.getElementById('js-confidential-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar);
new ConfidentialComp({
propsData: {
isConfidential: initialData.is_confidential,
isEditable: initialData.is_editable,
service: mediator.service,
},
}).$mount(el);
}
function mountLockComponent(mediator) {
const el = document.getElementById('js-lock-entry-point');
if (!el) return;
const dataNode = document.getElementById('js-lock-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
const LockComp = Vue.extend(LockIssueSidebar);
new LockComp({
propsData: {
isLocked: initialData.is_locked,
isEditable: initialData.is_editable,
mediator,
issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
},
}).$mount(el);
}
function domContentLoaded() { function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions); const mediator = new Mediator(sidebarOptions);
mediator.fetch(); mediator.fetch();
const sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees'); const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees');
const confidentialEl = document.querySelector('#js-confidential-entry-point');
// Only create the sidebarAssignees vue app if it is found in the DOM // Only create the sidebarAssignees vue app if it is found in the DOM
// We currently do not use sidebarAssignees for the MR page // We currently do not use sidebarAssignees for the MR page
if (sidebarAssigneesEl) { if (sidebarAssigneesEl) {
new Vue(sidebarAssignees).$mount(sidebarAssigneesEl); new Vue(SidebarAssignees).$mount(sidebarAssigneesEl);
} }
if (confidentialEl) { mountConfidentialComponent(mediator);
const dataNode = document.getElementById('js-confidential-issue-data'); mountLockComponent(mediator);
const initialData = JSON.parse(dataNode.innerHTML);
const ConfidentialComp = Vue.extend(confidential); new SidebarMoveIssue(
mediator,
new ConfidentialComp({ $('.js-move-issue'),
propsData: { $('.js-move-issue-confirmation-button'),
isConfidential: initialData.is_confidential, ).init();
isEditable: initialData.is_editable,
service: mediator.service,
},
}).$mount(confidentialEl);
new SidebarMoveIssue(
mediator,
$('.js-move-issue'),
$('.js-move-issue-confirmation-button'),
).init();
}
new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker'); new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker');
} }
document.addEventListener('DOMContentLoaded', domContentLoaded); document.addEventListener('DOMContentLoaded', domContentLoaded);
......
...@@ -15,6 +15,7 @@ export default class SidebarStore { ...@@ -15,6 +15,7 @@ export default class SidebarStore {
}; };
this.autocompleteProjects = []; this.autocompleteProjects = [];
this.moveToProjectId = 0; this.moveToProjectId = 0;
this.isLockDialogOpen = false;
SidebarStore.singleton = this; SidebarStore.singleton = this;
} }
......
/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */ /* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
import FilesCommentButton from './files_comment_button'; import FilesCommentButton from './files_comment_button';
import imageDiffHelper from './image_diff/helpers/index';
const WRAPPER = '<div class="diff-content"></div>'; const WRAPPER = '<div class="diff-content"></div>';
const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'; const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
...@@ -74,7 +75,11 @@ export default class SingleFileDiff { ...@@ -74,7 +75,11 @@ export default class SingleFileDiff {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
} }
FilesCommentButton.init($(_this.file)); const $file = $(_this.file);
FilesCommentButton.init($file);
const canCreateNote = $file.closest('.files').is('[data-can-create-note]');
imageDiffHelper.initImageDiff($file[0], canCreateNote);
if (cb) cb(); if (cb) cb();
}; };
......
...@@ -38,24 +38,40 @@ export default { ...@@ -38,24 +38,40 @@ export default {
return this.useCommitMessageWithDescription ? withoutDesc : withDesc; return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
}, },
mergeButtonClass() { status() {
const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr; const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
if (hasCI && !ciStatus) { if (hasCI && !ciStatus) {
return failedClass; return 'failed';
} else if (!pipeline) { } else if (!pipeline) {
return defaultClass; return 'success';
} else if (isPipelineActive) { } else if (isPipelineActive) {
return inActionClass; return 'pending';
} else if (isPipelineFailed) { } else if (isPipelineFailed) {
return 'failed';
}
return 'success';
},
mergeButtonClass() {
const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
if (this.status === 'failed') {
return failedClass; return failedClass;
} else if (this.status === 'pending') {
return inActionClass;
} }
return defaultClass; return defaultClass;
}, },
iconClass() {
if (this.status === 'failed' || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge) {
return 'failed';
}
return 'success';
},
mergeButtonText() { mergeButtonText() {
if (this.isMergingImmediately) { if (this.isMergingImmediately) {
return 'Merge in progress'; return 'Merge in progress';
...@@ -84,13 +100,8 @@ export default { ...@@ -84,13 +100,8 @@ export default {
}, },
}, },
methods: { methods: {
isMergeAllowed() {
return !this.mr.onlyAllowMergeIfPipelineSucceeds ||
this.mr.isPipelinePassing ||
this.mr.isPipelineSkipped;
},
shouldShowMergeControls() { shouldShowMergeControls() {
return this.isMergeAllowed() || this.shouldShowMergeWhenPipelineSucceedsText; return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText;
}, },
updateCommitMessage() { updateCommitMessage() {
const cmwd = this.mr.commitMessageWithDescription; const cmwd = this.mr.commitMessageWithDescription;
...@@ -209,7 +220,7 @@ export default { ...@@ -209,7 +220,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon status="success" /> <status-icon :status="iconClass" />
<div class="media-body"> <div class="media-body">
<div class="mr-widget-body-controls media space-children"> <div class="mr-widget-body-controls media space-children">
<span class="btn-group append-bottom-5"> <span class="btn-group append-bottom-5">
......
...@@ -73,6 +73,7 @@ export default class MergeRequestStore { ...@@ -73,6 +73,7 @@ export default class MergeRequestStore {
this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path; this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path;
this.hasSHAChanged = this.sha !== data.diff_head_sha; this.hasSHAChanged = this.sha !== data.diff_head_sha;
this.canBeMerged = data.can_be_merged || false; this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing; this.mergeOngoing = data.merge_ongoing;
// Cherry-pick and Revert actions related // Cherry-pick and Revert actions related
......
<script>
export default {
name: 'confidentialIssueWarning',
};
</script>
<template>
<div class="confidential-issue-warning">
<i
aria-hidden="true"
class="fa fa-eye-slash">
</i>
<span>
This is a confidential issue. Your comment will not be visible to the public.
</span>
</div>
</template>
<script>
export default {
props: {
isLocked: {
type: Boolean,
default: false,
required: false,
},
isConfidential: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
iconClass() {
return {
'fa-eye-slash': this.isConfidential,
'fa-lock': this.isLocked,
};
},
isLockedAndConfidential() {
return this.isConfidential && this.isLocked;
},
},
};
</script>
<template>
<div class="issuable-note-warning">
<i
aria-hidden="true"
class="fa icon"
:class="iconClass"
v-if="!isLockedAndConfidential"
></i>
<span v-if="isLockedAndConfidential">
{{ __('This issue is confidential and locked.') }}
{{ __('People without permission will never get a notification and won\'t be able to comment.') }}
</span>
<span v-else-if="isConfidential">
{{ __('This is a confidential issue.') }}
{{ __('Your comment will not be visible to the public.') }}
</span>
<span v-else-if="isLocked">
{{ __('This issue is locked.') }}
{{ __('Only project members can comment.') }}
</span>
</div>
</template>
...@@ -7,7 +7,7 @@ export default { ...@@ -7,7 +7,7 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
body: { text: {
type: String, type: String,
required: true, required: true,
}, },
...@@ -63,7 +63,9 @@ export default { ...@@ -63,7 +63,9 @@ export default {
<h4 class="modal-title">{{this.title}}</h4> <h4 class="modal-title">{{this.title}}</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>{{this.body}}</p> <slot name="body" :text="text">
<p>{{text}}</p>
</slot>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button <button
......
export default {
methods: {
issuableDisplayName(issuableType) {
const displayName = issuableType.replace(/_/, ' ');
return this.__ ? this.__(displayName) : displayName;
},
},
};
...@@ -40,6 +40,7 @@ ...@@ -40,6 +40,7 @@
@import "framework/tables"; @import "framework/tables";
@import "framework/notes"; @import "framework/notes";
@import "framework/timeline"; @import "framework/timeline";
@import "framework/tooltips";
@import "framework/typography"; @import "framework/typography";
@import "framework/zen"; @import "framework/zen";
@import "framework/blank"; @import "framework/blank";
......
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
&.s60 { @include avatar-size(60px, 12px); } &.s60 { @include avatar-size(60px, 12px); }
&.s70 { @include avatar-size(70px, 14px); } &.s70 { @include avatar-size(70px, 14px); }
&.s90 { @include avatar-size(90px, 15px); } &.s90 { @include avatar-size(90px, 15px); }
&.s100 { @include avatar-size(100px, 15px); }
&.s110 { @include avatar-size(110px, 15px); } &.s110 { @include avatar-size(110px, 15px); }
&.s140 { @include avatar-size(140px, 15px); } &.s140 { @include avatar-size(140px, 15px); }
&.s160 { @include avatar-size(160px, 20px); } &.s160 { @include avatar-size(160px, 20px); }
...@@ -78,6 +79,7 @@ ...@@ -78,6 +79,7 @@
&.s60 { font-size: 32px; line-height: 58px; } &.s60 { font-size: 32px; line-height: 58px; }
&.s70 { font-size: 34px; line-height: 70px; } &.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 88px; } &.s90 { font-size: 36px; line-height: 88px; }
&.s100 { font-size: 36px; line-height: 98px; }
&.s110 { font-size: 40px; line-height: 108px; font-weight: $gl-font-weight-normal; } &.s110 { font-size: 40px; line-height: 108px; font-weight: $gl-font-weight-normal; }
&.s140 { font-size: 72px; line-height: 138px; } &.s140 { font-size: 72px; line-height: 138px; }
&.s160 { font-size: 96px; line-height: 158px; } &.s160 { font-size: 96px; line-height: 158px; }
......
@mixin btn-comment-icon {
border-radius: 50%;
background: $white-light;
padding: 1px 5px;
font-size: 12px;
color: $blue-500;
width: 23px;
height: 23px;
border: 1px solid $blue-500;
&:hover,
&.inverted {
background: $blue-500;
border-color: $blue-600;
color: $white-light;
}
&:active {
outline: 0;
}
}
@mixin btn-default { @mixin btn-default {
border-radius: 3px; border-radius: 3px;
font-size: $gl-font-size; font-size: $gl-font-size;
...@@ -381,7 +403,11 @@ ...@@ -381,7 +403,11 @@
background: transparent; background: transparent;
border: 0; border: 0;
&:hover,
&:active,
&:focus { &:focus {
outline: 0; outline: 0;
background: transparent;
box-shadow: none;
} }
} }
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
.prepend-top-10 { margin-top: 10px; } .prepend-top-10 { margin-top: 10px; }
.prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top: 20px; } .prepend-top-20 { margin-top: 20px; }
.prepend-left-4 { margin-left: 4px; }
.prepend-left-5 { margin-left: 5px; } .prepend-left-5 { margin-left: 5px; }
.prepend-left-10 { margin-left: 10px; } .prepend-left-10 { margin-left: 10px; }
.prepend-left-default { margin-left: $gl-padding; } .prepend-left-default { margin-left: $gl-padding; }
...@@ -129,11 +130,6 @@ span.update-author { ...@@ -129,11 +130,6 @@ span.update-author {
} }
} }
.user-mention {
color: $user-mention-color;
font-weight: $gl-font-weight-bold;
}
.field_with_errors { .field_with_errors {
display: inline; display: inline;
} }
......
...@@ -6,3 +6,14 @@ ...@@ -6,3 +6,14 @@
.gfm-commit_range { .gfm-commit_range {
@extend .commit-sha; @extend .commit-sha;
} }
.gfm-project_member {
padding: 0 2px;
border-radius: #{$border-radius-default / 2};
background-color: $user-mention-bg;
&:hover {
background-color: $user-mention-bg-hover;
text-decoration: none;
}
}
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
} }
} }
.title { .navbar .title {
> a { > a {
&:hover, &:hover,
&:focus { &:focus {
......
.modal-header {
padding: #{3 * $grid-size} #{2 * $grid-size};
.page-title {
margin-top: 0;
}
}
.modal-body { .modal-body {
position: relative; position: relative;
padding: 15px; padding: #{3 * $grid-size} #{2 * $grid-size};
.form-actions { .form-actions {
margin: -$gl-padding + 1; margin: #{2 * $grid-size} #{-2 * $grid-size} #{-2 * $grid-size};
margin-top: 15px;
} }
.text-danger { .text-danger {
......
...@@ -48,31 +48,24 @@ ...@@ -48,31 +48,24 @@
} }
&:hover { &:hover {
background-color: $white-normal; border-color: $gray-darkest;
border-color: $border-white-normal;
color: $gl-text-color; color: $gl-text-color;
} }
} }
} }
.select2-drop { .select2-drop,
box-shadow: $select2-drop-shadow1 0 0 1px 0, $select2-drop-shadow2 0 2px 18px 0; .select2-drop.select2-drop-above {
border-radius: $border-radius-default; box-shadow: 0 2px 4px $dropdown-shadow-color;
border: none; border-radius: $border-radius-base;
border: 1px solid $dropdown-border-color;
min-width: 175px; min-width: 175px;
color: $gl-text-color;
} }
.select2-results .select2-result-label, .select2-drop.select2-drop-above.select2-drop-active {
.select2-more-results { border-top: 1px solid $dropdown-border-color;
padding: 10px 15px; margin-top: -6px;
}
.select2-drop {
color: $gl-grayish-blue;
}
.select2-highlighted {
background: $gl-link-color !important;
} }
.select2-results li.select2-result-with-children > .select2-result-label { .select2-results li.select2-result-with-children > .select2-result-label {
...@@ -87,13 +80,11 @@ ...@@ -87,13 +80,11 @@
} }
} }
.select2-dropdown-open { .select2-dropdown-open,
.select2-dropdown-open.select2-drop-above {
.select2-choice { .select2-choice {
border-color: $border-white-normal; border-color: $gray-darkest;
outline: 0; outline: 0;
background-image: none;
background-color: $white-dark;
box-shadow: $gl-btn-active-gradient;
} }
} }
...@@ -131,28 +122,14 @@ ...@@ -131,28 +122,14 @@
} }
} }
} }
&.select2-container-active .select2-choices,
&.select2-dropdown-open .select2-choices {
border-color: $border-white-normal;
box-shadow: $gl-btn-active-gradient;
}
} }
.select2-drop-active { .select2-drop-active {
margin-top: 6px; margin-top: $dropdown-vertical-offset;
font-size: 14px; font-size: 14px;
&.select2-drop-above {
margin-bottom: 8px;
}
.select2-results { .select2-results {
max-height: 350px; max-height: 350px;
.select2-highlighted {
background: $gl-primary;
}
} }
} }
...@@ -186,19 +163,35 @@ ...@@ -186,19 +163,35 @@
background-size: 16px 16px !important; background-size: 16px 16px !important;
} }
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-ajax-error,
.select2-results .select2-selection-limit {
background: $gray-light;
display: list-item;
padding: 10px 15px;
}
.select2-results { .select2-results {
margin: 0; margin: 0;
padding: 10px 0; padding: #{$gl-padding / 2} 0;
.select2-no-results,
.select2-searching,
.select2-ajax-error,
.select2-selection-limit {
background: transparent;
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-result-label,
.select2-more-results {
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-highlighted {
background: transparent;
color: $gl-text-color;
.select2-result-label {
background: $dropdown-item-hover-bg;
}
}
.select2-result {
padding: 0 1px;
}
} }
.ajax-users-select { .ajax-users-select {
...@@ -265,56 +258,10 @@ ...@@ -265,56 +258,10 @@
min-width: 250px !important; min-width: 250px !important;
} }
// TODO: change global style .select2-result-selectable,
.ajax-project-dropdown, .select2-result-unselectable {
.ajax-users-dropdown, .select2-match {
body[data-page="projects:edit"] #select2-drop, font-weight: $gl-font-weight-bold;
body[data-page="projects:new"] #select2-drop, text-decoration: none;
body[data-page="projects:merge_requests:edit"] #select2-drop,
body[data-page="projects:blob:new"] #select2-drop,
body[data-page="profiles:show"] #select2-drop,
body[data-page="admin:groups:show"] #select2-drop,
body[data-page="projects:issues:show"] #select2-drop,
body[data-page="projects:blob:edit"] #select2-drop {
&.select2-drop {
border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base;
color: $gl-text-color;
}
&.select2-drop-above {
border-top: none;
margin-top: -4px;
}
.select2-results {
.select2-no-results,
.select2-searching,
.select2-ajax-error,
.select2-selection-limit {
background: transparent;
}
.select2-result {
padding: 0 1px;
.select2-match {
font-weight: $gl-font-weight-bold;
text-decoration: none;
}
.select2-result-label {
padding: #{$gl-padding / 2} $gl-padding;
}
&.select2-highlighted {
background-color: transparent !important;
color: $gl-text-color;
.select2-result-label {
background-color: $dropdown-item-hover-bg;
}
}
}
} }
} }
...@@ -17,15 +17,19 @@ ...@@ -17,15 +17,19 @@
.diff-file { .diff-file {
border: 1px solid $border-color; border: 1px solid $border-color;
border-bottom: none;
margin: 0; margin: 0;
} }
&.text-file .diff-file {
border-bottom: none;
}
} }
.timeline-entry { .timeline-entry {
border-color: $white-normal; border-color: $white-normal;
color: $gl-text-color; color: $gl-text-color;
border-bottom: 1px solid $border-white-light; border-bottom: 1px solid $border-white-light;
background: $white-light;
.timeline-entry-inner { .timeline-entry-inner {
position: relative; position: relative;
......
.tooltip-inner {
font-size: $tooltip-font-size;
border-radius: $border-radius-default;
line-height: 16px;
font-weight: $gl-font-weight-normal;
padding: $gl-btn-padding;
}
/* /*
* Layout * Layout
*/ */
$grid-size: 8px;
$gutter_collapsed_width: 62px; $gutter_collapsed_width: 62px;
$gutter_width: 290px; $gutter_width: 290px;
$gutter_inner_width: 250px; $gutter_inner_width: 250px;
...@@ -202,6 +203,11 @@ $md-area-border: #ddd; ...@@ -202,6 +203,11 @@ $md-area-border: #ddd;
$code_font_size: 12px; $code_font_size: 12px;
$code_line_height: 1.6; $code_line_height: 1.6;
/*
* Tooltips
*/
$tooltip-font-size: 12px;
/* /*
* Padding * Padding
*/ */
...@@ -262,7 +268,8 @@ $well-pre-bg: #eee; ...@@ -262,7 +268,8 @@ $well-pre-bg: #eee;
$well-pre-color: #555; $well-pre-color: #555;
$loading-color: #555; $loading-color: #555;
$update-author-color: #999; $update-author-color: #999;
$user-mention-color: #2fa0bb; $user-mention-bg: rgba($blue-500, 0.044);
$user-mention-bg-hover: rgba($blue-500, 0.15);
$time-color: #999; $time-color: #999;
$project-member-show-color: #aaa; $project-member-show-color: #aaa;
$gl-promo-color: #aaa; $gl-promo-color: #aaa;
...@@ -316,6 +323,7 @@ $diff-image-info-color: grey; ...@@ -316,6 +323,7 @@ $diff-image-info-color: grey;
$diff-swipe-border: #999; $diff-swipe-border: #999;
$diff-view-modes-color: grey; $diff-view-modes-color: grey;
$diff-view-modes-border: #c1c1c1; $diff-view-modes-border: #c1c1c1;
$diff-jagged-border-gradient-color: darken($white-normal, 8%);
/* /*
* Fonts * Fonts
...@@ -699,3 +707,15 @@ Project Templates Icons ...@@ -699,3 +707,15 @@ Project Templates Icons
$rails: #c00; $rails: #c00;
$node: #353535; $node: #353535;
$java: #70ad51; $java: #70ad51;
/*
Issuable warning
*/
$issuable-warning-size: 24px;
$issuable-warning-icon-margin: 4px;
/*
Image Commenting cursor
*/
$image-comment-cursor-left-offset: 12;
$image-comment-cursor-top-offset: 30;
.edit-cluster-form {
.clipboard-addon {
background-color: $white-light;
}
.alert-block {
margin-bottom: 20px;
}
}
...@@ -297,6 +297,7 @@ ...@@ -297,6 +297,7 @@
.drag-track { .drag-track {
display: block; display: block;
position: absolute; position: absolute;
top: 0;
left: 12px; left: 12px;
height: 10px; height: 10px;
width: 276px; width: 276px;
...@@ -547,16 +548,23 @@ ...@@ -547,16 +548,23 @@
} }
.diff-notes-collapse { .diff-notes-collapse {
width: 19px; width: 24px;
height: 19px; height: 24px;
border-radius: 50%;
padding: 0; padding: 0;
transition: transform .1s ease-out; transition: transform .1s ease-out;
z-index: 100; z-index: 100;
.collapse-icon {
height: 50%;
width: 100%;
}
svg { svg {
vertical-align: text-top; vertical-align: middle;
} }
.collapse-icon,
path { path {
fill: $white-light; fill: $white-light;
} }
...@@ -644,3 +652,157 @@ ...@@ -644,3 +652,157 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.note-container {
background-color: $gray-light;
border-top: 1px solid $white-normal;
// double jagged line divider
.discussion-notes + .discussion-notes::before,
.discussion-notes + .discussion-form::before {
content: '';
position: relative;
display: block;
width: 100%;
height: 10px;
background-color: $white-light;
background-image: linear-gradient(45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
linear-gradient(225deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
linear-gradient(135deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
linear-gradient(-45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%);
background-position: 5px 5px,0 5px,0 5px,5px 5px;
background-size: 10px 10px;
background-repeat: repeat;
}
.notes {
position: relative;
}
.diff-notes-collapse {
position: absolute;
left: -12px;
}
}
.diff-file .note-container > .new-note,
.note-container .discussion-notes {
margin-left: 100px;
border-left: 1px solid $white-normal;
}
.notes.active {
.diff-file .note-container > .new-note,
.note-container .discussion-notes {
// Override our margin and border (set for diff tab)
// when user is on the discussion tab for MR
margin-left: inherit;
border-left: inherit;
}
}
.files:not([data-can-create-note]) .frame {
cursor: auto;
}
.frame.click-to-comment {
position: relative;
cursor: url(icon_image_comment.svg)
$image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
// Retina cursor
cursor: -webkit-image-set(url(icon_image_comment.svg) 1x, url(icon_image_comment@2x.svg) 2x)
$image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
.comment-indicator {
position: absolute;
padding: 0;
width: (2px * $image-comment-cursor-left-offset);
height: (1px * $image-comment-cursor-top-offset);
// center the indicator to match the top left click region
margin-top: (-1px * $image-comment-cursor-top-offset) + 2;
margin-left: (-1px * $image-comment-cursor-left-offset) + 1;
svg {
width: 100%;
height: 100%;
}
&:focus {
outline: none;
}
}
}
.frame .badge,
.image-diff-avatar-link .badge,
.notes > .badge {
position: absolute;
background-color: $blue-400;
color: $white-light;
border: $white-light 1px solid;
min-height: $gl-padding;
padding: 5px 8px;
border-radius: 12px;
&:focus {
outline: none;
}
}
.frame .badge,
.frame .image-comment-badge {
// Center align badges on the frame
transform: translate3d(-50%, -50%, 0);
}
.image-comment-badge {
@include btn-comment-icon;
position: absolute;
&.inverted {
border-color: $white-light;
}
}
.image-diff-avatar-link {
position: relative;
.badge,
.image-comment-badge {
top: 25px;
right: 8px;
}
}
.notes > .badge {
display: none;
left: -13px;
}
.discussion-notes {
min-height: 35px;
&:first-child {
// First child does not have the jagged borders
min-height: 25px;
}
&.collapsed {
background-color: $white-light;
.diff-notes-collapse,
.note,
.discussion-reply-holder, {
display: none;
}
.notes > .badge {
display: block;
}
}
}
.discussion-body .image .frame {
position: relative;
}
...@@ -207,10 +207,13 @@ ...@@ -207,10 +207,13 @@
} }
.prometheus-state { .prometheus-state {
margin-top: 10px; max-width: 430px;
margin: 10px auto;
text-align: center;
.state-button-section { .state-svg {
margin-top: 10px; max-width: 80vw;
margin: 0 auto;
} }
} }
...@@ -288,8 +291,14 @@ ...@@ -288,8 +291,14 @@
fill: $black; fill: $black;
} }
.tick > text { .tick {
font-size: 12px; > line {
stroke: $gray-darker;
}
> text {
font-size: 12px;
}
} }
.text-metric-title { .text-metric-title {
......
...@@ -5,27 +5,29 @@ ...@@ -5,27 +5,29 @@
margin-right: auto; margin-right: auto;
} }
.is-confidential { .issuable-warning-icon {
color: $orange-600; color: $orange-600;
background-color: $orange-100; background-color: $orange-100;
border-radius: $border-radius-default; border-radius: $border-radius-default;
padding: 5px; padding: 5px;
margin: 0 3px 0 -4px; margin: 0 $btn-side-margin 0 0;
width: $issuable-warning-size;
height: $issuable-warning-size;
text-align: center;
&:first-of-type {
margin-right: $issuable-warning-icon-margin;
}
} }
.is-not-confidential { .sidebar-item-icon {
border-radius: $border-radius-default; border-radius: $border-radius-default;
padding: 5px; padding: 5px;
margin: 0 3px 0 -4px; margin: 0 3px 0 -4px;
}
.confidentiality {
.is-not-confidential {
margin: auto;
}
.is-confidential { &.is-active {
margin: auto; color: $orange-600;
background-color: $orange-50;
} }
} }
......
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
} }
} }
.confidential-issue-warning { .issuable-note-warning {
color: $orange-600; color: $orange-600;
background-color: $orange-100; background-color: $orange-100;
border-radius: $border-radius-default $border-radius-default 0 0; border-radius: $border-radius-default $border-radius-default 0 0;
...@@ -110,37 +110,64 @@ ...@@ -110,37 +110,64 @@
padding: 3px 12px; padding: 3px 12px;
margin: auto; margin: auto;
align-items: center; align-items: center;
.icon {
margin-right: $issuable-warning-icon-margin;
}
}
.disabled-comment .issuable-note-warning {
border: none;
border-radius: $label-border-radius;
padding-top: $gl-vert-padding;
padding-bottom: $gl-vert-padding;
.icon svg {
position: relative;
top: 2px;
margin-right: $btn-xs-side-margin;
width: $gl-font-size;
height: $gl-font-size;
fill: $orange-600;
}
} }
.confidential-value { .sidebar-item-value {
.fa { .fa {
background-color: inherit; background-color: inherit;
} }
} }
.confidential-warning-message { .sidebar-item-warning-message {
line-height: 1.5; line-height: 1.5;
padding: 16px; padding: 16px;
.confidential-warning-message-actions { .text {
color: $text-color;
}
.sidebar-item-warning-message-actions {
display: flex; display: flex;
button { .btn {
flex-grow: 1; flex-grow: 1;
} }
} }
} }
.confidential-issue-warning + .md-area { .issuable-note-warning + .md-area {
border-top-left-radius: 0; border-top-left-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
} }
.discussion-form { .discussion-form {
padding: $gl-padding-top $gl-padding $gl-padding;
background-color: $white-light; background-color: $white-light;
} }
.discussion-form-container {
padding: $gl-padding-top $gl-padding $gl-padding;
}
.discussion-notes .disabled-comment { .discussion-notes .disabled-comment {
padding: 6px 0; padding: 6px 0;
} }
......
...@@ -650,29 +650,12 @@ ul.notes { ...@@ -650,29 +650,12 @@ ul.notes {
} }
.add-diff-note { .add-diff-note {
@include btn-comment-icon;
opacity: 0; opacity: 0;
margin-top: -2px; margin-top: -2px;
border-radius: 50%;
background: $white-light;
padding: 1px 5px;
font-size: 12px;
color: $blue-500;
margin-left: -55px; margin-left: -55px;
position: absolute; position: absolute;
z-index: 10; z-index: 10;
width: 23px;
height: 23px;
border: 1px solid $blue-500;
&:hover {
background: $blue-500;
border-color: $blue-600;
color: $white-light;
}
&:active {
outline: 0;
}
} }
.discussion-body, .discussion-body,
...@@ -703,6 +686,12 @@ ul.notes { ...@@ -703,6 +686,12 @@ ul.notes {
color: $note-disabled-comment-color; color: $note-disabled-comment-color;
padding: 90px 0; padding: 90px 0;
&.discussion-locked {
border: none;
background-color: $white-light;
}
a { a {
color: $gl-link-color; color: $gl-link-color;
} }
......
...@@ -108,6 +108,15 @@ ...@@ -108,6 +108,15 @@
} }
} }
.subkeys-list {
@include basic-list;
li {
padding: 3px 0;
border: none;
}
}
.key-list-item { .key-list-item {
.key-list-item-info { .key-list-item-info {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
...@@ -392,11 +401,11 @@ table.u2f-registrations { ...@@ -392,11 +401,11 @@ table.u2f-registrations {
} }
} }
.gpg-email-badge { .email-badge {
display: inline; display: inline;
margin-right: $gl-padding / 2; margin-right: $gl-padding / 2;
.gpg-email-badge-email { .email-badge-email {
display: inline; display: inline;
margin-right: $gl-padding / 4; margin-right: $gl-padding / 4;
} }
......
...@@ -499,73 +499,56 @@ a.deploy-project-label { ...@@ -499,73 +499,56 @@ a.deploy-project-label {
} }
} }
.fork-namespaces { .fork-thumbnail {
.row { height: 200px;
-webkit-flex-wrap: wrap; width: calc((100% / 2) - #{$gl-padding * 2});
display: -webkit-flex;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
.fork-thumbnail { @media (min-width: $screen-md-min) {
border-radius: $border-radius-base; width: calc((100% / 4) - #{$gl-padding * 2});
background-color: $white-light; }
border: 1px solid $border-white-light;
height: 202px;
margin: $gl-padding;
text-align: center;
width: 169px;
&:hover:not(.disabled), @media (min-width: $screen-lg-min) {
&.forked { width: calc((100% / 5) - #{$gl-padding * 2});
background-color: $row-hover; }
border-color: $row-hover-border;
}
.no-avatar { &:hover:not(.disabled),
width: 100px; &.forked {
height: 100px; background-color: $row-hover;
background-color: $gray-light; border-color: $row-hover-border;
border: 1px solid $white-normal; }
margin: 0 auto;
border-radius: 50%;
i {
font-size: 100px;
color: $white-normal;
}
}
a { .avatar-container,
display: block; .identicon {
width: 100%; float: none;
height: 100%; margin-left: auto;
padding-top: $gl-padding; margin-right: auto;
color: $gl-text-color; }
&.disabled {
opacity: .3;
cursor: not-allowed;
&:hover {
text-decoration: none;
}
}
.caption {
min-height: 30px;
padding: $gl-padding 0;
}
}
img { a {
border-radius: 50%; display: block;
max-width: 100px; width: 100%;
} height: 100%;
padding-top: $gl-padding;
text-decoration: none;
&.disabled {
opacity: .3;
cursor: not-allowed;
} }
} }
} }
.fork-thumbnail-container {
display: flex;
flex-wrap: wrap;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
> h5 {
width: 100%;
}
}
.project-template, .project-template,
.project-import { .project-import {
.form-group { .form-group {
......
...@@ -169,6 +169,14 @@ ...@@ -169,6 +169,14 @@
} }
} }
.tree-item-file-external-link {
margin-right: 4px;
span {
text-decoration: inherit;
}
}
.tree_commit { .tree_commit {
max-width: 320px; max-width: 320px;
......
...@@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController
def remove_email def remove_email
email = user.emails.find(params[:email_id]) email = user.emails.find(params[:email_id])
success = Emails::DestroyService.new(current_user, user: user, email: email.email).execute success = Emails::DestroyService.new(current_user, user: user).execute(email)
respond_to do |format| respond_to do |format|
if success if success
......
...@@ -85,12 +85,21 @@ class ApplicationController < ActionController::Base ...@@ -85,12 +85,21 @@ class ApplicationController < ActionController::Base
super super
payload[:remote_ip] = request.remote_ip payload[:remote_ip] = request.remote_ip
if current_user.present? logged_user = auth_user
payload[:user_id] = current_user.id
payload[:username] = current_user.username if logged_user.present?
payload[:user_id] = logged_user.try(:id)
payload[:username] = logged_user.try(:username)
end end
end end
# Controllers such as GitHttpController may use alternative methods
# (e.g. tokens) to authenticate the user, whereas Devise sets current_user
def auth_user
return current_user if current_user.present?
return try(:authenticated_user)
end
# This filter handles both private tokens and personal access tokens # This filter handles both private tokens and personal access tokens
def authenticate_user_from_private_token! def authenticate_user_from_private_token!
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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