Commit cc572c43 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'ce-to-ee-2017-05-17' into 'master'

CE upstream - Wednesday

Closes gitlab-ce#31902, gitaly#187, gitlab-ce#31932, gitaly#199, gitlab-ce#32125, #2128, gitaly#224, gitlab-ce#32096, gitlab-ce#32087, and omnibus-gitlab#2326

See merge request !1920
parents 1fe0c587 5a6ab710
......@@ -18,6 +18,7 @@ eslint-report.html
.sass-cache/
/.secret
/.vagrant
/.yarn-cache
/.byebug_history
/Vagrantfile
/backups/*
......@@ -49,6 +50,7 @@ eslint-report.html
/public/uploads/
/shared/artifacts/
/spec/javascripts/fixtures/blob/pdf/
/spec/javascripts/fixtures/blob/balsamiq/
/rails_best_practices_output.html
/tags
/tmp/*
......
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-phantomjs-2.1-node-7.1-postgresql-9.6"
cache:
key: "ruby-233"
key: "ruby-233-with-yarn"
paths:
- vendor/ruby
- .yarn-cache/
variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
......@@ -194,7 +195,7 @@ setup-test-env:
stage: prepare
script:
- node --version
- yarn install --pure-lockfile
- yarn install --pure-lockfile --cache-folder .yarn-cache
- bundle exec rake gitlab:assets:compile
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
artifacts:
......@@ -413,7 +414,8 @@ gitlab:assets:compile:
SKIP_STORAGE_VALIDATION: "true"
WEBPACK_REPORT: "true"
script:
- bundle exec rake yarn:install gitlab:assets:compile
- yarn install --pure-lockfile --production --cache-folder .yarn-cache
- bundle exec rake gitlab:assets:compile
artifacts:
name: webpack-report
expire_in: 31d
......@@ -421,9 +423,6 @@ gitlab:assets:compile:
- webpack-report/
karma:
cache:
paths:
- vendor/ruby
stage: test
<<: *use-pg
<<: *dedicated-runner
......
......@@ -40,6 +40,7 @@ logs, and code as it's very hard to read otherwise.)
#### Results of GitLab environment info
<details>
<summary>Expand for output related to GitLab environment info</summary>
<pre>
(For installations with omnibus-gitlab package run and paste the output of:
......@@ -54,6 +55,7 @@ logs, and code as it's very hard to read otherwise.)
#### Results of GitLab application Check
<details>
<summary>Expand for output related to the GitLab application check</summary>
<pre>
(For installations with omnibus-gitlab package run and paste the output of:
......
......@@ -255,6 +255,7 @@ entry.
## 9.1.0 (2017-04-22)
- Add Jupyter notebook rendering !10017
- Added merge requests empty state. !7342
- Add option to start a new resolvable discussion in an MR. !7527
- Hide form inputs for group member without editing rights. !7816
......
......@@ -154,12 +154,12 @@ gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs
gem 'sidekiq', '~> 5.0'
gem 'sidekiq-cron', '~> 0.4.4'
gem 'sidekiq-cron', '~> 0.6.0'
gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4'
# Cron Parser
gem 'rufus-scheduler', '~> 3.1.10'
gem 'rufus-scheduler', '~> 3.4'
# HTTP requests
gem 'httparty', '~> 0.13.3'
......@@ -377,6 +377,6 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client
gem 'gitaly', '~> 0.6.0'
gem 'gitaly', '~> 0.7.0'
gem 'toml-rb', '~> 0.3.15', require: false
......@@ -202,6 +202,8 @@ GEM
equalizer (0.0.11)
erubis (2.7.0)
escape_utils (1.1.1)
et-orbi (1.0.3)
tzinfo
eventmachine (1.0.8)
excon (0.55.0)
execjs (2.6.0)
......@@ -287,7 +289,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly (0.6.0)
gitaly (0.7.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -725,7 +727,8 @@ GEM
rubyntlm (0.5.2)
rubypants (0.2.0)
rubyzip (1.2.1)
rufus-scheduler (3.1.10)
rufus-scheduler (3.4.0)
et-orbi (~> 1.0)
rugged (0.25.1.1)
safe_yaml (1.0.4)
sanitize (2.1.0)
......@@ -762,9 +765,8 @@ GEM
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
redis (~> 3.3, >= 3.3.3)
sidekiq-cron (0.4.4)
redis-namespace (>= 1.5.2)
rufus-scheduler (>= 2.0.24)
sidekiq-cron (0.6.0)
rufus-scheduler (>= 3.3.0)
sidekiq (>= 4.2.1)
sidekiq-limit_fetch (3.4.0)
sidekiq (>= 4)
......@@ -955,7 +957,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.6.0)
gitaly (~> 0.7.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
......@@ -1049,7 +1051,7 @@ DEPENDENCIES
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
ruby_parser (~> 3.8.4)
rufus-scheduler (~> 3.1.10)
rufus-scheduler (~> 3.4)
rugged (~> 0.25.1.1)
sanitize (~> 2.0)
sass-rails (~> 5.0.6)
......@@ -1061,7 +1063,7 @@ DEPENDENCIES
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
sidekiq (~> 5.0)
sidekiq-cron (~> 0.4.4)
sidekiq-cron (~> 0.6.0)
sidekiq-limit_fetch (~> 3.4)
simplecov (~> 0.14.0)
slack-notifier (~> 1.5.1)
......
/* global Flash */
import sqljs from 'sql.js';
import { template as _template } from 'underscore';
......@@ -15,19 +13,27 @@ const PREVIEW_TEMPLATE = _template(`
class BalsamiqViewer {
constructor(viewer) {
this.viewer = viewer;
this.endpoint = this.viewer.dataset.endpoint;
}
loadFile() {
const xhr = new XMLHttpRequest();
loadFile(endpoint) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', endpoint, true);
xhr.responseType = 'arraybuffer';
xhr.onload = loadEvent => this.fileLoaded(loadEvent, resolve, reject);
xhr.onerror = reject;
xhr.send();
});
}
xhr.open('GET', this.endpoint, true);
xhr.responseType = 'arraybuffer';
fileLoaded(loadEvent, resolve, reject) {
if (loadEvent.target.status !== 200) return reject();
xhr.onload = this.renderFile.bind(this);
xhr.onerror = BalsamiqViewer.onError;
this.renderFile(loadEvent);
xhr.send();
return resolve();
}
renderFile(loadEvent) {
......@@ -103,12 +109,6 @@ class BalsamiqViewer {
static parseTitle(resource) {
return JSON.parse(resource.values[0][2]).name;
}
static onError() {
const flash = new Flash('Balsamiq file could not be loaded.');
return flash;
}
}
export default BalsamiqViewer;
/* global Flash */
import BalsamiqViewer from './balsamiq/balsamiq_viewer';
document.addEventListener('DOMContentLoaded', () => {
const balsamiqViewer = new BalsamiqViewer(document.getElementById('js-balsamiq-viewer'));
balsamiqViewer.loadFile();
});
function onError() {
const flash = new window.Flash('Balsamiq file could not be loaded.');
return flash;
}
function loadBalsamiqFile() {
const viewer = document.getElementById('js-balsamiq-viewer');
if (!(viewer instanceof Element)) return;
const endpoint = viewer.dataset.endpoint;
const balsamiqViewer = new BalsamiqViewer(viewer);
balsamiqViewer.loadFile(endpoint).catch(onError);
}
$(loadBalsamiqFile);
/* global Flash */
export default class BlobViewer {
constructor() {
BlobViewer.initAuxiliaryViewer();
this.initMainViewers();
}
static initAuxiliaryViewer() {
const auxiliaryViewer = document.querySelector('.blob-viewer[data-type="auxiliary"]');
if (!auxiliaryViewer) return;
BlobViewer.loadViewer(auxiliaryViewer);
}
initMainViewers() {
this.$fileHolder = $('.file-holder');
if (!this.$fileHolder.length) return;
this.switcher = document.querySelector('.js-blob-viewer-switcher');
this.switcherBtns = document.querySelectorAll('.js-blob-viewer-switch-btn');
this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn');
this.simpleViewer = document.querySelector('.blob-viewer[data-type="simple"]');
this.richViewer = document.querySelector('.blob-viewer[data-type="rich"]');
this.$fileHolder = $('.file-holder');
let initialViewerName = document.querySelector('.blob-viewer:not(.hidden)').getAttribute('data-type');
this.simpleViewer = this.$fileHolder[0].querySelector('.blob-viewer[data-type="simple"]');
this.richViewer = this.$fileHolder[0].querySelector('.blob-viewer[data-type="rich"]');
this.initBindings();
this.switchToInitialViewer();
}
switchToInitialViewer() {
const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)');
let initialViewerName = initialViewer.getAttribute('data-type');
if (this.switcher && location.hash.indexOf('#L') === 0) {
initialViewerName = 'simple';
}
......@@ -61,40 +82,13 @@ export default class BlobViewer {
$(this.copySourceBtn).tooltip('fixTitle');
}
loadViewer(viewerParam) {
const viewer = viewerParam;
const url = viewer.getAttribute('data-url');
if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
return;
}
viewer.setAttribute('data-loading', 'true');
$.ajax({
url,
dataType: 'JSON',
})
.fail(() => new Flash('Error loading source view'))
.done((data) => {
viewer.innerHTML = data.html;
$(viewer).syntaxHighlight();
viewer.setAttribute('data-loaded', 'true');
this.$fileHolder.trigger('highlight:line');
this.toggleCopyButtonState();
});
}
switchToViewer(name) {
const newViewer = document.querySelector(`.blob-viewer[data-type='${name}']`);
const newViewer = this.$fileHolder[0].querySelector(`.blob-viewer[data-type='${name}']`);
if (this.activeViewer === newViewer) return;
const oldButton = document.querySelector('.js-blob-viewer-switch-btn.active');
const newButton = document.querySelector(`.js-blob-viewer-switch-btn[data-viewer='${name}']`);
const oldViewer = document.querySelector(`.blob-viewer:not([data-type='${name}'])`);
const oldViewer = this.$fileHolder[0].querySelector(`.blob-viewer:not([data-type='${name}'])`);
if (oldButton) {
oldButton.classList.remove('active');
......@@ -115,6 +109,41 @@ export default class BlobViewer {
this.toggleCopyButtonState();
this.loadViewer(newViewer);
BlobViewer.loadViewer(newViewer)
.then((viewer) => {
$(viewer).syntaxHighlight();
this.$fileHolder.trigger('highlight:line');
gl.utils.handleLocationHash();
this.toggleCopyButtonState();
})
.catch(() => new Flash('Error loading viewer'));
}
static loadViewer(viewerParam) {
const viewer = viewerParam;
const url = viewer.getAttribute('data-url');
return new Promise((resolve, reject) => {
if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
resolve(viewer);
return;
}
viewer.setAttribute('data-loading', 'true');
$.ajax({
url,
dataType: 'JSON',
})
.fail(reject)
.done((data) => {
viewer.innerHTML = data.html;
viewer.setAttribute('data-loaded', 'true');
resolve(viewer);
});
});
}
}
......@@ -2,6 +2,7 @@
import boardNewIssue from './board_new_issue';
import boardCard from './board_card';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
const Store = gl.issueBoards.BoardsStore;
......@@ -44,6 +45,7 @@ export default {
components: {
boardCard,
boardNewIssue,
loadingIcon,
},
methods: {
listHeight() {
......@@ -156,10 +158,7 @@ export default {
class="board-list-loading text-center"
aria-label="Loading issues"
v-if="loading">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true">
</i>
<loading-icon />
</div>
<board-new-issue
:list="list"
......@@ -184,12 +183,12 @@ export default {
class="board-list-count text-center"
v-if="showCount"
data-id="-1">
<i
class="fa fa-spinner fa-spin"
aria-label="Loading more issues"
aria-hidden="true"
v-show="list.loadingMore">
</i>
<loading-icon
v-show="list.loadingMore"
label="Loading more issues"
/>
<span v-if="list.issues.length === list.issuesSize">
Showing all issues
</span>
......
......@@ -34,6 +34,9 @@ gl.issueBoards.BoardSidebar = Vue.extend({
},
assigneeId() {
return this.issue.assignee ? this.issue.assignee.id : 0;
},
milestoneTitle() {
return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
}
},
watch: {
......@@ -60,18 +63,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({
},
deep: true
},
issue () {
if (this.showSidebar) {
this.$nextTick(() => {
$('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
$('.right-sidebar').getNiceScroll().resize();
});
}
this.issue = this.detail.issue;
this.list = this.detail.list;
},
deep: true
},
methods: {
closeSidebar () {
......
......@@ -2,6 +2,8 @@
import Vue from 'vue';
import queryData from '../../utils/query_data';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import './header';
import './list';
import './footer';
......@@ -136,6 +138,7 @@ gl.issueBoards.IssuesModal = Vue.extend({
'modal-list': gl.issueBoards.ModalList,
'modal-footer': gl.issueBoards.ModalFooter,
'empty-state': gl.issueBoards.ModalEmptyState,
loadingIcon,
},
template: `
<div
......@@ -160,7 +163,7 @@ gl.issueBoards.IssuesModal = Vue.extend({
class="add-issues-list text-center"
v-if="loading || filterLoading">
<div class="add-issues-list-loading">
<i class="fa fa-spinner fa-spin"></i>
<loading-icon />
</div>
</section>
<modal-footer></modal-footer>
......
import Vue from 'vue';
import Visibility from 'visibilityjs';
import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
import pipelinesTableComponent from '../../vue_shared/components/pipelines_table';
import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store';
import eventHub from '../../pipelines/event_hub';
import EmptyState from '../../pipelines/components/empty_state.vue';
import ErrorState from '../../pipelines/components/error_state.vue';
import emptyState from '../../pipelines/components/empty_state.vue';
import errorState from '../../pipelines/components/error_state.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor';
import Poll from '../../lib/utils/poll';
......@@ -17,16 +18,15 @@ import Poll from '../../lib/utils/poll';
* We need a store to store the received environemnts.
* We need a service to communicate with the server.
*
* Necessary SVG in the table are provided as props. This should be refactored
* as soon as we have Webpack and can load them directly into JS files.
*/
export default Vue.component('pipelines-table', {
components: {
'pipelines-table-component': PipelinesTableComponent,
'error-state': ErrorState,
'empty-state': EmptyState,
pipelinesTableComponent,
errorState,
emptyState,
loadingIcon,
},
/**
......@@ -47,6 +47,7 @@ export default Vue.component('pipelines-table', {
hasError: false,
isMakingRequest: false,
updateGraphDropdown: false,
hasMadeRequest: false,
};
},
......@@ -55,9 +56,15 @@ export default Vue.component('pipelines-table', {
return this.hasError && !this.isLoading;
},
/**
* Empty state is only rendered if after the first request we receive no pipelines.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.state.pipelines.length &&
!this.isLoading &&
this.hasMadeRequest &&
!this.hasError;
},
......@@ -94,6 +101,10 @@ export default Vue.component('pipelines-table', {
if (!Visibility.hidden()) {
this.isLoading = true;
this.poll.makeRequest();
} else {
// If tab is not visible we need to make the first request so we don't show the empty
// state without knowing if there are any pipelines
this.fetchPipelines();
}
Visibility.change(() => {
......@@ -127,6 +138,8 @@ export default Vue.component('pipelines-table', {
successCallback(resp) {
const response = resp.json();
this.hasMadeRequest = true;
// depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = response.pipelines || response;
this.store.storePipelines(pipelines);
......@@ -151,13 +164,12 @@ export default Vue.component('pipelines-table', {
template: `
<div class="content-list pipelines">
<div
class="realtime-loading"
v-if="isLoading">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true" />
</div>
<loading-icon
label="Loading pipelines"
size="3"
v-if="isLoading"
/>
<empty-state
v-if="shouldRenderEmptyState"
......
......@@ -33,7 +33,7 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({
<span>
{{ __('FirstPushedBy|First') }}
<span class="commit-icon">${iconCommit}</span>
<a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
<a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a>
{{ __('FirstPushedBy|pushed by') }}
<a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }}
......
......@@ -26,9 +26,9 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({
<h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch">${iconBranch}</span>
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="build-date">{{ build.date }}</a>
......
......@@ -29,9 +29,9 @@ global.cycleAnalytics.StageTestComponent = Vue.extend({
&middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch">${iconBranch}</span>
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="issue-date">
......
<script>
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
data() {
......@@ -22,6 +23,11 @@
default: 'btn-default',
},
},
components: {
loadingIcon,
},
methods: {
doAction() {
this.isLoading = true;
......@@ -44,11 +50,6 @@
:disabled="isLoading"
@click="doAction">
{{ text }}
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="Loading">
</i>
<loading-icon v-if="isLoading" />
</button>
</template>
......@@ -4,6 +4,7 @@
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import keysPanel from './keys_panel.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
data() {
......@@ -28,6 +29,7 @@
},
components: {
keysPanel,
loadingIcon,
},
methods: {
fetchKeys() {
......@@ -74,15 +76,11 @@
<template>
<div class="col-lg-9 col-lg-offset-3 append-bottom-default deploy-keys">
<div
class="text-center"
v-if="isLoading && !hasKeys">
<i
class="fa fa-spinner fa-spin fa-2x"
aria-hidden="true"
aria-label="Loading deploy keys">
</i>
</div>
<loading-icon
v-if="isLoading && !hasKeys"
size="2"
label="Loading deploy keys"
/>
<div v-else-if="hasKeys">
<keys-panel
title="Enabled deploy keys for this project"
......
......@@ -65,4 +65,6 @@ $(() => {
'resolve-count': ResolveCount
},
});
$(window).trigger('resize.nav');
});
......@@ -14,7 +14,6 @@
/* global NotificationsForm */
/* global TreeView */
/* global NotificationsDropdown */
/* global UsersSelect */
/* global GroupAvatar */
/* global LineHighlighter */
/* global ProjectFork */
......@@ -55,6 +54,8 @@ import BlobViewer from './blob/viewer/index';
import GeoNodes from './geo_nodes';
import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import UsersSelect from './users_select';
import RefSelectDropdown from './ref_select_dropdown';
import GfmAutoComplete from './gfm_auto_complete';
import ShortcutsBlob from './shortcuts_blob';
......@@ -121,6 +122,7 @@ import ApproversSelect from './approvers_select';
case 'projects:boards:show':
case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
break;
case 'projects:builds:show':
new Build();
......@@ -135,6 +137,7 @@ import ApproversSelect from './approvers_select';
prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
});
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
break;
case 'projects:issues:show':
new Issue();
......@@ -147,6 +150,10 @@ import ApproversSelect from './approvers_select';
new Milestone();
new Sidebar();
break;
case 'groups:issues':
case 'groups:merge_requests':
new UsersSelect();
break;
case 'dashboard:todos:index':
new gl.Todos();
break;
......@@ -215,6 +222,7 @@ import ApproversSelect from './approvers_select';
case 'projects:tags:new':
new ZenMode();
new gl.GLForm($('.tag-form'));
new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs);
break;
case 'projects:releases:edit':
new ZenMode();
......@@ -232,6 +240,10 @@ import ApproversSelect from './approvers_select';
case 'dashboard:activity':
new gl.Activities();
break;
case 'dashboard:issues':
case 'dashboard:merge_requests':
new UsersSelect();
break;
case 'projects:commit:show':
new Commit();
new gl.Diff();
......@@ -251,6 +263,7 @@ import ApproversSelect from './approvers_select';
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:edit':
new UsersSelect();
const el = document.querySelector('.js-service-desk-setting-root');
if (el) {
const serviceDeskRoot = new ServiceDeskRoot(el);
......@@ -262,6 +275,7 @@ import ApproversSelect from './approvers_select';
new NotificationsForm();
if ($('#tree-slider').length) {
new TreeView();
new BlobViewer();
}
break;
case 'projects:pipelines:builds':
......@@ -316,6 +330,7 @@ import ApproversSelect from './approvers_select';
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
new TreeView();
new BlobViewer();
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:find_file:show':
......@@ -399,6 +414,9 @@ import ApproversSelect from './approvers_select';
new LineHighlighter();
new BlobViewer();
break;
case 'import:fogbugz:new_user_map':
new UsersSelect();
break;
}
switch (path.first()) {
case 'sessions':
......
/* eslint-disable */
import utils from './utils';
import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
var DropDown = function(list) {
this.currentIndex = 0;
this.hidden = true;
this.list = typeof list === 'string' ? document.querySelector(list) : list;
this.items = [];
class DropDown {
constructor(list) {
this.currentIndex = 0;
this.hidden = true;
this.list = typeof list === 'string' ? document.querySelector(list) : list;
this.items = [];
this.eventWrapper = {};
this.eventWrapper = {};
this.getItems();
this.initTemplateString();
this.addEvents();
this.getItems();
this.initTemplateString();
this.addEvents();
this.initialState = list.innerHTML;
};
this.initialState = list.innerHTML;
}
Object.assign(DropDown.prototype, {
getItems: function() {
getItems() {
this.items = [].slice.call(this.list.querySelectorAll('li'));
return this.items;
},
}
initTemplateString: function() {
var items = this.items || this.getItems();
initTemplateString() {
const items = this.items || this.getItems();
var templateString = '';
let templateString = '';
if (items.length > 0) templateString = items[items.length - 1].outerHTML;
this.templateString = templateString;
return this.templateString;
},
}
clickEvent: function(e) {
clickEvent(e) {
if (e.target.tagName === 'UL') return;
if (e.target.classList.contains(IGNORE_CLASS)) return;
var selected = utils.closest(e.target, 'LI');
const selected = utils.closest(e.target, 'LI');
if (!selected) return;
this.addSelectedClass(selected);
......@@ -46,95 +44,95 @@ Object.assign(DropDown.prototype, {
e.preventDefault();
this.hide();
var listEvent = new CustomEvent('click.dl', {
const listEvent = new CustomEvent('click.dl', {
detail: {
list: this,
selected: selected,
selected,
data: e.target.dataset,
},
});
this.list.dispatchEvent(listEvent);
},
}
addSelectedClass: function (selected) {
addSelectedClass(selected) {
this.removeSelectedClasses();
selected.classList.add(SELECTED_CLASS);
},
}
removeSelectedClasses: function () {
removeSelectedClasses() {
const items = this.items || this.getItems();
items.forEach(item => item.classList.remove(SELECTED_CLASS));
},
}
addEvents: function() {
this.eventWrapper.clickEvent = this.clickEvent.bind(this)
addEvents() {
this.eventWrapper.clickEvent = this.clickEvent.bind(this);
this.list.addEventListener('click', this.eventWrapper.clickEvent);
},
toggle: function() {
this.hidden ? this.show() : this.hide();
},
}
setData: function(data) {
setData(data) {
this.data = data;
this.render(data);
},
}
addData: function(data) {
addData(data) {
this.data = (this.data || []).concat(data);
this.render(this.data);
},
}
render: function(data) {
render(data) {
const children = data ? data.map(this.renderChildren.bind(this)) : [];
const renderableList = this.list.querySelector('ul[data-dynamic]') || this.list;
renderableList.innerHTML = children.join('');
},
}
renderChildren: function(data) {
var html = utils.template(this.templateString, data);
var template = document.createElement('div');
renderChildren(data) {
const html = utils.template(this.templateString, data);
const template = document.createElement('div');
template.innerHTML = html;
this.setImagesSrc(template);
DropDown.setImagesSrc(template);
template.firstChild.style.display = data.droplab_hidden ? 'none' : 'block';
return template.firstChild.outerHTML;
},
setImagesSrc: function(template) {
const images = [].slice.call(template.querySelectorAll('img[data-src]'));
images.forEach((image) => {
image.src = image.getAttribute('data-src');
image.removeAttribute('data-src');
});
},
}
show: function() {
show() {
if (!this.hidden) return;
this.list.style.display = 'block';
this.currentIndex = 0;
this.hidden = false;
},
}
hide: function() {
hide() {
if (this.hidden) return;
this.list.style.display = 'none';
this.currentIndex = 0;
this.hidden = true;
},
}
toggle: function () {
this.hidden ? this.show() : this.hide();
},
toggle() {
if (this.hidden) return this.show();
destroy: function() {
return this.hide();
}
destroy() {
this.hide();
this.list.removeEventListener('click', this.eventWrapper.clickEvent);
}
});
static setImagesSrc(template) {
const images = [...template.querySelectorAll('img[data-src]')];
images.forEach((image) => {
const img = image;
img.src = img.getAttribute('data-src');
img.removeAttribute('data-src');
});
}
}
export default DropDown;
/* eslint-disable */
import HookButton from './hook_button';
import HookInput from './hook_input';
import utils from './utils';
import Keyboard from './keyboard';
import { DATA_TRIGGER } from './constants';
var DropLab = function() {
this.ready = false;
this.hooks = [];
this.queuedData = [];
this.config = {};
class DropLab {
constructor() {
this.ready = false;
this.hooks = [];
this.queuedData = [];
this.config = {};
this.eventWrapper = {};
};
this.eventWrapper = {};
}
Object.assign(DropLab.prototype, {
loadStatic: function(){
var dropdownTriggers = [].slice.apply(document.querySelectorAll(`[${DATA_TRIGGER}]`));
loadStatic() {
const dropdownTriggers = [].slice.apply(document.querySelectorAll(`[${DATA_TRIGGER}]`));
this.addHooks(dropdownTriggers);
},
}
addData: function () {
var args = [].slice.apply(arguments);
this.applyArgs(args, '_addData');
},
addData(...args) {
this.applyArgs(args, 'processAddData');
}
setData: function() {
var args = [].slice.apply(arguments);
this.applyArgs(args, '_setData');
},
setData(...args) {
this.applyArgs(args, 'processSetData');
}
destroy: function() {
destroy() {
this.hooks.forEach(hook => hook.destroy());
this.hooks = [];
this.removeEvents();
},
}
applyArgs: function(args, methodName) {
if (this.ready) return this[methodName].apply(this, args);
applyArgs(args, methodName) {
if (this.ready) return this[methodName](...args);
this.queuedData = this.queuedData || [];
this.queuedData.push(args);
},
_addData: function(trigger, data) {
this._processData(trigger, data, 'addData');
},
return this.ready;
}
processAddData(trigger, data) {
this.processData(trigger, data, 'addData');
}
_setData: function(trigger, data) {
this._processData(trigger, data, 'setData');
},
processSetData(trigger, data) {
this.processData(trigger, data, 'setData');
}
_processData: function(trigger, data, methodName) {
processData(trigger, data, methodName) {
this.hooks.forEach((hook) => {
if (Array.isArray(trigger)) hook.list[methodName](trigger);
if (hook.trigger.id === trigger) hook.list[methodName](data);
});
},
}
addEvents: function() {
this.eventWrapper.documentClicked = this.documentClicked.bind(this)
addEvents() {
this.eventWrapper.documentClicked = this.documentClicked.bind(this);
document.addEventListener('click', this.eventWrapper.documentClicked);
},
}
documentClicked: function(e) {
documentClicked(e) {
let thisTag = e.target;
if (thisTag.tagName !== 'UL') thisTag = utils.closest(thisTag, 'UL');
if (utils.isDropDownParts(thisTag, this.hooks) || utils.isDropDownParts(e.target, this.hooks)) return;
if (utils.isDropDownParts(thisTag, this.hooks)) return;
if (utils.isDropDownParts(e.target, this.hooks)) return;
this.hooks.forEach(hook => hook.list.hide());
},
}
removeEvents: function(){
removeEvents() {
document.removeEventListener('click', this.eventWrapper.documentClicked);
},
changeHookList: function(trigger, list, plugins, config) {
const availableTrigger = typeof trigger === 'string' ? document.getElementById(trigger) : trigger;
}
changeHookList(trigger, list, plugins, config) {
const availableTrigger = typeof trigger === 'string' ? document.getElementById(trigger) : trigger;
this.hooks.forEach((hook, i) => {
hook.list.list.dataset.dropdownActive = false;
const aHook = hook;
aHook.list.list.dataset.dropdownActive = false;
if (hook.trigger !== availableTrigger) return;
if (aHook.trigger !== availableTrigger) return;
hook.destroy();
aHook.destroy();
this.hooks.splice(i, 1);
this.addHook(availableTrigger, list, plugins, config);
});
},
}
addHook: function(hook, list, plugins, config) {
addHook(hook, list, plugins, config) {
const availableHook = typeof hook === 'string' ? document.querySelector(hook) : hook;
let availableList;
......@@ -111,18 +111,18 @@ Object.assign(DropLab.prototype, {
this.hooks.push(new HookObject(availableHook, availableList, plugins, config));
return this;
},
}
addHooks: function(hooks, plugins, config) {
addHooks(hooks, plugins, config) {
hooks.forEach(hook => this.addHook(hook, null, plugins, config));
return this;
},
}
setConfig: function(obj){
setConfig(obj) {
this.config = obj;
},
}
fireReady: function() {
fireReady() {
const readyEvent = new CustomEvent('ready.dl', {
detail: {
dropdown: this,
......@@ -131,10 +131,14 @@ Object.assign(DropLab.prototype, {
document.dispatchEvent(readyEvent);
this.ready = true;
},
}
init: function (hook, list, plugins, config) {
hook ? this.addHook(hook, list, plugins, config) : this.loadStatic();
init(hook, list, plugins, config) {
if (hook) {
this.addHook(hook, list, plugins, config);
} else {
this.loadStatic();
}
this.addEvents();
......@@ -146,7 +150,7 @@ Object.assign(DropLab.prototype, {
this.queuedData = [];
return this;
},
});
}
}
export default DropLab;
/* eslint-disable */
import DropDown from './drop_down';
var Hook = function(trigger, list, plugins, config){
this.trigger = trigger;
this.list = new DropDown(list);
this.type = 'Hook';
this.event = 'click';
this.plugins = plugins || [];
this.config = config || {};
this.id = trigger.id;
};
Object.assign(Hook.prototype, {
addEvents: function(){},
constructor: Hook,
});
class Hook {
constructor(trigger, list, plugins, config) {
this.trigger = trigger;
this.list = new DropDown(list);
this.type = 'Hook';
this.event = 'click';
this.plugins = plugins || [];
this.config = config || {};
this.id = trigger.id;
}
}
export default Hook;
/* eslint-disable */
import Hook from './hook';
var HookButton = function(trigger, list, plugins, config) {
Hook.call(this, trigger, list, plugins, config);
this.type = 'button';
this.event = 'click';
class HookButton extends Hook {
constructor(trigger, list, plugins, config) {
super(trigger, list, plugins, config);
this.eventWrapper = {};
this.type = 'button';
this.event = 'click';
this.addEvents();
this.addPlugins();
};
this.eventWrapper = {};
HookButton.prototype = Object.create(Hook.prototype);
this.addEvents();
this.addPlugins();
}
Object.assign(HookButton.prototype, {
addPlugins: function() {
addPlugins() {
this.plugins.forEach(plugin => plugin.init(this));
},
}
clicked: function(e){
var buttonEvent = new CustomEvent('click.dl', {
clicked(e) {
const buttonEvent = new CustomEvent('click.dl', {
detail: {
hook: this,
},
bubbles: true,
cancelable: true
cancelable: true,
});
e.target.dispatchEvent(buttonEvent);
this.list.toggle();
},
}
addEvents: function(){
addEvents() {
this.eventWrapper.clicked = this.clicked.bind(this);
this.trigger.addEventListener('click', this.eventWrapper.clicked);
},
}
removeEvents: function(){
removeEvents() {
this.trigger.removeEventListener('click', this.eventWrapper.clicked);
},
}
restoreInitialState: function() {
restoreInitialState() {
this.list.list.innerHTML = this.list.initialState;
},
}
removePlugins: function() {
removePlugins() {
this.plugins.forEach(plugin => plugin.destroy());
},
}
destroy: function() {
destroy() {
this.restoreInitialState();
this.removeEvents();
this.removePlugins();
},
constructor: HookButton,
});
}
}
export default HookButton;
/* eslint-disable */
import Hook from './hook';
var HookInput = function(trigger, list, plugins, config) {
Hook.call(this, trigger, list, plugins, config);
class HookInput extends Hook {
constructor(trigger, list, plugins, config) {
super(trigger, list, plugins, config);
this.type = 'input';
this.event = 'input';
this.type = 'input';
this.event = 'input';
this.eventWrapper = {};
this.eventWrapper = {};
this.addEvents();
this.addPlugins();
};
this.addEvents();
this.addPlugins();
}
Object.assign(HookInput.prototype, {
addPlugins: function() {
addPlugins() {
this.plugins.forEach(plugin => plugin.init(this));
},
}
addEvents: function(){
addEvents() {
this.eventWrapper.mousedown = this.mousedown.bind(this);
this.eventWrapper.input = this.input.bind(this);
this.eventWrapper.keyup = this.keyup.bind(this);
......@@ -29,19 +27,19 @@ Object.assign(HookInput.prototype, {
this.trigger.addEventListener('input', this.eventWrapper.input);
this.trigger.addEventListener('keyup', this.eventWrapper.keyup);
this.trigger.addEventListener('keydown', this.eventWrapper.keydown);
},
}
removeEvents: function() {
removeEvents() {
this.hasRemovedEvents = true;
this.trigger.removeEventListener('mousedown', this.eventWrapper.mousedown);
this.trigger.removeEventListener('input', this.eventWrapper.input);
this.trigger.removeEventListener('keyup', this.eventWrapper.keyup);
this.trigger.removeEventListener('keydown', this.eventWrapper.keydown);
},
}
input: function(e) {
if(this.hasRemovedEvents) return;
input(e) {
if (this.hasRemovedEvents) return;
this.list.show();
......@@ -51,12 +49,12 @@ Object.assign(HookInput.prototype, {
text: e.target.value,
},
bubbles: true,
cancelable: true
cancelable: true,
});
e.target.dispatchEvent(inputEvent);
},
}
mousedown: function(e) {
mousedown(e) {
if (this.hasRemovedEvents) return;
const mouseEvent = new CustomEvent('mousedown.dl', {
......@@ -68,21 +66,21 @@ Object.assign(HookInput.prototype, {
cancelable: true,
});
e.target.dispatchEvent(mouseEvent);
},
}
keyup: function(e) {
keyup(e) {
if (this.hasRemovedEvents) return;
this.keyEvent(e, 'keyup.dl');
},
}
keydown: function(e) {
keydown(e) {
if (this.hasRemovedEvents) return;
this.keyEvent(e, 'keydown.dl');
},
}
keyEvent: function(e, eventName) {
keyEvent(e, eventName) {
this.list.show();
const keyEvent = new CustomEvent(eventName, {
......@@ -96,17 +94,17 @@ Object.assign(HookInput.prototype, {
cancelable: true,
});
e.target.dispatchEvent(keyEvent);
},
}
restoreInitialState: function() {
restoreInitialState() {
this.list.list.innerHTML = this.list.initialState;
},
}
removePlugins: function() {
removePlugins() {
this.plugins.forEach(plugin => plugin.destroy());
},
}
destroy: function() {
destroy() {
this.restoreInitialState();
this.removeEvents();
......@@ -114,6 +112,6 @@ Object.assign(HookInput.prototype, {
this.list.destroy();
}
});
}
export default HookInput;
This diff is collapsed.
......@@ -3,6 +3,7 @@
import EnvironmentsService from '../services/environments_service';
import environmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import '../../lib/utils/common_utils';
import eventHub from '../event_hub';
......@@ -12,6 +13,7 @@ export default {
components: {
environmentTable,
tablePagination,
loadingIcon,
},
data() {
......@@ -208,14 +210,11 @@ export default {
</div>
<div class="content-list environments-container">
<div
class="environments-list-loading text-center"
v-if="isLoading">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true" />
</div>
<loading-icon
label="Loading environments"
size="3"
v-if="isLoading"
/>
<div
class="blank-state blank-state-no-icon"
......
<script>
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
props: {
......@@ -11,6 +12,10 @@ export default {
},
},
components: {
loadingIcon,
},
data() {
return {
playIconSvg,
......@@ -60,10 +65,7 @@ export default {
<i
class="fa fa-caret-down"
aria-hidden="true"/>
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true"/>
<loading-icon v-if="isLoading" />
</span>
</button>
......
<script>
import Timeago from 'timeago.js';
import _ from 'underscore';
import '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
......@@ -60,7 +61,7 @@ export default {
hasLastDeploymentKey() {
if (this.model &&
this.model.last_deployment &&
!this.$options.isObjectEmpty(this.model.last_deployment)) {
!_.isEmpty(this.model.last_deployment)) {
return true;
}
return false;
......@@ -311,8 +312,8 @@ export default {
*/
deploymentHasUser() {
return this.model &&
!this.$options.isObjectEmpty(this.model.last_deployment) &&
!this.$options.isObjectEmpty(this.model.last_deployment.user);
!_.isEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment.user);
},
/**
......@@ -323,8 +324,8 @@ export default {
*/
deploymentUser() {
if (this.model &&
!this.$options.isObjectEmpty(this.model.last_deployment) &&
!this.$options.isObjectEmpty(this.model.last_deployment.user)) {
!_.isEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment.user)) {
return this.model.last_deployment.user;
}
return {};
......@@ -339,8 +340,8 @@ export default {
*/
shouldRenderBuildName() {
return !this.model.isFolder &&
!this.$options.isObjectEmpty(this.model.last_deployment) &&
!this.$options.isObjectEmpty(this.model.last_deployment.deployable);
!_.isEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment.deployable);
},
/**
......@@ -381,7 +382,7 @@ export default {
*/
shouldRenderDeploymentID() {
return !this.model.isFolder &&
!this.$options.isObjectEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment) &&
this.model.last_deployment.iid !== undefined;
},
......@@ -411,21 +412,6 @@ export default {
},
},
/**
* Helper to verify if certain given object are empty.
* Should be replaced by lodash _.isEmpty - https://lodash.com/docs/4.17.2#isEmpty
* @param {Object} object
* @returns {Bollean}
*/
isObjectEmpty(object) {
for (const key in object) { // eslint-disable-line
if (hasOwnProperty.call(object, key)) {
return false;
}
}
return true;
},
methods: {
onClickFolder() {
eventHub.$emit('toggleFolder', this.model, this.folderUrl);
......
......@@ -21,7 +21,6 @@ export default {
<a
class="btn monitoring-url has-tooltip"
data-container="body"
target="_blank"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"
:title="title"
......
......@@ -6,6 +6,7 @@
* Makes a post request when the button is clicked.
*/
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
props: {
......@@ -20,6 +21,10 @@ export default {
},
},
components: {
loadingIcon,
},
data() {
return {
isLoading: false,
......@@ -49,9 +54,6 @@ export default {
Rollback
</span>
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
<loading-icon v-if="isLoading" />
</button>
</template>
......@@ -4,6 +4,7 @@
* Used in environments table.
*/
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
props: {
......@@ -19,6 +20,10 @@ export default {
};
},
components: {
loadingIcon,
},
computed: {
title() {
return 'Stop';
......@@ -51,9 +56,6 @@ export default {
<i
class="fa fa-stop stop-env-icon"
aria-hidden="true" />
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
<loading-icon v-if="isLoading" />
</button>
</template>
......@@ -4,11 +4,13 @@
*/
import EnvironmentTableRowComponent from './environment_item.vue';
import DeployBoard from './deploy_board_component.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {
'environment-item': EnvironmentTableRowComponent,
DeployBoard,
loadingIcon,
},
props: {
......@@ -104,10 +106,8 @@ export default {
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<tr v-if="isLoadingFolderContent">
<td colspan="6" class="text-center">
<i
class="fa fa-spin fa-spinner fa-2x"
aria-hidden="true" />
<td colspan="6">
<loading-icon size="2" />
</td>
</tr>
......
......@@ -3,6 +3,7 @@
import EnvironmentsService from '../services/environments_service';
import environmentTable from '../components/environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor';
......@@ -11,6 +12,7 @@ export default {
components: {
environmentTable,
tablePagination,
loadingIcon,
},
data() {
......@@ -165,13 +167,12 @@ export default {
</div>
<div class="environments-container">
<div
class="environments-list-loading text-center"
v-if="isLoading">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"/>
</div>
<loading-icon
label="Loading environments"
v-if="isLoading"
size="3"
/>
<div
class="table-holder"
......
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */
import { isObject } from './lib/utils/type_utility';
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote;
......@@ -93,7 +94,7 @@ GitLabDropdownFilter = (function() {
// { prop: 'def' }
// ]
// }
if (gl.utils.isObject(data)) {
if (isObject(data)) {
results = {};
for (key in data) {
group = data[key];
......@@ -396,7 +397,7 @@ GitLabDropdown = (function() {
html = [this.noResults()];
} else {
// Handle array groups
if (gl.utils.isObject(data)) {
if (isObject(data)) {
html = [];
for (name in data) {
groupData = data[name];
......@@ -623,7 +624,12 @@ GitLabDropdown = (function() {
var link = document.createElement('a');
link.href = url;
link.textContent = text;
if (this.highlight) {
link.innerHTML = text;
} else {
link.textContent = text;
}
if (selected) {
link.className = 'is-active';
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
/* global UsersSelect */
/* global bp */
import Cookies from 'js-cookie';
import UsersSelect from './users_select';
(function() {
this.IssuableContext = (function() {
......@@ -47,7 +47,6 @@ import Cookies from 'js-cookie';
Cookies.set('collapsed_gutter', true);
}
});
$(".right-sidebar").niceScroll();
}
IssuableContext.prototype.initParticipants = function() {
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* global GitLab */
/* global UsersSelect */
/* global ZenMode */
/* global Autosave */
/* global GroupsSelect */
/* global dateFormat */
/* global Pikaday */
import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete';
(function() {
......
export default (newStateData, tasks) => {
const $tasks = $('#task_status');
const $tasksShort = $('#task_status_short');
const $issueableHeader = $('.issuable-header');
const tasksStates = { newState: null, currentState: null };
if ($tasks.length === 0) {
if (!(newStateData.task_status.indexOf('0 of 0') === 0)) {
$issueableHeader.append(`<span id="task_status">${newStateData.task_status}</span>`);
} else {
$issueableHeader.append('<span id="task_status"></span>');
}
} else {
tasksStates.newState = newStateData.task_status.indexOf('0 of 0') === 0;
tasksStates.currentState = tasks.indexOf('0 of 0') === 0;
}
if ($tasks.length !== 0 && !tasksStates.newState) {
$tasks.text(newStateData.task_status);
$tasksShort.text(newStateData.task_status);
} else if (tasksStates.currentState) {
$issueableHeader.append(`<span id="task_status">${newStateData.task_status}</span>`);
} else if (tasksStates.newState) {
$tasks.remove();
$tasksShort.remove();
}
};
<script>
import Visibility from 'visibilityjs';
import Poll from '../../lib/utils/poll';
import Service from '../services/index';
import Store from '../stores';
import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
export default {
props: {
endpoint: {
required: true,
type: String,
},
canUpdate: {
required: true,
type: Boolean,
},
issuableRef: {
type: String,
required: true,
},
initialTitle: {
type: String,
required: true,
},
initialDescriptionHtml: {
type: String,
required: false,
default: '',
},
initialDescriptionText: {
type: String,
required: false,
default: '',
},
},
data() {
const store = new Store({
titleHtml: this.initialTitle,
descriptionHtml: this.initialDescriptionHtml,
descriptionText: this.initialDescriptionText,
});
return {
store,
state: store.state,
};
},
components: {
descriptionComponent,
titleComponent,
},
created() {
const resource = new Service(this.endpoint);
const poll = new Poll({
resource,
method: 'getData',
successCallback: (res) => {
this.store.updateState(res.json());
},
errorCallback(err) {
throw new Error(err);
},
});
if (!Visibility.hidden()) {
poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
},
};
</script>
<template>
<div>
<title-component
:issuable-ref="issuableRef"
:title-html="state.titleHtml"
:title-text="state.titleText" />
<description-component
v-if="state.descriptionHtml"
:can-update="canUpdate"
:description-html="state.descriptionHtml"
:description-text="state.descriptionText"
:updated-at="state.updatedAt"
:task-status="state.taskStatus" />
</div>
</template>
<script>
import animateMixin from '../mixins/animate';
export default {
mixins: [animateMixin],
props: {
canUpdate: {
type: Boolean,
required: true,
},
descriptionHtml: {
type: String,
required: true,
},
descriptionText: {
type: String,
required: true,
},
updatedAt: {
type: String,
required: true,
},
taskStatus: {
type: String,
required: true,
},
},
data() {
return {
preAnimation: false,
pulseAnimation: false,
timeAgoEl: $('.js-issue-edited-ago'),
};
},
watch: {
descriptionHtml() {
this.animateChange();
this.$nextTick(() => {
const toolTipTime = gl.utils.formatDate(this.updatedAt);
this.timeAgoEl.attr('datetime', this.updatedAt)
.attr('title', toolTipTime)
.tooltip('fixTitle');
this.renderGFM();
});
},
taskStatus() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of (\d+)/);
const $issuableHeader = $('.issuable-meta');
const $tasks = $('#task_status', $issuableHeader);
const $tasksShort = $('#task_status_short', $issuableHeader);
if (taskRegexMatches) {
$tasks.text(this.taskStatus);
$tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`);
} else {
$tasks.text('');
$tasksShort.text('');
}
},
},
methods: {
renderGFM() {
$(this.$refs['gfm-entry-content']).renderGFM();
if (this.canUpdate) {
// eslint-disable-next-line no-new
new gl.TaskList({
dataType: 'issue',
fieldName: 'description',
selector: '.detail-page-description',
});
}
},
},
mounted() {
this.renderGFM();
},
};
</script>
<template>
<div
class="description"
:class="{
'js-task-list-container': canUpdate
}">
<div
class="wiki"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
v-html="descriptionHtml"
ref="gfm-content">
</div>
<textarea
class="hidden js-task-list-field"
v-if="descriptionText"
v-model="descriptionText">
</textarea>
</div>
</template>
<script>
import animateMixin from '../mixins/animate';
export default {
mixins: [animateMixin],
data() {
return {
preAnimation: false,
pulseAnimation: false,
titleEl: document.querySelector('title'),
};
},
props: {
issuableRef: {
type: String,
required: true,
},
titleHtml: {
type: String,
required: true,
},
titleText: {
type: String,
required: true,
},
},
watch: {
titleHtml() {
this.setPageTitle();
this.animateChange();
},
},
methods: {
setPageTitle() {
const currentPageTitleScope = this.titleEl.innerText.split('·');
currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `;
this.titleEl.textContent = currentPageTitleScope.join('·');
},
},
};
</script>
<template>
<h2
class="title"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
v-html="titleHtml"
>
</h2>
</template>
import Vue from 'vue';
import IssueTitle from './issue_title_description.vue';
import issuableApp from './components/app.vue';
import '../vue_shared/vue_resource_interceptor';
(() => {
const issueTitleData = document.querySelector('.issue-title-data').dataset;
const { canUpdateTasksClass, endpoint } = issueTitleData;
document.addEventListener('DOMContentLoaded', () => new Vue({
el: document.getElementById('js-issuable-app'),
components: {
issuableApp,
},
data() {
const issuableElement = this.$options.el;
const issuableTitleElement = issuableElement.querySelector('.title');
const issuableDescriptionElement = issuableElement.querySelector('.wiki');
const issuableDescriptionTextarea = issuableElement.querySelector('.js-task-list-field');
const {
canUpdate,
endpoint,
issuableRef,
} = issuableElement.dataset;
const vm = new Vue({
el: '.issue-title-entrypoint',
render: createElement => createElement(IssueTitle, {
return {
canUpdate: gl.utils.convertPermissionToBoolean(canUpdate),
endpoint,
issuableRef,
initialTitle: issuableTitleElement.innerHTML,
initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '',
initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '',
};
},
render(createElement) {
return createElement('issuable-app', {
props: {
canUpdateTasksClass,
endpoint,
canUpdate: this.canUpdate,
endpoint: this.endpoint,
issuableRef: this.issuableRef,
initialTitle: this.initialTitle,
initialDescriptionHtml: this.initialDescriptionHtml,
initialDescriptionText: this.initialDescriptionText,
},
}),
});
return vm;
})();
});
},
}));
<script>
import Visibility from 'visibilityjs';
import Poll from './../lib/utils/poll';
import Service from './services/index';
import tasks from './actions/tasks';
export default {
props: {
endpoint: {
required: true,
type: String,
},
canUpdateTasksClass: {
required: true,
type: String,
},
},
data() {
const resource = new Service(this.$http, this.endpoint);
const poll = new Poll({
resource,
method: 'getTitle',
successCallback: (res) => {
this.renderResponse(res);
},
errorCallback: (err) => {
throw new Error(err);
},
});
return {
poll,
apiData: {},
tasks: '0 of 0',
title: null,
titleText: '',
titleFlag: {
pre: true,
pulse: false,
},
description: null,
descriptionText: '',
descriptionChange: false,
descriptionFlag: {
pre: true,
pulse: false,
},
timeAgoEl: $('.issue_edited_ago'),
titleEl: document.querySelector('title'),
};
},
methods: {
updateFlag(key, toggle) {
this[key].pre = toggle;
this[key].pulse = !toggle;
},
renderResponse(res) {
this.apiData = res.json();
this.triggerAnimation();
},
updateTaskHTML() {
tasks(this.apiData, this.tasks);
},
elementsToVisualize(noTitleChange, noDescriptionChange) {
if (!noTitleChange) {
this.titleText = this.apiData.title_text;
this.updateFlag('titleFlag', true);
}
if (!noDescriptionChange) {
// only change to true when we need to bind TaskLists the html of description
this.descriptionChange = true;
this.updateTaskHTML();
this.tasks = this.apiData.task_status;
this.updateFlag('descriptionFlag', true);
}
},
setTabTitle() {
const currentTabTitleScope = this.titleEl.innerText.split('·');
currentTabTitleScope[0] = `${this.titleText} (#${this.apiData.issue_number}) `;
this.titleEl.innerText = currentTabTitleScope.join('·');
},
animate(title, description) {
this.title = title;
this.description = description;
this.setTabTitle();
this.$nextTick(() => {
this.updateFlag('titleFlag', false);
this.updateFlag('descriptionFlag', false);
});
},
triggerAnimation() {
// always reset to false before checking the change
this.descriptionChange = false;
const { title, description } = this.apiData;
this.descriptionText = this.apiData.description_text;
const noTitleChange = this.title === title;
const noDescriptionChange = this.description === description;
/**
* since opacity is changed, even if there is no diff for Vue to update
* we must check the title/description even on a 304 to ensure no visual change
*/
if (noTitleChange && noDescriptionChange) return;
this.elementsToVisualize(noTitleChange, noDescriptionChange);
this.animate(title, description);
},
updateEditedTimeAgo() {
const toolTipTime = gl.utils.formatDate(this.apiData.updated_at);
this.timeAgoEl.attr('datetime', this.apiData.updated_at);
this.timeAgoEl.attr('title', toolTipTime).tooltip('fixTitle');
},
},
created() {
if (!Visibility.hidden()) {
this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
},
updated() {
// if new html is injected (description changed) - bind TaskList and call renderGFM
if (this.descriptionChange) {
this.updateEditedTimeAgo();
$(this.$refs['issue-content-container-gfm-entry']).renderGFM();
const tl = new gl.TaskList({
dataType: 'issue',
fieldName: 'description',
selector: '.detail-page-description',
});
return tl && null;
}
return null;
},
};
</script>
<template>
<div>
<h2
class="title"
:class="{ 'issue-realtime-pre-pulse': titleFlag.pre, 'issue-realtime-trigger-pulse': titleFlag.pulse }"
ref="issue-title"
v-html="title"
>
</h2>
<div
class="description is-task-list-enabled"
:class="canUpdateTasksClass"
v-if="description"
>
<div
class="wiki"
:class="{ 'issue-realtime-pre-pulse': descriptionFlag.pre, 'issue-realtime-trigger-pulse': descriptionFlag.pulse }"
v-html="description"
ref="issue-content-container-gfm-entry"
>
</div>
<textarea
class="hidden js-task-list-field"
v-if="descriptionText"
>{{descriptionText}}</textarea>
</div>
</div>
</template>
export default {
methods: {
animateChange() {
this.preAnimation = true;
this.pulseAnimation = false;
this.$nextTick(() => {
this.preAnimation = false;
this.pulseAnimation = true;
});
},
},
};
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class Service {
constructor(resource, endpoint) {
this.resource = resource;
constructor(endpoint) {
this.endpoint = endpoint;
this.resource = Vue.resource(this.endpoint);
}
getTitle() {
return this.resource.get(this.endpoint);
getData() {
return this.resource.get();
}
}
export default class Store {
constructor({
titleHtml,
descriptionHtml,
descriptionText,
}) {
this.state = {
titleHtml,
titleText: '',
descriptionHtml,
descriptionText,
taskStatus: '',
updatedAt: '',
};
}
updateState(data) {
this.state.titleHtml = data.title;
this.state.titleText = data.title_text;
this.state.descriptionHtml = data.description;
this.state.descriptionText = data.description_text;
this.state.taskStatus = data.task_status;
this.state.updatedAt = data.updated_at;
}
}
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */
import _ from 'underscore';
(function() {
var hideEndFade;
......@@ -45,4 +46,13 @@
}
});
});
function applyScrollNavClass() {
const scrollOpacityHeight = 40;
$('.navbar-border').css('opacity', Math.min($(window).scrollTop() / scrollOpacityHeight, 1));
}
$(() => {
$(window).on('scroll', _.throttle(applyScrollNavClass, 100));
});
}).call(window);
const AjaxCache = {
internalStorage: { },
class AjaxCache {
constructor() {
this.internalStorage = { };
this.pendingRequests = { };
}
get(endpoint) {
return this.internalStorage[endpoint];
},
}
hasData(endpoint) {
return Object.prototype.hasOwnProperty.call(this.internalStorage, endpoint);
},
purge(endpoint) {
}
remove(endpoint) {
delete this.internalStorage[endpoint];
},
}
retrieve(endpoint) {
if (AjaxCache.hasData(endpoint)) {
return Promise.resolve(AjaxCache.get(endpoint));
if (this.hasData(endpoint)) {
return Promise.resolve(this.get(endpoint));
}
return new Promise((resolve, reject) => {
$.ajax(endpoint) // eslint-disable-line promise/catch-or-return
.then(data => resolve(data),
(jqXHR, textStatus, errorThrown) => {
const error = new Error(`${endpoint}: ${errorThrown}`);
error.textStatus = textStatus;
reject(error);
},
);
})
.then((data) => { this.internalStorage[endpoint] = data; })
.then(() => AjaxCache.get(endpoint));
},
};
export default AjaxCache;
let pendingRequest = this.pendingRequests[endpoint];
if (!pendingRequest) {
pendingRequest = new Promise((resolve, reject) => {
// jQuery 2 is not Promises/A+ compatible (missing catch)
$.ajax(endpoint) // eslint-disable-line promise/catch-or-return
.then(data => resolve(data),
(jqXHR, textStatus, errorThrown) => {
const error = new Error(`${endpoint}: ${errorThrown}`);
error.textStatus = textStatus;
reject(error);
},
);
})
.then((data) => {
this.internalStorage[endpoint] = data;
delete this.pendingRequests[endpoint];
})
.catch((error) => {
delete this.pendingRequests[endpoint];
throw error;
});
this.pendingRequests[endpoint] = pendingRequest;
}
return pendingRequest.then(() => this.get(endpoint));
}
}
export default new AjaxCache();
......@@ -135,7 +135,10 @@
gl.utils.getUrlParamsArray = function () {
// We can trust that each param has one & since values containing & will be encoded
// Remove the first character of search as it is always ?
return window.location.search.slice(1).split('&');
return window.location.search.slice(1).split('&').map((param) => {
const split = param.split('=');
return [decodeURI(split[0]), split[1]].join('=');
});
};
gl.utils.isMetaKey = function(e) {
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, no-return-assign, max-len */
(function() {
(function(w) {
var base;
if (w.gl == null) {
w.gl = {};
}
if ((base = w.gl).utils == null) {
base.utils = {};
}
return w.gl.utils.isObject = function(obj) {
return (obj != null) && (obj.constructor === Object);
};
})(window);
}).call(window);
// eslint-disable-next-line import/prefer-default-export
export const isObject = obj => obj && obj.constructor === Object;
......@@ -59,7 +59,6 @@ import './lib/utils/datetime_utility';
import './lib/utils/notify';
import './lib/utils/pretty_time';
import './lib/utils/text_utility';
import './lib/utils/type_utility';
import './lib/utils/url_utility';
// u2f
......
......@@ -369,18 +369,26 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
initAffix() {
const $tabs = $('.js-tabs-affix');
const $fixedNav = $('.navbar-gitlab');
// Screen space on small screens is usually very sparse
// So we dont affix the tabs on these
if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return;
/**
If the browser does not support position sticky, it returns the position as static.
If the browser does support sticky, then we allow the browser to handle it, if not
then we default back to Bootstraps affix
**/
if ($tabs.css('position') !== 'static') return;
const $diffTabs = $('#diff-notes-app');
$tabs.off('affix.bs.affix affix-top.bs.affix')
.affix({
offset: {
top: () => (
$diffTabs.offset().top - $tabs.height()
$diffTabs.offset().top - $tabs.height() - $fixedNav.height()
),
},
})
......
......@@ -18,12 +18,11 @@
}
$els.each(function(i, dropdown) {
var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove;
var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, defaultNo, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, selectedMilestoneDefault, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
milestonesUrl = $dropdown.data('milestones');
issueUpdateURL = $dropdown.data('issueUpdate');
selectedMilestone = $dropdown.data('selected');
showNo = $dropdown.data('show-no');
showAny = $dropdown.data('show-any');
showMenuAbove = $dropdown.data('showMenuAbove');
......@@ -31,6 +30,7 @@
showStarted = $dropdown.data('show-started');
useId = $dropdown.data('use-id');
defaultLabel = $dropdown.data('default-label');
defaultNo = $dropdown.data('default-no');
issuableId = $dropdown.data('issuable-id');
abilityName = $dropdown.data('ability-name');
$selectbox = $dropdown.closest('.selectbox');
......@@ -38,6 +38,9 @@
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
selectedMilestoneDefault = (showAny ? '' : null);
selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault);
selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
if (issueUpdateURL) {
milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
......@@ -86,8 +89,18 @@
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
});
},
renderRow: function(milestone) {
return `
<li data-milestone-id="${milestone.name}">
<a href='#' class='dropdown-menu-milestone-link'>
${_.escape(milestone.title)}
</a>
</li>
`;
},
filterable: true,
search: {
fields: ['title']
......@@ -125,6 +138,14 @@
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
opened: function(e) {
const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar')) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
$(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
hideRow: function(milestone) {
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
......@@ -146,11 +167,13 @@
const { $el, e } = options;
let selected = options.selectedObj;
var data, isIssueIndex, isMRIndex, page, boardsStore;
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
if (!selected) return;
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
isSelecting = (selected.name !== selectedMilestone);
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
return;
......@@ -164,16 +187,11 @@
boardsStore[$dropdown.data('field-name')] = selected.name;
e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (selected.name != null) {
selectedMilestone = selected.name;
} else {
selectedMilestone = '';
}
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (selected.id !== -1) {
if (selected.id !== -1 && isSelecting) {
gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
id: selected.id,
title: selected.name
......
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */
import RefSelectDropdown from '~/ref_select_dropdown';
(function() {
this.NewBranchForm = (function() {
function NewBranchForm(form, availableRefs) {
......@@ -6,7 +8,7 @@
this.branchNameError = form.find('.js-branch-name-error');
this.name = form.find('.js-branch-name');
this.ref = form.find('#ref');
this.setupAvailableRefs(availableRefs);
new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new
this.setupRestrictions();
this.addBinding();
this.init();
......@@ -22,49 +24,6 @@
}
};
NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) {
var $branchSelect = $('.js-branch-select');
$branchSelect.glDropdown({
data: availableRefs,
filterable: true,
filterByText: true,
remote: false,
fieldName: $branchSelect.data('field-name'),
filterInput: 'input[type="search"]',
selectable: true,
isSelectable: function(branch, $el) {
return !$el.hasClass('is-active');
},
text: function(branch) {
return branch;
},
id: function(branch) {
return branch;
},
toggleLabel: function(branch) {
if (branch) {
return branch;
}
}
});
const $dropdownContainer = $branchSelect.closest('.dropdown');
const $fieldInput = $(`input[name="${$branchSelect.data('field-name')}"]`, $dropdownContainer);
const $filterInput = $('input[type="search"]', $dropdownContainer);
$filterInput.on('keyup', (e) => {
const keyCode = e.keyCode || e.which;
if (keyCode !== 13) return;
const text = $filterInput.val();
$fieldInput.val(text);
$('.dropdown-toggle-text', $branchSelect).text(text);
$dropdownContainer.removeClass('open');
});
};
NewBranchForm.prototype.setupRestrictions = function() {
var endsWith, invalid, single, startsWith;
startsWith = {
......
......@@ -48,6 +48,7 @@ const normalizeNewlines = function(str) {
this.keydownNoteText = this.keydownNoteText.bind(this);
this.toggleCommitList = this.toggleCommitList.bind(this);
this.postComment = this.postComment.bind(this);
this.clearFlashWrapper = this.clearFlash.bind(this);
this.notes_url = notes_url;
this.note_ids = note_ids;
......@@ -58,6 +59,7 @@ const normalizeNewlines = function(str) {
this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge"));
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
this.flashErrors = [];
this.cleanBinding();
this.addBinding();
......@@ -299,7 +301,7 @@ const normalizeNewlines = function(str) {
if (!noteEntity.valid) {
if (noteEntity.errors.commands_only) {
new Flash(noteEntity.errors.commands_only, 'notice', this.parentTimeline);
this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline);
this.refresh();
}
return;
......@@ -552,14 +554,14 @@ const normalizeNewlines = function(str) {
return this.renderNote(note);
};
Notes.prototype.addNoteError = ($form) => {
Notes.prototype.addNoteError = function($form) {
let formParentTimeline;
if ($form.hasClass('js-main-target-form')) {
formParentTimeline = $form.parents('.timeline');
} else if ($form.hasClass('js-discussion-note-form')) {
formParentTimeline = $form.closest('.discussion-notes').find('.notes');
}
return new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline);
return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline);
};
Notes.prototype.updateNoteError = $parentTimeline => new Flash('Your comment could not be updated! Please check your network connection and try again.');
......@@ -1114,6 +1116,15 @@ const normalizeNewlines = function(str) {
});
};
Notes.prototype.addFlash = function(...flashParams) {
this.flashErrors.push(new Flash(...flashParams));
};
Notes.prototype.clearFlash = function() {
this.flashErrors.forEach(flash => flash.flashContainer.remove());
this.flashErrors = [];
};
Notes.prototype.cleanForm = function($form) {
// Remove JS classes that are not needed here
$form
......@@ -1197,9 +1208,6 @@ const normalizeNewlines = function(str) {
<span class="hidden-xs">${currentUserFullname}</span>
<span class="note-headline-light">@${currentUsername}</span>
</a>
<span class="note-headline-light">
<i class="fa fa-spinner fa-spin" aria-label="Comment is being posted" aria-hidden="true"></i>
</span>
</div>
</div>
<div class="note-body">
......@@ -1295,6 +1303,8 @@ const normalizeNewlines = function(str) {
.then((note) => {
// Submission successful! remove placeholder
$notesContainer.find(`#${uniqueId}`).remove();
// Clear previous form errors
this.clearFlashWrapper();
// Check if this was discussion comment
if (isDiscussionForm) {
......
......@@ -24,9 +24,6 @@ export default {
};
},
computed: {
showUnsetWarning() {
return this.cronInterval === '';
},
intervalIsPreset() {
return _.contains(this.cronIntervalPresets, this.cronInterval);
},
......@@ -63,67 +60,75 @@ export default {
},
template: `
<div class="interval-pattern-form-group">
<input
id="custom"
class="label-light"
type="radio"
:name="inputNameAttribute"
:value="cronInterval"
:checked="isEditable"
@click="toggleCustomInput(true)"
/>
<div class="cron-preset-radio-input">
<input
id="custom"
class="label-light"
type="radio"
:name="inputNameAttribute"
:value="cronInterval"
:checked="isEditable"
@click="toggleCustomInput(true)"
/>
<label for="custom">
Custom
</label>
<label for="custom">
Custom
</label>
<span class="cron-syntax-link-wrap">
(<a :href="cronSyntaxUrl" target="_blank">Cron syntax</a>)
</span>
<span class="cron-syntax-link-wrap">
(<a :href="cronSyntaxUrl" target="_blank">Cron syntax</a>)
</span>
</div>
<input
id="every-day"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyDay"
@click="toggleCustomInput(false)"
/>
<div class="cron-preset-radio-input">
<input
id="every-day"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyDay"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-day">
Every day (at 4:00am)
</label>
<label class="label-light" for="every-day">
Every day (at 4:00am)
</label>
</div>
<input
id="every-week"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyWeek"
@click="toggleCustomInput(false)"
/>
<div class="cron-preset-radio-input">
<input
id="every-week"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyWeek"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-week">
Every week (Sundays at 4:00am)
</label>
<label class="label-light" for="every-week">
Every week (Sundays at 4:00am)
</label>
</div>
<input
id="every-month"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyMonth"
@click="toggleCustomInput(false)"
/>
<div class="cron-preset-radio-input">
<input
id="every-month"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyMonth"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-month">
Every month (on the 1st at 4:00am)
</label>
<label class="label-light" for="every-month">
Every month (on the 1st at 4:00am)
</label>
</div>
<div class="cron-interval-input-wrapper col-md-6">
<div class="cron-interval-input-wrapper">
<input
id="schedule_cron"
class="form-control inline cron-interval-input"
......@@ -135,9 +140,6 @@ export default {
:disabled="!isEditable"
/>
</div>
<span class="cron-unset-status col-md-3" v-if="showUnsetWarning">
Schedule not yet set
</span>
</div>
`,
};
......@@ -4,8 +4,10 @@ import illustrationSvg from '../icons/intro_illustration.svg';
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',
};
......@@ -28,13 +30,15 @@ export default {
<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.
<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
<!-- FIXME -->
<a href="random.com">pipeline schedules documentation</a>.
<a
:href="docsUrl"
target="_blank"
rel="nofollow">pipeline schedules documentation</a>. <!-- oneline to prevent extra space before period -->
</p>
</div>
</div>
......
......@@ -3,7 +3,7 @@ export default class TargetBranchDropdown {
this.$dropdown = $('.js-target-branch-dropdown');
this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text');
this.$input = $('#schedule_ref');
this.initialValue = this.$input.val();
this.initDefaultBranch();
this.initDropdown();
}
......@@ -29,13 +29,23 @@ export default class TargetBranchDropdown {
}
setDropdownToggle() {
if (this.initialValue) {
this.$dropdownToggle.text(this.initialValue);
const initialValue = this.$input.val();
this.$dropdownToggle.text(initialValue);
}
initDefaultBranch() {
const initialValue = this.$input.val();
const defaultBranch = this.$dropdown.data('defaultBranch');
if (!initialValue) {
this.$input.val(defaultBranch);
}
}
updateInputValue({ selectedObj, e }) {
e.preventDefault();
this.$input.val(selectedObj.name);
gl.pipelineScheduleFieldErrors.updateFormValidityState();
}
......
/* eslint-disable class-methods-use-this */
const defaultTimezone = 'UTC';
export default class TimezoneDropdown {
constructor() {
this.$dropdown = $('.js-timezone-dropdown');
this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text');
this.$input = $('#schedule_cron_timezone');
this.timezoneData = this.$dropdown.data('data');
this.initialValue = this.$input.val();
this.initDefaultTimezone();
this.initDropdown();
}
......@@ -42,12 +44,20 @@ export default class TimezoneDropdown {
return `[UTC ${this.formatUtcOffset(item.offset)}] ${item.name}`;
}
setDropdownToggle() {
if (this.initialValue) {
this.$dropdownToggle.text(this.initialValue);
initDefaultTimezone() {
const initialValue = this.$input.val();
if (!initialValue) {
this.$input.val(defaultTimezone);
}
}
setDropdownToggle() {
const initialValue = this.$input.val();
this.$dropdownToggle.text(initialValue);
}
updateInputValue({ selectedObj, e }) {
e.preventDefault();
this.$input.val(selectedObj.identifier);
......
import Vue from 'vue';
import PipelineSchedulesCallout from './components/pipeline_schedules_callout';
const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
document.addEventListener('DOMContentLoaded', () => {
new PipelineSchedulesCalloutComponent()
.$mount('#scheduling-pipelines-callout');
});
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipeline-schedules-callout',
components: {
'pipeline-schedules-callout': PipelineSchedulesCallout,
},
render(createElement) {
return createElement('pipeline-schedules-callout');
},
}));
......@@ -3,6 +3,7 @@
/* global Flash */
import '~/flash';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
props: {
......@@ -37,6 +38,10 @@ export default {
},
},
components: {
loadingIcon,
},
data() {
return {
isLoading: false,
......@@ -94,9 +99,6 @@ export default {
<i
:class="iconClass"
aria-hidden="true" />
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
v-if="isLoading" />
<loading-icon v-if="isLoading" />
</button>
</template>
......@@ -5,11 +5,13 @@
import PipelineService from '../../services/pipeline_service';
import PipelineStore from '../../stores/pipeline_store';
import stageColumnComponent from './stage_column_component.vue';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import '../../../flash';
export default {
components: {
stageColumnComponent,
loadingIcon,
},
data() {
......@@ -89,11 +91,10 @@
<div class="build-content middle-block js-pipeline-graph">
<div class="pipeline-visualization pipeline-graph">
<div class="text-center">
<i
<loading-icon
v-if="isLoading"
class="loading-icon fa fa-spin fa-spinner fa-3x"
aria-label="Loading"
aria-hidden="true" />
size="3"
/>
</div>
<ul
......
......@@ -29,7 +29,7 @@ export default {
</a>
<span
v-if="!user"
class="js-pipeline-url-api api monospace">
class="js-pipeline-url-api api">
API
</span>
<span
......
......@@ -3,6 +3,7 @@
import '~/flash';
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIconComponent from '../../vue_shared/components/loading_icon.vue';
export default {
props: {
......@@ -17,6 +18,10 @@ export default {
},
},
components: {
loadingIconComponent,
},
data() {
return {
playIconSvg,
......@@ -65,10 +70,7 @@ export default {
<i
class="fa fa-caret-down"
aria-hidden="true" />
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
<loading-icon v-if="isLoading" />
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
......
......@@ -15,6 +15,7 @@
/* global Flash */
import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
props: {
......@@ -38,6 +39,10 @@ export default {
};
},
components: {
loadingIcon,
},
updated() {
if (this.dropdownContent.length > 0) {
this.stopDropdownClickPropagation();
......@@ -153,15 +158,7 @@ export default {
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu">
<div
class="text-center"
v-if="isLoading">
<i
class="fa fa-spin fa-spinner"
aria-hidden="true"
aria-label="Loading">
</i>
</div>
<loading-icon v-if="isLoading"/>
<ul
v-else
......
import Visibility from 'visibilityjs';
import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub';
import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
import pipelinesTableComponent from '../vue_shared/components/pipelines_table';
import tablePagination from '../vue_shared/components/table_pagination.vue';
import EmptyState from './components/empty_state.vue';
import ErrorState from './components/error_state.vue';
import NavigationTabs from './components/navigation_tabs';
import NavigationControls from './components/nav_controls';
import emptyState from './components/empty_state.vue';
import errorState from './components/error_state.vue';
import navigationTabs from './components/navigation_tabs';
import navigationControls from './components/nav_controls';
import loadingIcon from '../vue_shared/components/loading_icon.vue';
import Poll from '../lib/utils/poll';
export default {
......@@ -19,11 +20,12 @@ export default {
components: {
tablePagination,
'pipelines-table-component': PipelinesTableComponent,
'empty-state': EmptyState,
'error-state': ErrorState,
'navigation-tabs': NavigationTabs,
'navigation-controls': NavigationControls,
pipelinesTableComponent,
emptyState,
errorState,
navigationTabs,
navigationControls,
loadingIcon,
},
data() {
......@@ -50,6 +52,7 @@ export default {
hasError: false,
isMakingRequest: false,
updateGraphDropdown: false,
hasMadeRequest: false,
};
},
......@@ -76,6 +79,7 @@ export default {
shouldRenderEmptyState() {
return !this.isLoading &&
!this.hasError &&
this.hasMadeRequest &&
!this.state.pipelines.length &&
(this.scope === 'all' || this.scope === null);
},
......@@ -148,6 +152,10 @@ export default {
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
} else {
// If tab is not visible we need to make the first request so we don't show the empty
// state without knowing if there are any pipelines
this.fetchPipelines();
}
Visibility.change(() => {
......@@ -200,6 +208,7 @@ export default {
this.isLoading = false;
this.updateGraphDropdown = true;
this.hasMadeRequest = true;
},
errorCallback() {
......@@ -244,13 +253,11 @@ export default {
<div class="content-list pipelines">
<div
class="realtime-loading"
v-if="isLoading">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true" />
</div>
<loading-icon
label="Loading Pipelines"
size="3"
v-if="isLoading"
/>
<empty-state
v-if="shouldRenderEmptyState"
......
class RefSelectDropdown {
constructor($dropdownButton, availableRefs) {
$dropdownButton.glDropdown({
data: availableRefs,
filterable: true,
filterByText: true,
remote: false,
fieldName: $dropdownButton.data('field-name'),
filterInput: 'input[type="search"]',
selectable: true,
isSelectable(branch, $el) {
return !$el.hasClass('is-active');
},
text(branch) {
return branch;
},
id(branch) {
return branch;
},
toggleLabel(branch) {
return branch;
},
});
const $dropdownContainer = $dropdownButton.closest('.dropdown');
const $fieldInput = $(`input[name="${$dropdownButton.data('field-name')}"]`, $dropdownContainer);
const $filterInput = $('input[type="search"]', $dropdownContainer);
$filterInput.on('keyup', (e) => {
const keyCode = e.keyCode || e.which;
if (keyCode !== 13) return;
const ref = $filterInput.val().trim();
if (ref === '') {
return;
}
$fieldInput.val(ref);
$('.dropdown-toggle-text', $dropdownButton).text(ref);
$dropdownContainer.removeClass('open');
});
}
}
export default RefSelectDropdown;
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */
/* global Flash */
/* global Api */
(function() {
......@@ -7,6 +8,7 @@
var $groupDropdown, $projectDropdown;
$groupDropdown = $('.js-search-group-dropdown');
$projectDropdown = $('.js-search-project-dropdown');
this.groupId = $groupDropdown.data('group-id');
this.eventListeners();
$groupDropdown.glDropdown({
selectable: true,
......@@ -46,14 +48,18 @@
search: {
fields: ['name']
},
data: function(term, callback) {
return Api.projects(term, { order_by: 'id' }, function(data) {
data.unshift({
name_with_namespace: 'Any'
});
data.splice(1, 0, 'divider');
return callback(data);
});
data: (term, callback) => {
this.getProjectsData(term)
.then((data) => {
data.unshift({
name_with_namespace: 'Any'
});
data.splice(1, 0, 'divider');
return data;
})
.then(data => callback(data))
.catch(() => new Flash('Error fetching projects'));
},
id: function(obj) {
return obj.id;
......@@ -95,6 +101,18 @@
return $('.js-search-input').val('').trigger('keyup').focus();
};
Search.prototype.getProjectsData = function(term) {
return new Promise((resolve) => {
if (this.groupId) {
Api.groupProjects(this.groupId, term, resolve);
} else {
Api.projects(term, {
order_by: 'id',
}, resolve);
}
});
};
return Search;
})();
}).call(window);
/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */
/* global UsersSelect */
import UsersSelect from './users_select';
class Todos {
constructor() {
......
This diff is collapsed.
......@@ -12,6 +12,15 @@ export default {
commitsText() {
return gl.text.pluralize('commit', this.mr.divergedCommitsCount);
},
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
// works around ClipboardJS limitations to allow the context-specific
// copy/pasting of plain text or GFM.
return JSON.stringify({
text: this.mr.sourceBranch,
gfm: `\`${this.mr.sourceBranch}\``,
});
},
},
methods: {
isBranchTitleLong(branchTitle) {
......@@ -61,29 +70,34 @@ export default {
</span>
</div>
<div class="normal">
<b>Request to merge</b>
<span
class="label-branch"
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.sourceBranch)}"
:title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
data-placement="bottom"
v-html="mr.sourceBranchLink"></span>
<button
class="btn btn-transparent btn-clipboard has-tooltip"
data-title="Copy branch name to clipboard"
:data-clipboard-text="mr.sourceBranch">
<i
aria-hidden="true"
class="fa fa-clipboard"></i>
</button>
<b>into</b>
<span
class="label-branch"
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}"
:title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
data-placement="bottom">
<a :href="mr.targetBranchCommitsPath">{{mr.targetBranch}}</a>
</span>
<strong>
Request to merge
<span
class="label-branch"
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.sourceBranch)}"
:title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
data-placement="bottom"
v-html="mr.sourceBranchLink"></span>
<button
class="btn btn-transparent btn-clipboard has-tooltip"
data-title="Copy branch name to clipboard"
:data-clipboard-text="branchNameClipboardData">
<i
aria-hidden="true"
class="fa fa-clipboard"></i>
</button>
into
<span
class="label-branch"
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}"
:title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
data-placement="bottom">
<a
:href="mr.targetBranchPath">
{{mr.targetBranch}}
</a>
</span>
</strong>
<span
v-if="shouldShowCommitsBehindText"
class="diverged-commits-count">
......
......@@ -26,9 +26,6 @@ export default {
status() {
return this.mr.pipeline.details.status || {};
},
statusPath() {
return this.status ? this.status.details_path : '';
},
},
template: `
<div class="mr-widget-heading">
......@@ -47,7 +44,7 @@ export default {
<div>
<a
class="icon-link"
:href="statusPath">
:href="this.status.details_path">
<ci-icon :status="status" />
</a>
</div>
......@@ -57,6 +54,9 @@ export default {
:href="mr.pipeline.path"
class="pipeline-id">#{{mr.pipeline.id}}</a>
{{mr.pipeline.details.status.label}}
</span>
<span
v-if="mr.pipeline.details.stages.length > 0">
with {{stageText}}
</span>
<div class="mr-widget-pipeline-graph">
......@@ -73,7 +73,7 @@ export default {
for
<a
:href="mr.pipeline.commit.commit_path"
class="monospace js-commit-link">
class="commit-sha js-commit-link">
{{mr.pipeline.commit.short_id}}</a>.
</span>
<span
......
......@@ -20,7 +20,7 @@ export default {
<p>
The changes were not merged into
<a
:href="mr.targetBranchCommitsPath"
:href="mr.targetBranchPath"
class="label-branch">
{{mr.targetBranch}}</a>.
</p>
......
......@@ -16,7 +16,7 @@ export default {
The changes will be merged into
<span class="label-branch">
<a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
</span>
</span>.
</p>
</section>
</div>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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