Commit 35659da1 authored by Clement Ho's avatar Clement Ho

Merge branch 'master' into 'fix-btn-alignment'

# Conflicts:
#   app/views/projects/merge_requests/_nav_btns.html.haml
parents 1405bf84 fe09c25d
Pipeline #10884970 passed with stages
in 77 minutes and 51 seconds
......@@ -226,6 +226,7 @@ update-tests-metadata:
flaky-examples-check:
<<: *dedicated-runner
<<: *except-docs
image: ruby:2.3-alpine
services: []
before_script: []
......
......@@ -324,6 +324,7 @@ group :development, :test do
gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3'
gem 'rspec-parameterized'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0'
......
......@@ -2,6 +2,7 @@ GEM
remote: https://rubygems.org/
specs:
RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.2)
actionmailer (4.2.8)
actionpack (= 4.2.8)
......@@ -41,6 +42,9 @@ GEM
tzinfo (~> 1.1)
acts-as-taggable-on (4.0.0)
activerecord (>= 4.0)
adamantium (0.2.0)
ice_nine (~> 0.11.0)
memoizable (~> 0.4.0)
addressable (2.3.8)
after_commit_queue (1.3.0)
activerecord (>= 3.0)
......@@ -124,6 +128,9 @@ GEM
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
colorize (0.7.7)
concord (0.1.5)
adamantium (~> 0.2.0)
equalizer (~> 0.0.9)
concurrent-ruby (1.0.5)
concurrent-ruby-ext (1.0.5)
concurrent-ruby (= 1.0.5)
......@@ -470,6 +477,8 @@ GEM
mime-types (>= 1.16, < 4)
mail_room (0.9.1)
memoist (0.15.0)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2)
mime-types (2.99.3)
mimemagic (0.3.0)
......@@ -610,6 +619,11 @@ GEM
premailer-rails (1.9.7)
actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9)
proc_to_ast (0.1.0)
coderay
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.7.0.beta11)
mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4)
......@@ -718,6 +732,10 @@ GEM
chunky_png
rqrcode-rails3 (0.1.7)
rqrcode (>= 0.4.2)
rspec (3.6.0)
rspec-core (~> 3.6.0)
rspec-expectations (~> 3.6.0)
rspec-mocks (~> 3.6.0)
rspec-core (3.6.0)
rspec-support (~> 3.6.0)
rspec-expectations (3.6.0)
......@@ -726,6 +744,12 @@ GEM
rspec-mocks (3.6.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0)
rspec-parameterized (0.4.0)
binding_of_caller
parser
proc_to_ast
rspec (>= 2.13, < 4)
unparser
rspec-rails (3.6.0)
actionpack (>= 3.0)
activesupport (>= 3.0)
......@@ -892,6 +916,14 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
unparser (0.2.6)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
diff-lcs (~> 1.3)
equalizer (~> 0.0.9)
parser (>= 2.3.1.2, < 2.5)
procto (~> 0.0.2)
url_safe_base64 (0.2.2)
validates_hostname (1.0.6)
activerecord (>= 3.0)
......@@ -1094,6 +1126,7 @@ DEPENDENCIES
responders (~> 2.0)
rouge (~> 2.0)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.6.0)
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
......
export default class GpgBadges {
static fetch() {
const badges = $('.js-loading-gpg-badge');
const form = $('.commits-search-form');
badges.html('<i class="fa fa-spinner fa-spin"></i>');
$.get({
url: form.data('signatures-path'),
data: form.serialize(),
}).done((response) => {
const badges = $('.js-loading-gpg-badge');
response.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
});
......
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../../vue_shared/translate';
import illustrationSvg from '../icons/intro_illustration.svg';
Vue.use(Translate);
const cookieKey = 'pipeline_schedules_callout_dismissed';
export default {
name: 'PipelineSchedulesCallout',
data() {
return {
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
illustrationSvg,
calloutDismissed: Cookies.get(cookieKey) === 'true',
};
},
methods: {
dismissCallout() {
this.calloutDismissed = true;
Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
},
},
template: `
<div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout">
<div class="bordered-box landing content-block">
<button
id="dismiss-callout-btn"
class="btn btn-default close"
@click="dismissCallout">
<i class="fa fa-times"></i>
</button>
<div class="svg-container" v-html="illustrationSvg"></div>
<div class="user-callout-copy">
<h4>{{ __('Scheduling Pipelines') }}</h4>
<p>
{{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }}
</p>
<p> {{ __('Learn more in the') }}
<a
:href="docsUrl"
target="_blank"
rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period -->
</p>
</div>
</div>
</div>
`,
};
<script>
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../../vue_shared/translate';
import illustrationSvg from '../icons/intro_illustration.svg';
Vue.use(Translate);
const cookieKey = 'pipeline_schedules_callout_dismissed';
export default {
name: 'PipelineSchedulesCallout',
data() {
return {
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
calloutDismissed: Cookies.get(cookieKey) === 'true',
};
},
methods: {
dismissCallout() {
this.calloutDismissed = true;
Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
},
},
created() {
this.illustrationSvg = illustrationSvg;
},
};
</script>
<template>
<div
v-if="!calloutDismissed"
class="pipeline-schedules-user-callout user-callout">
<div class="bordered-box landing content-block">
<button
id="dismiss-callout-btn"
class="btn btn-default close"
@click="dismissCallout">
<i
aria-hidden="true"
class="fa fa-times">
</i>
</button>
<div class="svg-container" v-html="illustrationSvg"></div>
<div class="user-callout-copy">
<h4>{{ __('Scheduling Pipelines') }}</h4>
<p>
{{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }}
</p>
<p> {{ __('Learn more in the') }}
<a
:href="docsUrl"
target="_blank"
rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period -->
</p>
</div>
</div>
</div>
</template>
import Vue from 'vue';
import PipelineSchedulesCallout from './components/pipeline_schedules_callout';
import PipelineSchedulesCallout from './components/pipeline_schedules_callout.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipeline-schedules-callout',
......
......@@ -48,6 +48,27 @@
return `${this.job.name} - ${this.job.status.label}`;
},
},
methods: {
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
.on('click', (e) => {
e.stopPropagation();
});
},
},
mounted() {
this.stopDropdownClickPropagation();
},
};
</script>
<template>
......
......@@ -29,12 +29,10 @@ export default {
editMode() {
if (this.editMode) {
$('.project-refs-form').addClass('disabled');
$('.fa-long-arrow-right').show();
$('.project-refs-target-form').show();
$('.js-tree-ref-target-holder').show();
} else {
$('.project-refs-form').removeClass('disabled');
$('.fa-long-arrow-right').hide();
$('.project-refs-target-form').hide();
$('.js-tree-ref-target-holder').hide();
}
},
},
......
......@@ -4,7 +4,7 @@ import Store from '../stores/repo_store';
export default {
data: () => Store,
mounted() {
$(this.$el).find('.file-content').syntaxHighlight();
this.highlightFile();
},
computed: {
html() {
......@@ -12,10 +12,16 @@ export default {
},
},
methods: {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
},
watch: {
html() {
this.$nextTick(() => {
$(this.$el).find('.file-content').syntaxHighlight();
this.highlightFile();
});
},
},
......@@ -24,9 +30,23 @@ export default {
<template>
<div>
<div v-if="!activeFile.render_error" v-html="activeFile.html"></div>
<div v-if="activeFile.render_error" class="vertical-center render-error">
<p class="text-center">The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.</p>
<div
v-if="!activeFile.render_error"
v-html="activeFile.html">
</div>
<div
v-else-if="activeFile.tooLarge"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.
</p>
</div>
<div
v-else
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because a rendering error occured. You can <a :href="activeFile.raw_path">download</a> it instead.
</p>
</div>
</div>
</template>
......@@ -33,32 +33,30 @@ const RepoSidebar = {
});
},
linkClicked(clickedFile) {
let url = '';
fileClicked(clickedFile) {
let file = clickedFile;
if (typeof file === 'object') {
file.loading = true;
if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file);
file.loading = false;
} else {
url = file.url;
Service.url = url;
// I need to refactor this to do the `then` here.
// Not a callback. For now this is good enough.
// it works.
Helper.getContent(file, () => {
file.loading = true;
if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file);
file.loading = false;
} else {
Service.url = file.url;
Helper.getContent(file)
.then(() => {
file.loading = false;
Helper.scrollTabsRight();
});
}
} else if (typeof file === 'string') {
// go back
url = file;
Service.url = url;
Helper.getContent(null, () => Helper.scrollTabsRight());
})
.catch(Helper.loadingError);
}
},
goToPreviousDirectoryClicked(prevURL) {
Service.url = prevURL;
Helper.getContent(null)
.then(() => Helper.scrollTabsRight())
.catch(Helper.loadingError);
},
},
};
......@@ -82,7 +80,7 @@ export default RepoSidebar;
<repo-previous-directory
v-if="isRoot"
:prev-url="prevURL"
@linkclicked="linkClicked(prevURL)"/>
@linkclicked="goToPreviousDirectoryClicked(prevURL)"/>
<repo-loading-file
v-for="n in 5"
:key="n"
......@@ -94,7 +92,7 @@ export default RepoSidebar;
:key="file.id"
:file="file"
:is-mini="isMini"
@linkclicked="linkClicked(file)"
@linkclicked="fileClicked(file)"
:is-tree="isTree"
:has-files="!!files.length"
:active-file="activeFile"/>
......
......@@ -10,6 +10,12 @@ const RepoTab = {
},
computed: {
closeLabel() {
if (this.tab.changed) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
changedClass() {
const tabChangedObj = {
'fa-times': !this.tab.changed,
......@@ -34,12 +40,24 @@ export default RepoTab;
<template>
<li>
<a href="#" class="close" @click.prevent="xClicked(tab)" v-if="!tab.loading">
<i class="fa" :class="changedClass"></i>
<a
href="#0"
class="close"
@click.prevent="xClicked(tab)"
:aria-label="closeLabel">
<i
class="fa"
:class="changedClass"
aria-hidden="true">
</i>
</a>
<a href="#" class="repo-tab" v-if="!tab.loading" :title="tab.url" @click.prevent="tabClicked(tab)">{{tab.name}}</a>
<i v-if="tab.loading" class="fa fa-spinner fa-spin"></i>
<a
href="#"
class="repo-tab"
:title="tab.url"
@click.prevent="tabClicked(tab)">
{{tab.name}}
</a>
</li>
</template>
<script>
import Vue from 'vue';
import Store from '../stores/repo_store';
import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin';
......@@ -14,29 +13,19 @@ const RepoTabs = {
data: () => Store,
methods: {
isOverflow() {
return this.$el.scrollWidth > this.$el.offsetWidth;
},
xClicked(file) {
Store.removeFromOpenedFiles(file);
},
},
watch: {
openedFiles() {
Vue.nextTick(() => {
this.tabsOverflow = this.isOverflow();
});
},
},
};
export default RepoTabs;
</script>
<template>
<ul id="tabs" v-if="isMini" v-cloak :class="{'overflown': tabsOverflow}">
<ul
v-if="isMini"
id="tabs">
<repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/>
<li class="tabs-divider" />
</ul>
......
......@@ -10,7 +10,10 @@ function repoEditorLoader() {
Store.monaco = monaco;
Store.monacoLoading = false;
resolve(RepoEditor);
}, reject);
}, () => {
Store.monacoLoading = false;
reject();
});
});
}
......
......@@ -33,12 +33,16 @@ const RepoHelper = {
? window.performance
: Date,
getFileExtension(fileName) {
return fileName.split('.').pop();
},
getBranch() {
return $('button.dropdown-menu-toggle').attr('data-ref');
},
getLanguageIDForFile(file, langs) {
const ext = file.name.split('.').pop();
const ext = RepoHelper.getFileExtension(file.name);
const foundLang = RepoHelper.findLanguage(ext, langs);
return foundLang ? foundLang.id : 'plaintext';
......@@ -135,21 +139,19 @@ const RepoHelper = {
return isRoot;
},
getContent(treeOrFile, cb) {
getContent(treeOrFile) {
let file = treeOrFile;
// const loadingData = RepoHelper.setLoading(true);
return Service.getContent()
.then((response) => {
const data = response.data;
// RepoHelper.setLoading(false, loadingData);
if (cb) cb();
Store.isTree = RepoHelper.isTree(data);
if (!Store.isTree) {
if (!file) file = data;
Store.binary = data.binary;
if (data.binary) {
Store.binaryMimeType = data.mime_type;
// file might be undefined
RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview();
......@@ -188,9 +190,8 @@ const RepoHelper = {
setFile(data, file) {
const newFile = data;
newFile.url = file.url || location.pathname;
newFile.url = file.url;
if (newFile.render_error === 'too_large') {
if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
newFile.tooLarge = true;
}
newFile.newContent = '';
......@@ -199,10 +200,6 @@ const RepoHelper = {
Store.setActiveFiles(newFile);
},
toFA(icon) {
return `fa-${icon}`;
},
serializeBlob(blob) {
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
simpleBlob.lastCommitMessage = blob.last_commit.message;
......@@ -226,7 +223,7 @@ const RepoHelper = {
type,
name,
url,
icon: RepoHelper.toFA(icon),
icon: `fa-${icon}`,
level: 0,
loading: false,
};
......@@ -244,7 +241,7 @@ const RepoHelper = {
setTimeout(() => {
const tabs = document.getElementById('tabs');
if (!tabs) return;
tabs.scrollLeft = 12000;
tabs.scrollLeft = tabs.scrollWidth;
}, 200);
},
......
......@@ -7,8 +7,7 @@ import RepoEditButton from './components/repo_edit_button.vue';
import Translate from '../vue_shared/translate';
function initDropdowns() {
$('.project-refs-target-form').hide();
$('.fa-long-arrow-right').hide();
$('.js-tree-ref-target-holder').hide();
}
function addEventsForNonVueEls() {
......
......@@ -2,6 +2,7 @@
import axios from 'axios';
import Store from '../stores/repo_store';
import Api from '../../api';
import Helper from '../helpers/repo_helper';
const RepoService = {
url: '',
......@@ -22,6 +23,7 @@ const RepoService = {
getRaw(url) {
return axios.get(url, {
// Stop Axios from parsing a JSON file into a JS object
transformResponse: [res => res],
});
},
......@@ -36,7 +38,7 @@ const RepoService = {
},
urlIsRichBlob(url = this.url) {
const extension = url.split('.').pop();
const extension = Helper.getFileExtension(url);
return this.richExtensionRegExp.test(extension);
},
......
......@@ -3,13 +3,10 @@ import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
const RepoStore = {
ideEl: {},
monaco: {},
monacoLoading: false,
monacoInstance: {},