Commit 7dabca1b authored by Luke "Jared" Bennett's avatar Luke "Jared" Bennett

Merge branch 'master' into droplab-templating-xss-fix

parents a695b855 c7680264
...@@ -14,7 +14,8 @@ ...@@ -14,7 +14,8 @@
"plugins": [ "plugins": [
"filenames", "filenames",
"import", "import",
"html" "html",
"promise"
], ],
"settings": { "settings": {
"html/html-extensions": [".html", ".html.raw", ".vue"], "html/html-extensions": [".html", ".html.raw", ".vue"],
...@@ -26,6 +27,7 @@ ...@@ -26,6 +27,7 @@
}, },
"rules": { "rules": {
"filenames/match-regex": [2, "^[a-z0-9_]+$"], "filenames/match-regex": [2, "^[a-z0-9_]+$"],
"no-multiple-empty-lines": ["error", { "max": 1 }] "no-multiple-empty-lines": ["error", { "max": 1 }],
"promise/catch-or-return": "error"
} }
} }
...@@ -201,7 +201,13 @@ rake config_lint: *exec ...@@ -201,7 +201,13 @@ rake config_lint: *exec
rake brakeman: *exec rake brakeman: *exec
rake flay: *exec rake flay: *exec
license_finder: *exec license_finder: *exec
rake downtime_check: *exec rake downtime_check:
<<: *exec
except:
- master
- tags
- /^[\d-]+-stable(-ee)?$/
rake ee_compat_check: rake ee_compat_check:
<<: *exec <<: *exec
only: only:
...@@ -278,7 +284,6 @@ rake karma: ...@@ -278,7 +284,6 @@ rake karma:
cache: cache:
paths: paths:
- vendor/ruby - vendor/ruby
- node_modules
stage: test stage: test
<<: *use-db <<: *use-db
<<: *dedicated-runner <<: *dedicated-runner
...@@ -377,9 +382,6 @@ coverage: ...@@ -377,9 +382,6 @@ coverage:
lint:javascript: lint:javascript:
<<: *dedicated-runner <<: *dedicated-runner
cache:
paths:
- node_modules/
stage: test stage: test
before_script: [] before_script: []
script: script:
...@@ -387,9 +389,6 @@ lint:javascript: ...@@ -387,9 +389,6 @@ lint:javascript:
lint:javascript:report: lint:javascript:report:
<<: *dedicated-runner <<: *dedicated-runner
cache:
paths:
- node_modules/
stage: post-test stage: post-test
before_script: [] before_script: []
script: script:
......
This diff is collapsed.
...@@ -57,16 +57,16 @@ star, smile, etc.). Some good tips about code reviews can be found in our ...@@ -57,16 +57,16 @@ star, smile, etc.). Some good tips about code reviews can be found in our
[Code Review Guidelines]: https://docs.gitlab.com/ce/development/code_review.html [Code Review Guidelines]: https://docs.gitlab.com/ce/development/code_review.html
## Feature Freeze ## Feature freeze on the 7th for the release on the 22nd
After the 7th (Pacific Standard Time Zone) of each month, RC1 of the upcoming release is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it. After the 7th (Pacific Standard Time Zone) of each month, RC1 of the upcoming release (to be shipped on the 22nd) is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it.
Merge requests may still be merged into master during this period, Merge requests may still be merged into master during this period,
but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch. but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch.
By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things. By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things.
### Between the 1st and the 7th ### Between the 1st and the 7th
These types of merge requests need special consideration: These types of merge requests for the upcoming release need special consideration:
* **Large features**: a large feature is one that is highlighted in the kick-off * **Large features**: a large feature is one that is highlighted in the kick-off
and the release blogpost; typically this will have its own channel in Slack and the release blogpost; typically this will have its own channel in Slack
...@@ -114,14 +114,15 @@ subsequent EE merge, as we often merge a lot to CE on the release date. For more ...@@ -114,14 +114,15 @@ subsequent EE merge, as we often merge a lot to CE on the release date. For more
information, see information, see
[limit conflicts with EE when developing on CE][limit_ee_conflicts]. [limit conflicts with EE when developing on CE][limit_ee_conflicts].
### Between the 7th and the 22nd ### After the 7th
Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release) Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release)
and security issues will be cherry-picked into the stable branch. and security issues will be cherry-picked into the stable branch.
Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch. Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch.
These fixes will be released in the next RC (before the 22nd) or patch release (after the 22nd). These fixes will be shipped in the next RC for that release if it is before the 22nd.
If the fixes are are completed on or after the 22nd, they will be shipped in a patch for that release.
If you think a merge request should go into the upcoming release even though it does not meet these requirements, If you think a merge request should go into an RC or patch even though it does not meet these requirements,
you can ask for an exception to be made. Exceptions require sign-off from 3 people besides the developer: you can ask for an exception to be made. Exceptions require sign-off from 3 people besides the developer:
1. a Release Manager 1. a Release Manager
......
...@@ -73,7 +73,7 @@ One small thing you also have to do when installing it yourself is to copy the e ...@@ -73,7 +73,7 @@ One small thing you also have to do when installing it yourself is to copy the e
cp config/unicorn.rb.example.development config/unicorn.rb cp config/unicorn.rb.example.development config/unicorn.rb
Instructions on how to start GitLab and how to run the tests can be found in the [development section of the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit#development). Instructions on how to start GitLab and how to run the tests can be found in the [getting started section of the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit#getting-started).
## Software stack ## Software stack
......
9.1.0-pre 9.2.0-pre
...@@ -239,6 +239,9 @@ AwardsHandler ...@@ -239,6 +239,9 @@ AwardsHandler
if (menu) { if (menu) {
menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish')); menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish'));
} }
}).catch((err) => {
emojiContentElement.insertAdjacentHTML('beforeend', '<p>We encountered an error while adding the remaining categories</p>');
throw new Error(`Error occurred in addRemainingEmojiMenuCategories: ${err.message}`);
}); });
}; };
......
...@@ -22,6 +22,7 @@ $(() => { ...@@ -22,6 +22,7 @@ $(() => {
} }
$('body').on('click', '.js-toggle-button', function toggleButton(e) { $('body').on('click', '.js-toggle-button', function toggleButton(e) {
e.target.classList.toggle('open');
toggleContainer($(this).closest('.js-toggle-container')); toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.currentTarget.tagName.toLowerCase(); const targetTag = e.currentTarget.tagName.toLowerCase();
......
...@@ -35,7 +35,7 @@ export default class BlobFileDropzone { ...@@ -35,7 +35,7 @@ export default class BlobFileDropzone {
this.removeFile(file); this.removeFile(file);
}); });
this.on('sending', function (file, xhr, formData) { this.on('sending', function (file, xhr, formData) {
formData.append('target_branch', form.find('input[name="target_branch"]').val()); formData.append('branch_name', form.find('input[name="branch_name"]').val());
formData.append('create_merge_request', form.find('.js-create-merge-request').val()); formData.append('create_merge_request', form.find('.js-create-merge-request').val());
formData.append('commit_message', form.find('.js-commit-message').val()); formData.append('commit_message', form.find('.js-commit-message').val());
}); });
......
function BlobForkSuggestion(openButton, cancelButton, suggestionSection) { const defaults = {
if (openButton) { // Buttons that will show the `suggestionSections`
openButton.addEventListener('click', () => { // has `data-fork-path`, and `data-action`
openButtons: [],
// Update the href(from `openButton` -> `data-fork-path`)
// whenever a `openButton` is clicked
forkButtons: [],
// Buttons to hide the `suggestionSections`
cancelButtons: [],
// Section to show/hide
suggestionSections: [],
// Pieces of text that need updating depending on the action, `edit`, `replace`, `delete`
actionTextPieces: [],
};
class BlobForkSuggestion {
constructor(options) {
this.elementMap = Object.assign({}, defaults, options);
this.onClickWrapper = this.onClick.bind(this);
document.addEventListener('click', this.onClickWrapper);
}
showSuggestionSection(forkPath, action = 'edit') {
[].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => {
suggestionSection.classList.remove('hidden'); suggestionSection.classList.remove('hidden');
}); });
[].forEach.call(this.elementMap.forkButtons, (forkButton) => {
forkButton.setAttribute('href', forkPath);
});
[].forEach.call(this.elementMap.actionTextPieces, (actionTextPiece) => {
// eslint-disable-next-line no-param-reassign
actionTextPiece.textContent = action;
});
} }
if (cancelButton) { hideSuggestionSection() {
cancelButton.addEventListener('click', () => { [].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => {
suggestionSection.classList.add('hidden'); suggestionSection.classList.add('hidden');
}); });
} }
onClick(e) {
const el = e.target;
if ([].includes.call(this.elementMap.openButtons, el)) {
const { forkPath, action } = el.dataset;
this.showSuggestionSection(forkPath, action);
}
if ([].includes.call(this.elementMap.cancelButtons, el)) {
this.hideSuggestionSection();
}
}
destroy() {
document.removeEventListener('click', this.onClickWrapper);
}
} }
export default BlobForkSuggestion; export default BlobForkSuggestion;
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ /* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
/* global BoardService */ /* global BoardService */
/* global Flash */
import Vue from 'vue'; import Vue from 'vue';
import VueResource from 'vue-resource'; import VueResource from 'vue-resource';
...@@ -93,7 +94,7 @@ $(() => { ...@@ -93,7 +94,7 @@ $(() => {
Store.addBlankState(); Store.addBlankState();
this.loading = false; this.loading = false;
}); }).catch(() => new Flash('An error occurred. Please try again.'));
}, },
methods: { methods: {
updateTokens() { updateTokens() {
......
...@@ -57,12 +57,15 @@ export default { ...@@ -57,12 +57,15 @@ export default {
}, },
loadNextPage() { loadNextPage() {
const getIssues = this.list.nextPage(); const getIssues = this.list.nextPage();
const loadingDone = () => {
this.list.loadingMore = false;
};
if (getIssues) { if (getIssues) {
this.list.loadingMore = true; this.list.loadingMore = true;
getIssues.then(() => { getIssues
this.list.loadingMore = false; .then(loadingDone)
}); .catch(loadingDone);
} }
}, },
toggleForm() { toggleForm() {
......
...@@ -51,11 +51,13 @@ gl.issueBoards.IssuesModal = Vue.extend({ ...@@ -51,11 +51,13 @@ gl.issueBoards.IssuesModal = Vue.extend({
showAddIssuesModal() { showAddIssuesModal() {
if (this.showAddIssuesModal && !this.issues.length) { if (this.showAddIssuesModal && !this.issues.length) {
this.loading = true; this.loading = true;
const loadingDone = () => {
this.loading = false;
};
this.loadIssues() this.loadIssues()
.then(() => { .then(loadingDone)
this.loading = false; .catch(loadingDone);
});
} else if (!this.showAddIssuesModal) { } else if (!this.showAddIssuesModal) {
this.issues = []; this.issues = [];
this.selectedIssues = []; this.selectedIssues = [];
...@@ -67,11 +69,13 @@ gl.issueBoards.IssuesModal = Vue.extend({ ...@@ -67,11 +69,13 @@ gl.issueBoards.IssuesModal = Vue.extend({
if (this.$el.tagName) { if (this.$el.tagName) {
this.page = 1; this.page = 1;
this.filterLoading = true; this.filterLoading = true;
const loadingDone = () => {
this.filterLoading = false;
};
this.loadIssues(true) this.loadIssues(true)
.then(() => { .then(loadingDone)
this.filterLoading = false; .catch(loadingDone);
});
} }
}, },
deep: true, deep: true,
......
/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var */ /* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var,
promise/catch-or-return */
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {}; window.gl.issueBoards = window.gl.issueBoards || {};
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
/* global ListLabel */ /* global ListLabel */
import queryData from '../utils/query_data'; import queryData from '../utils/query_data';
const PER_PAGE = 20;
class List { class List {
constructor (obj) { constructor (obj) {
this.id = obj.id; this.id = obj.id;
...@@ -58,7 +60,9 @@ class List { ...@@ -58,7 +60,9 @@ class List {
nextPage () { nextPage () {
if (this.issuesSize > this.issues.length) { if (this.issuesSize > this.issues.length) {
this.page += 1; if (this.issues.length / PER_PAGE >= 1) {
this.page += 1;
}
return this.getIssues(false); return this.getIssues(false);
} }
...@@ -145,10 +149,7 @@ class List { ...@@ -145,10 +149,7 @@ class List {
} }
updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) { updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) {
gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid) gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid);
.then(() => {
listFrom.getIssues(false);
});
} }
findIssue (id) { findIssue (id) {
......
...@@ -36,6 +36,9 @@ gl.issueBoards.BoardsStore = { ...@@ -36,6 +36,9 @@ gl.issueBoards.BoardsStore = {
.save() .save()
.then(() => { .then(() => {
this.state.lists = _.sortBy(this.state.lists, 'position'); this.state.lists = _.sortBy(this.state.lists, 'position');
})
.catch(() => {
// https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
}); });
this.removeBlankState(); this.removeBlankState();
}, },
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
consistent-return, prefer-rest-params */ consistent-return, prefer-rest-params */
/* global Breakpoints */ /* global Breakpoints */
import { bytesToKiB } from './lib/utils/number_utils';
const bind = function (fn, me) { return function () { return fn.apply(me, arguments); }; }; const bind = function (fn, me) { return function () { return fn.apply(me, arguments); }; };
const AUTO_SCROLL_OFFSET = 75; const AUTO_SCROLL_OFFSET = 75;
const DOWN_BUILD_TRACE = '#down-build-trace'; const DOWN_BUILD_TRACE = '#down-build-trace';
...@@ -20,6 +22,7 @@ window.Build = (function () { ...@@ -20,6 +22,7 @@ window.Build = (function () {
this.state = this.options.logState; this.state = this.options.logState;
this.buildStage = this.options.buildStage; this.buildStage = this.options.buildStage;
this.$document = $(document); this.$document = $(document);
this.logBytes = 0;
this.updateDropdown = bind(this.updateDropdown, this); this.updateDropdown = bind(this.updateDropdown, this);
...@@ -98,15 +101,22 @@ window.Build = (function () { ...@@ -98,15 +101,22 @@ window.Build = (function () {
if (log.append) { if (log.append) {
$buildContainer.append(log.html); $buildContainer.append(log.html);
this.logBytes += log.size;
} else { } else {
$buildContainer.html(log.html); $buildContainer.html(log.html);
if (log.truncated) { this.logBytes = log.size;
$('.js-truncated-info-size').html(` ${log.size} `); }
this.$truncatedInfo.removeClass('hidden');
this.initAffixTruncatedInfo(); // if the incremental sum of logBytes we received is less than the total
} else { // we need to show a message warning the user about that.
this.$truncatedInfo.addClass('hidden'); if (this.logBytes < log.total) {
} // size is in bytes, we need to calculate KiB
const size = bytesToKiB(this.logBytes);
$('.js-truncated-info-size').html(`${size}`);
this.$truncatedInfo.removeClass('hidden');
this.initAffixTruncatedInfo();
} else {
this.$truncatedInfo.addClass('hidden');
} }
this.checkAutoscroll(); this.checkAutoscroll();
......
import CANCELED_SVG from 'icons/_icon_status_canceled_borderless.svg';
import CREATED_SVG from 'icons/_icon_status_created_borderless.svg';
import FAILED_SVG from 'icons/_icon_status_failed_borderless.svg';
import MANUAL_SVG from 'icons/_icon_status_manual_borderless.svg';
import PENDING_SVG from 'icons/_icon_status_pending_borderless.svg';
import RUNNING_SVG from 'icons/_icon_status_running_borderless.svg';
import SKIPPED_SVG from 'icons/_icon_status_skipped_borderless.svg';
import SUCCESS_SVG from 'icons/_icon_status_success_borderless.svg';
import WARNING_SVG from 'icons/_icon_status_warning_borderless.svg';
const StatusIconEntityMap = {
icon_status_canceled: CANCELED_SVG,
icon_status_created: CREATED_SVG,
icon_status_failed: FAILED_SVG,
icon_status_manual: MANUAL_SVG,
icon_status_pending: PENDING_SVG,
icon_status_running: RUNNING_SVG,
icon_status_skipped: SKIPPED_SVG,
icon_status_success: SUCCESS_SVG,
icon_status_warning: WARNING_SVG,
};
export {
CANCELED_SVG,
CREATED_SVG,
FAILED_SVG,
MANUAL_SVG,
PENDING_SVG,
RUNNING_SVG,
SKIPPED_SVG,
SUCCESS_SVG,
WARNING_SVG,
StatusIconEntityMap as default,
};
import Vue from 'vue'; import Vue from 'vue';
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import PipelinesTableComponent from '../../vue_shared/components/pipelines_table'; import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
import PipelinesService from '../../vue_pipelines_index/services/pipelines_service'; import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store'; import PipelineStore from '../../pipelines/stores/pipelines_store';
import eventHub from '../../vue_pipelines_index/event_hub'; import eventHub from '../../pipelines/event_hub';
import EmptyState from '../../vue_pipelines_index/components/empty_state.vue'; import EmptyState from '../../pipelines/components/empty_state.vue';
import ErrorState from '../../vue_pipelines_index/components/error_state.vue'; import ErrorState from '../../pipelines/components/error_state.vue';
import '../../lib/utils/common_utils'; import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor'; import '../../vue_shared/vue_resource_interceptor';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
...@@ -55,7 +55,15 @@ export default Vue.component('pipelines-table', { ...@@ -55,7 +55,15 @@ export default Vue.component('pipelines-table', {
}, },
shouldRenderEmptyState() { shouldRenderEmptyState() {
return !this.state.pipelines.length && !this.isLoading; return !this.state.pipelines.length &&
!this.isLoading &&
!this.hasError;
},
shouldRenderTable() {
return !this.isLoading &&
this.state.pipelines.length > 0 &&
!this.hasError;
}, },
}, },
...@@ -145,8 +153,12 @@ export default Vue.component('pipelines-table', { ...@@ -145,8 +153,12 @@ export default Vue.component('pipelines-table', {
template: ` template: `
<div class="content-list pipelines"> <div class="content-list pipelines">
<div class="realtime-loading" v-if="isLoading"> <div
<i class="fa fa-spinner fa-spin"></i> class="realtime-loading"
v-if="isLoading">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true" />
</div> </div>
<empty-state <empty-state
...@@ -155,8 +167,9 @@ export default Vue.component('pipelines-table', { ...@@ -155,8 +167,9 @@ export default Vue.component('pipelines-table', {
<error-state v-if="shouldRenderErrorState" /> <error-state v-if="shouldRenderErrorState" />
<div class="table-holder" <div
v-if="!isLoading && state.pipelines.length > 0"> class="table-holder"
v-if="shouldRenderTable">
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
:service="service" /> :service="service" />
......
// ECMAScript polyfills // ECMAScript polyfills
import 'core-js/fn/array/find'; import 'core-js/fn/array/find';
import 'core-js/fn/array/from'; import 'core-js/fn/array/from';
import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign'; import 'core-js/fn/object/assign';
import 'core-js/fn/promise'; import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at'; import 'core-js/fn/string/code-point-at';
......
...@@ -3,65 +3,63 @@ ...@@ -3,65 +3,63 @@
import Vue from 'vue'; import Vue from 'vue';
(() => { const CommentAndResolveBtn = Vue.extend({
const CommentAndResolveBtn = Vue.extend({ props: {
props: { discussionId: String,
discussionId: String, },
data() {
return {
textareaIsEmpty: true,
discussion: {},
};
},
computed: {
showButton: function () {
if (this.discussion) {
return this.discussion.isResolvable();
} else {
return false;
}
}, },
data() { isDiscussionResolved: function () {
return { return this.discussion.isResolved();
textareaIsEmpty: true,
discussion: {},
};
}, },
computed: { buttonText: function () {
showButton: function () { if (this.isDiscussionResolved) {
if (this.discussion) { if (this.textareaIsEmpty) {
return this.discussion.isResolvable(); return "Unresolve discussion";
} else { } else {
return false; return "Comment & unresolve discussion";
} }
}, } else {
isDiscussionResolved: function () { if (this.textareaIsEmpty) {
return this.discussion.isResolved(); return "Resolve discussion";
},
buttonText: function () {
if (this.isDiscussionResolved) {
if (this.textareaIsEmpty) {
return "Unresolve discussion";
} else {
return "Comment & unresolve discussion";
}
} else { } else {
if (this.textareaIsEmpty) { return "Comment & resolve discussion";
return "Resolve discussion";
} else {
return "Comment & resolve discussion";
}
} }
} }
}, }
created() { },
if (this.discussionId) { created() {
this.discussion = CommentsStore.state[this.discussionId]; if (this.discussionId) {
} this.discussion = CommentsStore.state[this.discussionId];
}, }
mounted: function () { },
if (!this.discussionId) return; mounted: function () {
if (!this.discussionId) return;
const $textarea = $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`); const $textarea = $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`);
this.textareaIsEmpty = $textarea.val() === ''; this.textareaIsEmpty = $textarea.val() === '';
$textarea.on('input.comment-and-resolve-btn', () => { $textarea.on('input.comment-and-resolve-btn', () => {
this.textareaIsEmpty = $textarea.val() === ''; this.textareaIsEmpty = $textarea.val() === '';
}); });
}, },
destroyed: function () { destroyed: function () {
if (!this.discussionId) return; if (!this.discussionId) return;
$(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off('input.comment-and-resolve-btn'); $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off('input.comment-and-resolve-btn');
} }
}); });
Vue.component('comment-and-resolve-btn', CommentAndResolveBtn); Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
})(window);
...@@ -4,155 +4,153 @@ ...@@ -4,155 +4,153 @@
import Vue from 'vue'; import Vue from 'vue';
import collapseIcon from '../icons/collapse_icon.svg'; import collapseIcon from '../icons/collapse_icon.svg';
(() => { const DiffNoteAvatars = Vue.extend({
const DiffNoteAvatars = Vue.extend({ props: ['discussionId'],
props: ['discussionId'], data() {
data() { return {
return { isVisible: false,
isVisible: false, lineType: '',
lineType: '', storeState: CommentsStore.state,
storeState: CommentsStore.state, shownAvatars: 3,
shownAvatars: 3, collapseIcon,
collapseIcon, };
}; },
}, template: `
template: ` <div class="diff-comment-avatar-holders"
<div class="diff-comment-avatar-holders" v-show="notesCount !== 0">
v-show="notesCount !== 0"> <div v-if="!isVisible">
<div v-if="!isVisible"> <img v-for="note in notesSubset"
<img v-for="note in notesSubset" class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar"
class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar" width="19"
width="19" height="19"
height="19" role="button"
role="button" data-container="body"
data-container="body" data-placement="top"
data-placement="top" data-html="true"
data-html="true" :data-line-type="lineType"
:data-line-type="lineType" :title="note.authorName + ': ' + note.noteTruncated"
:title="note.authorName + ': ' + note.noteTruncated" :src="note.authorAvatar"
:src="note.authorAvatar" @click="clickedAvatar($event)" />
@click="clickedAvatar($event)" /> <span v-if="notesCount > shownAvatars"
<span v-if="notesCount > shownAvatars" class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
class="diff-comments-more-count has-tooltip js-diff-comment-avatar" data-container="body"
data-container="body" data-placement="top"
data-placement="top" ref="extraComments"
ref="extraComments" role="button"
role="button"
:data-line-type="lineType"
:title="extraNotesTitle"
@click="clickedAvatar($event)">{{ moreText }}</span>
</div>
<button class="diff-notes-collapse js-diff-comment-avatar"
type="button"
aria-label="Show comments"
:data-line-type="lineType" :data-line-type="lineType"
@click="clickedAvatar($event)" :title="extraNotesTitle"
v-if="isVisible" @click="clickedAvatar($event)">{{ moreText }}</span>
v-html="collapseIcon">
</button>
</div> </div>
`, <button class="diff-notes-collapse js-diff-comment-avatar"
mounted() { type="button"
aria-label="Show comments"
:data-line-type="lineType"
@click="clickedAvatar($event)"
v-if="isVisible"
v-html="collapseIcon">
</button>
</div>
`,
mounted() {
this.$nextTick(() => {
this.addNoCommentClass();
this.setDiscussionVisible();
this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
});
$(document).on('toggle.comments', () => {
this.$nextTick(() => { this.$nextTick(() => {
this.addNoCommentClass();
this.setDiscussionVisible(); this.setDiscussionVisible();
this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
}); });
});
$(document).on('toggle.comments', () => { },
destroyed() {
$(document).off('toggle.comments');
},
watch: {
storeState: {
handler() {
this.$nextTick(() => { this.$nextTick(() => {
this.setDiscussionVisible(); $('.has-tooltip', this.$el).tooltip('fixTitle');
// We need to add/remove a class to an element that is outside the Vue instance
this.addNoCommentClass();
}); });
});
},
destroyed() {
$(document).off('toggle.comments');
},
watch: {
storeState: {
handler() {
this.$nextTick(() => {
$('.has-tooltip', this.$el).tooltip('fixTitle');
// We need to add/remove a class to an element that is outside the Vue instance
this.addNoCommentClass();
});
},
deep: true,
}, },
deep: true,
}, },
computed: { },
notesSubset() { computed: {
let notes = []; notesSubset() {
let notes = [];
if (this.discussion) {
notes = Object.keys(this.discussion.notes) if (this.discussion) {
.slice(0, this.shownAvatars) notes = Object.keys(this.discussion.notes)
.map(noteId => this.discussion.notes[noteId]); .slice(0, this.shownAvatars)
} .map(noteId => this.discussion.notes[noteId]);
}
return notes;
}, return notes;
extraNotesTitle() { },
if (this.discussion) { extraNotesTitle() {
const extra = this.discussion.notesCount() - this.shownAvatars; if (this.discussion) {
const extra = this.discussion.notesCount() - this.shownAvatars;
return `${extra} more comment${extra > 1 ? 's' : ''}`; return `${extra} more comment${extra > 1 ? 's' : ''}`;
} }
return ''; return '';
}, },
discussion() { discussion() {
return this.storeState[this.discussionId]; return this.storeState[this.discussionId];
}, },
notesCount() { notesCount() {
if (this.discussion) { if (this.discussion) {
return this.discussion.notesCount(); return this.discussion.notesCount();
} }
return 0; return 0;
}, },
moreText() { moreText() {
const plusSign = this.notesCount < 100 ? '+' : ''; const plusSign = this.notesCount < 100 ? '+' : '';
return `${plusSign}${this.notesCount - this.shownAvatars}`; return `${plusSign}${this.notesCount - this.shownAvatars}`;
},
}, },
methods: { },
clickedAvatar(e) { methods: {
notes.addDiffNote(e); clickedAvatar(e) {
notes.addDiffNote(e);
// Toggle the active state of the toggle all button // Toggle the active state of the toggle all button
this.toggleDiscussionsToggleState(); this.toggleDiscussionsToggleState();
this.$nextTick(() => { this.$nextTick(() => {
this.setDiscussionVisible(); this.setDiscussionVisible();
$('.has-tooltip', this.$el).tooltip('fixTitle'); $('.has-tooltip', this.$el).tooltip('fixTitle');
$('.has-tooltip', this.$el).tooltip('hide'); $('.has-tooltip', this.$el).tooltip('hide');
}); });
}, },
addNoCommentClass() { addNoCommentClass() {
const notesCount = this.notesCount; const notesCount = this.notesCount;
$(this.$el).closest('.js-avatar-container') $(this.$el).closest('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0) .toggleClass('js-no-comment-btn', notesCount > 0)
.nextUntil('.js-avatar-container') .nextUntil('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0); .toggleClass('js-no-comment-btn', notesCount > 0);
}, },
toggleDiscussionsToggleState() { toggleDiscussionsToggleState() {
const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
const $visibleNotesHolders = $notesHolders.filter(':visible'); const $visibleNotesHolders = $notesHolders.filter(':visible');
const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments'); const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments');
$toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length); $toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length);
}, },
setDiscussionVisible() { setDiscussionVisible() {
this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible'); this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible');
},
}, },
}); },
});
Vue.component('diff-note-avatars', DiffNoteAvatars); Vue.component('diff-note-avatars', DiffNoteAvatars);
})();
...@@ -2,29 +2,27 @@ ...@@ -2,29 +2,27 @@
import Vue from 'vue'; import Vue from 'vue';
(() => { const NewIssueForDiscussion = Vue.extend({
const NewIssueForDiscussion = Vue.extend({ props: {
props: { discussionId: {
discussionId: { type: String,
type: String, required: true,
required: true,
},
}, },
data() { },
return { data() {
discussions: CommentsStore.state, return {
}; discussions: CommentsStore.state,
};
},
computed: {
discussion() {
return this.discussions[this.discussionId];
}, },
computed: { showButton() {
discussion() { if (this.discussion) return !this.discussion.isResolved();
return this.discussions[this.discussionId]; return false;
},
showButton() {
if (this.discussion) return !this.discussion.isResolved();
return false;
},
}, },
}); },
});
Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion); Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion);
})();
...@@ -5,117 +5,119 @@ ...@@ -5,117 +5,119 @@
import Vue from 'vue'; import Vue from 'vue';
(() => { const ResolveBtn = Vue.extend({
const ResolveBtn = Vue.extend({ props: {
props: { noteId: Number,
noteId: Number, discussionId: String,
discussionId: String, resolved: Boolean,
resolved: Boolean, canResolve: Boolean,
canResolve: Boolean, resolvedBy: String,
resolvedBy: String, authorName: String,
authorName: String, authorAvatar: String,
authorAvatar: String, noteTruncated: String,
noteTruncated: String, },
data: function () {
return {
discussions: CommentsStore.state,
loading: false
};
},
watch: {
'discussions': {
handler: 'updateTooltip',
deep: true
}
},
computed: {
discussion: function () {
return this.discussions[this.discussionId];
}, },
data: function () { note: function () {
return { return this.discussion ? this.discussion.getNote(this.noteId) : {};
discussions: CommentsStore.state,
loading: false
};
}, },
watch: { buttonText: function () {
'discussions': { if (this.isResolved) {
handler: 'updateTooltip', return `Resolved by ${this.resolvedByName}`;
deep: true } else if (this.canResolve) {
return 'Mark as resolved';
} else {
return 'Unable to resolve';
} }
}, },
computed: { isResolved: function () {
discussion: function () { if (this.note) {
return this.discussions[this.discussionId]; return this.note.resolved;
}, } else {
note: function () { return false;
return this.discussion ? this.discussion.getNote(this.noteId) : {}; }
}, },
buttonText: function () { resolvedByName: function () {
if (this.isResolved) { return this.note.resolved_by;
return `Resolved by ${this.resolvedByName}`;
} else if (this.canResolve) {
return 'Mark as resolved';
} else {
return 'Unable to resolve';
}
},
isResolved: function () {
if (this.note) {
return this.note.resolved;
} else {
return false;
}
},
resolvedByName: function () {
return this.note.resolved_by;
},
}, },
methods: { },
updateTooltip: function () { methods: {
this.$nextTick(() => { updateTooltip: function () {
$(this.$refs.button) this.$nextTick(() => {
.tooltip('hide') $(this.$refs.button)
.tooltip('fixTitle'); .tooltip('hide')
}); .tooltip('fixTitle');
}, });
resolve: function () { },
if (!this.canResolve) return; resolve: function () {
const errorFlashMsg = 'An error occurred when trying to resolve a comment. Please try again.';
let promise; if (!this.canResolve) return;
this.loading = true;
if (this.isResolved) { let promise;
promise = ResolveService this.loading = true;
.unresolve(this.noteId);
} else {
promise = ResolveService
.resolve(this.noteId);
}
promise.then((response) => { if (this.isResolved) {
this.loading = false; promise = ResolveService
.unresolve(this.noteId);
} else {
promise = ResolveService
.resolve(this.noteId);
}
if (response.status === 200) { promise.then((response) => {
const data = response.json(); this.loading = false;
const resolved_by = data ? data.resolved_by : null;
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by); if (response.status === 200) {
this.discussion.updateHeadline(data); const data = response.json();
} else { const resolved_by = data ? data.resolved_by : null;
new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
}
this.updateTooltip(); CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
}); this.discussion.updateHeadline(data);
} } else {
}, new Flash(errorFlashMsg);
mounted: function () { }
$(this.$refs.button).tooltip({
container: 'body' this.updateTooltip();
}); }).catch(() => {
}, new Flash(errorFlashMsg);
beforeDestroy: function () {
CommentsStore.delete(this.discussionId, this.noteId);
},
created: function () {
CommentsStore.create({
discussionId: this.discussionId,
noteId: this.noteId,
canResolve: this.canResolve,
resolved: this.resolved,
resolvedBy: this.resolvedBy,
authorName: this.authorName,
authorAvatar: this.authorAvatar,
noteTruncated: this.noteTruncated,
}); });
} }
}); },
mounted: function () {
$(this.$refs.button).tooltip({
container: 'body'
});
},
beforeDestroy: function () {
CommentsStore.delete(this.discussionId, this.noteId);
},
created: function () {
CommentsStore.create({
discussionId: this.discussionId,
noteId: this.noteId,
canResolve: this.canResolve,
resolved: this.resolved,
resolvedBy: this.resolvedBy,
authorName: this.authorName,
authorAvatar: this.authorAvatar,
noteTruncated: this.noteTruncated,
});
}
});
Vue.component('resolve-btn', ResolveBtn); Vue.component('resolve-btn', ResolveBtn);
})();
...@@ -4,24 +4,22 @@ ...@@ -4,24 +4,22 @@
import Vue from 'vue'; import Vue from 'vue';
((w) => { window.ResolveCount = Vue.extend({
w.ResolveCount = Vue.extend({ mixins: [DiscussionMixins],
mixins: [DiscussionMixins], props: {
props: { loggedOut: Boolean
loggedOut: Boolean },
data: function () {
return {
discussions: CommentsStore.state
};
},
computed: {
allResolved: function () {
return this.resolvedDiscussionCount === this.discussionCount;
}, },
data: function () { resolvedCountText() {
return { return this.discussionCount === 1 ? 'discussion' : 'discussions';
discussions: CommentsStore.state
};
},
computed: {
allResolved: function () {
return this.resolvedDiscussionCount === this.discussionCount;
},
resolvedCountText() {
return this.discussionCount === 1 ? 'discussion' : 'discussions';
}
} }
}); }
})(window); });
...@@ -4,59 +4,57 @@ ...@@ -4,59 +4,57 @@
import Vue from 'vue'; import Vue from 'vue';
(() => { const ResolveDiscussionBtn = Vue.extend({
const ResolveDiscussionBtn = Vue.extend({ props: {
props: { discussionId: String,
discussionId: String, mergeRequestId: Number,
mergeRequestId: Number, canResolve: Boolean,
canResolve: Boolean, },
}, data: function() {
data: function() { return {
return { discussion: {},
discussion: {}, };
}; },
computed: {
showButton: function () {
if (this.discussion) {
return this.discussion.isResolvable();
} else {
return false;
}
}, },
computed: { isDiscussionResolved: function () {
showButton: function () { if (this.discussion) {
if (this.discussion) { return this.discussion.isResolved();
return this.discussion.isResolvable(); } else {
} else { return false;
return false;
}
},
isDiscussionResolved: function () {
if (this.discussion) {
return this.discussion.isResolved();
} else {
return false;
}
},
buttonText: function () {
if (this.isDiscussionResolved) {
return "Unresolve discussion";
} else {
return "Resolve discussion";
}
},
loading: function () {
if (this.discussion) {
return this.discussion.loading;
} else {
return false;
}
} }
}, },
methods: { buttonText: function () {
resolve: function () { if (this.isDiscussionResolved) {
ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId); return "Unresolve discussion";
} else {
return "Resolve discussion";
} }
}, },
created: function () { loading: function () {
CommentsStore.createDiscussion(this.discussionId, this.canResolve); if (this.discussion) {
return this.discussion.loading;
this.discussion = CommentsStore.state[this.discussionId]; } else {
return false;
}
}
},
methods: {
resolve: function () {
ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
} }
}); },
created: function () {
CommentsStore.createDiscussion(this.discussionId, this.canResolve);
this.discussion = CommentsStore.state[this.discussionId];
}
});
Vue.component('resolve-discussion-btn', ResolveDiscussionBtn); Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
})();
/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */ /* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */
((w) => { window.DiscussionMixins = {
w.DiscussionMixins = { computed: {
computed: { discussionCount: function () {
discussionCount: function () { return Object.keys(this.discussions).length;
return Object.keys(this.discussions).length; },
}, resolvedDiscussionCount: function () {
resolvedDiscussionCount: function () { let resolvedCount = 0;
let resolvedCount = 0;
for (const discussionId in this.discussions) { for (const discussionId in this.discussions) {
const discussion = this.discussions[discussionId]; const discussion = this.discussions[discussionId];
if (discussion.isResolved()) { if (discussion.isResolved()) {
resolvedCount += 1; resolvedCount += 1;
}
} }
}
return resolvedCount; return resolvedCount;
}, },
unresolvedDiscussionCount: function () { unresolvedDiscussionCount: function () {
let unresolvedCount = 0; let unresolvedCount = 0;
for (const discussionId in this.discussions) { for (const discussionId in this.discussions) {
const discussion = this.discussions[discussionId]; const discussion = this.discussions[discussionId];
if (!discussion.isResolved()) { if (!discussion.isResolved()) {
unresolvedCount += 1; unresolvedCount += 1;
}
} }
return unresolvedCount;
} }
return unresolvedCount;
} }
}; }
})(window); };
...@@ -9,76 +9,76 @@ require('../../vue_shared/vue_resource_interceptor'); ...@@ -9,76 +9,76 @@ require('../../vue_shared/vue_resource_interceptor');
Vue.use(VueResource); Vue.use(VueResource);
(() => { window.gl = window.gl || {};
window.gl = window.gl || {};
class ResolveServiceClass { class ResolveServiceClass {
constructor(root) { constructor(root) {
this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`); this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`);
this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`); this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`);
} }
resolve(noteId) {
return this.noteResource.save({ noteId }, {});
}
unresolve(noteId) { resolve(noteId) {
return this.noteResource.delete({ noteId }, {}); return this.noteResource.save({ noteId }, {});
} }
toggleResolveForDiscussion(mergeRequestId, discussionId) { unresolve(noteId) {
const discussion = CommentsStore.state[discussionId]; return this.noteResource.delete({ noteId }, {});
const isResolved = discussion.isResolved(); }
let promise;
if (isResolved) { toggleResolveForDiscussion(mergeRequestId, discussionId) {
promise = this.unResolveAll(mergeRequestId, discussionId); const discussion = CommentsStore.state[discussionId];
} else { const isResolved = discussion.isResolved();
promise = this.resolveAll(mergeRequestId, discussionId); let promise;
}
promise.then((response) => { if (isResolved) {
discussion.loading = false; promise = this.unResolveAll(mergeRequestId, discussionId);
} else {
promise = this.resolveAll(mergeRequestId, discussionId);
}
if (response.status === 200) { promise.then((response) => {
const data = response.json(); discussion.loading = false;
const resolved_by = data ? data.resolved_by : null;
if (isResolved) { if (response.status === 200) {
discussion.unResolveAllNotes(); const data = response.json();
} else { const resolved_by = data ? data.resolved_by : null;
discussion.resolveAllNotes(resolved_by);
}
discussion.updateHeadline(data); if (isResolved) {
discussion.unResolveAllNotes();
} else { } else {
new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert'); discussion.resolveAllNotes(resolved_by);
} }
});
}
resolveAll(mergeRequestId, discussionId) { discussion.updateHeadline(data);
const discussion = CommentsStore.state[discussionId]; } else {
throw new Error('An error occurred when trying to resolve discussion.');
}
}).catch(() => {
new Flash('An error occurred when trying to resolve a discussion. Please try again.');
});
}
discussion.loading = true; resolveAll(mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
return this.discussionResource.save({ discussion.loading = true;
mergeRequestId,
discussionId return this.discussionResource.save({
}, {}); mergeRequestId,
} discussionId
}, {});
}
unResolveAll(mergeRequestId, discussionId) { unResolveAll(mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId]; const discussion = CommentsStore.state[discussionId];
discussion.loading = true; discussion.loading = true;
return this.discussionResource.delete({ return this.discussionResource.delete({
mergeRequestId, mergeRequestId,
discussionId discussionId
}, {}); }, {});
}
} }
}
gl.DiffNotesResolveServiceClass = ResolveServiceClass; gl.DiffNotesResolveServiceClass = ResolveServiceClass;
})();
...@@ -3,56 +3,54 @@ ...@@ -3,56 +3,54 @@
import Vue from 'vue'; import Vue from 'vue';
((w) => { window.CommentsStore = {
w.CommentsStore = { state: {},
state: {}, get: function (discussionId, noteId) {
get: function (discussionId, noteId) { return this.state[discussionId].getNote(noteId);
return this.state[discussionId].getNote(noteId); },
}, createDiscussion: function (discussionId, canResolve) {
createDiscussion: function (discussionId, canResolve) { let discussion = this.state[discussionId];
let discussion = this.state[discussionId]; if (!this.state[discussionId]) {
if (!this.state[discussionId]) { discussion = new DiscussionModel(discussionId);
discussion = new DiscussionModel(discussionId); Vue.set(this.state, discussionId, discussion);
Vue.set(this.state, discussionId, discussion); }
}
if (canResolve !== undefined) { if (canResolve !== undefined) {
discussion.canResolve = canResolve; discussion.canResolve = canResolve;
} }
return discussion; return discussion;
}, },
create: function (noteObj) { create: function (noteObj) {
const discussion = this.createDiscussion(noteObj.discussionId); const discussion = this.createDiscussion(noteObj.discussionId);
discussion.createNote(noteObj);
},
update: function (discussionId, noteId, resolved, resolved_by) {
const discussion = this.state[discussionId];
const note = discussion.getNote(noteId);
note.resolved = resolved;
note.resolved_by = resolved_by;
},
delete: function (discussionId, noteId) {
const discussion = this.state[discussionId];
discussion.deleteNote(noteId);
if (discussion.notesCount() === 0) {
Vue.delete(this.state, discussionId);
}
},
unresolvedDiscussionIds: function () {
const ids = [];
discussion.createNote(noteObj); for (const discussionId in this.state) {
},
update: function (discussionId, noteId, resolved, resolved_by) {
const discussion = this.state[discussionId];
const note = discussion.getNote(noteId);
note.resolved = resolved;
note.resolved_by = resolved_by;
},
delete: function (discussionId, noteId) {
const discussion = this.state[discussionId]; const discussion = this.state[discussionId];
discussion.deleteNote(noteId);
if (discussion.notesCount() === 0) { if (!discussion.isResolved()) {
Vue.delete(this.state, discussionId); ids.push(discussion.id);
} }
},
unresolvedDiscussionIds: function () {
const ids = [];
for (const discussionId in this.state) {
const discussion = this.state[discussionId];
if (!discussion.isResolved()) {
ids.push(discussion.id);
}
}
return ids;
} }
};
})(window); return ids;
}
};
...@@ -33,6 +33,7 @@ ...@@ -33,6 +33,7 @@
/* global Labels */ /* global Labels */
/* global Shortcuts */ /* global Shortcuts */
/* global Sidebar */ /* global Sidebar */
/* global ShortcutsWiki */
import Issue from './issue'; import Issue from './issue';
...@@ -46,6 +47,7 @@ import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; ...@@ -46,6 +47,7 @@ import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import BlobForkSuggestion from './blob/blob_fork_suggestion'; import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout'; import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags'; import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
import ShortcutsWiki from './shortcuts_wiki';
const ShortcutsBlob = require('./shortcuts_blob'); const ShortcutsBlob = require('./shortcuts_blob');
...@@ -89,11 +91,13 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -89,11 +91,13 @@ const ShortcutsBlob = require('./shortcuts_blob');
fileBlobPermalinkUrl, fileBlobPermalinkUrl,
}); });
new BlobForkSuggestion( new BlobForkSuggestion({
document.querySelector('.js-edit-blob-link-fork-toggler'), openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'),
document.querySelector('.js-cancel-fork-suggestion'), forkButtons: document.querySelectorAll('.js-fork-suggestion-button'),
document.querySelector('.js-file-fork-suggestion-section'), cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'),
); suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'),
actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'),
});
} }
switch (page) { switch (page) {
...@@ -148,13 +152,13 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -148,13 +152,13 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:milestones:new': case 'projects:milestones:new':
case 'projects:milestones:edit': case 'projects:milestones:edit':
case 'projects:milestones:update': case 'projects:milestones:update':
case 'groups:milestones:new':
case 'groups:milestones:edit':
case 'groups:milestones:update':
new ZenMode(); new ZenMode();
new gl.DueDateSelectors(); new gl.DueDateSelectors();
new gl.GLForm($('.milestone-form')); new gl.GLForm($('.milestone-form'));
break; break;
case 'groups:milestones:new':
new ZenMode();
break;
case 'projects:compare:show': case 'projects:compare:show':
new gl.Diff(); new gl.Diff();
break; break;
...@@ -365,6 +369,9 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -365,6 +369,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'admin': case 'admin':
new Admin(); new Admin();
switch (path[1]) { switch (path[1]) {
case 'cohorts':
new gl.UsagePing();
break;
case 'groups': case 'groups':
new UsersSelect(); new UsersSelect();
break; break;
...@@ -416,7 +423,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -416,7 +423,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
break; break;
case 'wikis': case 'wikis':
new gl.Wikis(); new gl.Wikis();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsWiki();
new ZenMode(); new ZenMode();
new gl.GLForm($('.wiki-form')); new gl.GLForm($('.wiki-form'));
break; break;
......
...@@ -3,6 +3,7 @@ const DATA_DROPDOWN = 'data-dropdown'; ...@@ -3,6 +3,7 @@ const DATA_DROPDOWN = 'data-dropdown';
const SELECTED_CLASS = 'droplab-item-selected'; const SELECTED_CLASS = 'droplab-item-selected';
const ACTIVE_CLASS = 'droplab-item-active'; const ACTIVE_CLASS = 'droplab-item-active';
const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g; const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
const IGNORE_CLASS = 'droplab-item-ignore';
export { export {
DATA_TRIGGER, DATA_TRIGGER,
...@@ -10,4 +11,5 @@ export { ...@@ -10,4 +11,5 @@ export {
SELECTED_CLASS, SELECTED_CLASS,
ACTIVE_CLASS, ACTIVE_CLASS,
TEMPLATE_REGEX, TEMPLATE_REGEX,
IGNORE_CLASS,
}; };
/* eslint-disable */ /* eslint-disable */
import utils from './utils'; import utils from './utils';
import { SELECTED_CLASS } from './constants'; import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
var DropDown = function(list) { var DropDown = function(list) {
this.currentIndex = 0; this.currentIndex = 0;
...@@ -36,6 +36,7 @@ Object.assign(DropDown.prototype, { ...@@ -36,6 +36,7 @@ Object.assign(DropDown.prototype, {
clickEvent: function(e) { clickEvent: function(e) {
if (e.target.tagName === 'UL') return; if (e.target.tagName === 'UL') return;
if (e.target.classList.contains(IGNORE_CLASS)) return;
var selected = utils.closest(e.target, 'LI'); var selected = utils.closest(e.target, 'LI');
if (!selected) return; if (!selected) return;
......
...@@ -38,6 +38,9 @@ window.DropzoneInput = (function() { ...@@ -38,6 +38,9 @@ window.DropzoneInput = (function() {
"opacity": 0, "opacity": 0,
"display": "none" "display": "none"
}); });
if (!project_uploads_path) return;
dropzone = form_dropzone.dropzone({ dropzone = form_dropzone.dropzone({
url: project_uploads_path, url: project_uploads_path,
dictDefaultMessage: "", dictDefaultMessage: "",
...@@ -130,13 +133,15 @@ window.DropzoneInput = (function() { ...@@ -130,13 +133,15 @@ window.DropzoneInput = (function() {
var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
var formattedText = text; var formattedText = text;
if (shouldPad) formattedText += "\n\n"; if (shouldPad) formattedText += "\n\n";
caretStart = $(child)[0].selectionStart; const textarea = child.get(0);
caretEnd = $(child)[0].selectionEnd; caretStart = textarea.selectionStart;
caretEnd = textarea.selectionEnd;
textEnd = $(child).val().length; textEnd = $(child).val().length;
beforeSelection = $(child).val().substring(0, caretStart); beforeSelection = $(child).val().substring(0, caretStart);
afterSelection = $(child).val().substring(caretEnd, textEnd); afterSelection = $(child).val().substring(caretEnd, textEnd);
$(child).val(beforeSelection + formattedText + afterSelection); $(child).val(beforeSelection + formattedText + afterSelection);
child.get(0).setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
textarea.style.height = `${textarea.scrollHeight}px`;
return form_textarea.trigger("input"); return form_textarea.trigger("input");
}; };
getFilename = function(e) { getFilename = function(e) {
...@@ -180,7 +185,7 @@ window.DropzoneInput = (function() { ...@@ -180,7 +185,7 @@ window.DropzoneInput = (function() {
}; };
insertToTextArea = function(filename, url) { insertToTextArea = function(filename, url) {
return $(child).val(function(index, val) { return $(child).val(function(index, val) {
return val.replace("{{" + filename + "}}", url + "\n"); return val.replace("{{" + filename + "}}", url);
}); });
}; };
appendToTextArea = function(url) { appendToTextArea = function(url) {
...@@ -215,6 +220,7 @@ window.DropzoneInput = (function() { ...@@ -215,6 +220,7 @@ window.DropzoneInput = (function() {
form.find(".markdown-selector").click(function(e) { form.find(".markdown-selector").click(function(e) {
e.preventDefault(); e.preventDefault();
$(this).closest('.gfm-form').find('.div-dropzone').click(); $(this).closest('.gfm-form').find('.div-dropzone').click();
form_textarea.focus();
}); });
} }
......
...@@ -115,11 +115,13 @@ class DueDateSelect { ...@@ -115,11 +115,13 @@ class DueDateSelect {
this.$dropdown.trigger('loading.gl.dropdown'); this.$dropdown.trigger('loading.gl.dropdown');
this.$selectbox.hide(); this.$selectbox.hide();
this.$value.css('display', ''); this.$value.css('display', '');
const fadeOutLoader = () => {
this.$loading.fadeOut();
};
gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update')) gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
.then(() => { .then(fadeOutLoader)
this.$loading.fadeOut(); .catch(fadeOutLoader);
});
} }
submitSelectedDate(isDropdown) { submitSelectedDate(isDropdown) {
...@@ -168,8 +170,9 @@ class DueDateSelectors { ...@@ -168,8 +170,9 @@ class DueDateSelectors {
const $datePicker = $(this); const $datePicker = $(this);
const calendar = new Pikaday({ const calendar = new Pikaday({
field: $datePicker.get(0), field: $datePicker.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd', format: 'yyyy-mm-dd',
container: $datePicker.parent().get(0),
onSelect(dateText) { onSelect(dateText) {
$datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/* global Flash */ /* global Flash */
import Vue from 'vue'; import Vue from 'vue';
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table'; import EnvironmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store'; import EnvironmentsStore from '../stores/environments_store';
import TablePaginationComponent from '../../vue_shared/components/table_pagination'; import TablePaginationComponent from '../../vue_shared/components/table_pagination';
import '../../lib/utils/common_utils'; import '../../lib/utils/common_utils';
......
<script>
/* global Flash */ /* global Flash */
/* eslint-disable no-new */ /* eslint-disable no-new */
...@@ -35,6 +36,8 @@ export default { ...@@ -35,6 +36,8 @@ export default {
onClickAction(endpoint) { onClickAction(endpoint) {
this.isLoading = true; this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
this.service.postAction(endpoint) this.service.postAction(endpoint)
.then(() => { .then(() => {
this.isLoading = false; this.isLoading = false;
...@@ -54,44 +57,47 @@ export default { ...@@ -54,44 +57,47 @@ export default {
return !action.playable; return !action.playable;
}, },
}, },
};
</script>
<template>
<div
class="btn-group"
role="group">
<button
type="button"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
data-container="body"
data-toggle="dropdown"
ref="tooltip"
:title="title"
:aria-label="title"
:disabled="isLoading">
<span>
<span v-html="playIconSvg"></span>
<i
class="fa fa-caret-down"
aria-hidden="true"/>
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true"/>
</span>
</button>
template: ` <ul class="dropdown-menu dropdown-menu-align-right">
<div class="btn-group" role="group"> <li v-for="action in actions">
<button <button
type="button" type="button"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip" class="js-manual-action-link no-btn btn"
data-container="body" @click="onClickAction(action.play_path)"
data-toggle="dropdown" :class="{ disabled: isActionDisabled(action) }"
:title="title" :disabled="isActionDisabled(action)">
:aria-label="title"
:disabled="isLoading">
<span>
<span v-html="playIconSvg"></span> <span v-html="playIconSvg"></span>
<i <span>
class="fa fa-caret-down" {{action.name}}
aria-hidden="true"/> </span>
<i </button>
v-if="isLoading" </li>
class="fa fa-spinner fa-spin" </ul>
aria-hidden="true"/>
</span>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<button
type="button"
class="js-manual-action-link no-btn btn"
@click="onClickAction(action.play_path)"
:class="{ 'disabled': isActionDisabled(action) }"
:disabled="isActionDisabled(action)">
${playIconSvg}
<span>
{{action.name}}
</span>
</button>
</li>
</ul>
</div> </div>
`, </template>
};
<script>
/** /**
* Renders the external url link in environments table. * Renders the external url link in environments table.
*/ */
...@@ -5,7 +6,7 @@ export default { ...@@ -5,7 +6,7 @@ export default {
props: { props: {
externalUrl: { externalUrl: {
type: String, type: String,
default: '', required: true,
}, },
}, },
...@@ -14,17 +15,19 @@ export default { ...@@ -14,17 +15,19 @@ export default {
return 'Open'; return 'Open';
}, },
}, },
template: `
<a
class="btn external-url has-tooltip"
data-container="body"
:href="externalUrl"
target="_blank"
rel="noopener noreferrer nofollow"
:title="title"
:aria-label="title">
<i class="fa fa-external-link" aria-hidden="true"></i>
</a>
`,
}; };
</script>
<template>
<a
class="btn external-url has-tooltip"
data-container="body"
target="_blank"
rel="noopener noreferrer nofollow"
:title="title"
:aria-label="title"
:href="externalUrl">
<i
class="fa fa-external-link"
aria-hidden="true" />
</a>
</template>
<script>
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import '../../lib/utils/text_utility'; import '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions'; import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url'; import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop'; import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback'; import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button'; import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring'; import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit'; import CommitComponent from '../../vue_shared/components/commit';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -434,117 +435,140 @@ export default { ...@@ -434,117 +435,140 @@ export default {
eventHub.$emit('toggleFolder', this.model, this.folderUrl); eventHub.$emit('toggleFolder', this.model, this.folderUrl);
}, },
}, },
};
template: ` </script>
<tr :class="{ 'js-child-row': model.isChildren }"> <template>
<td> <tr :class="{ 'js-child-row': model.isChildren }">
<a v-if="!model.isFolder" <td>
class="environment-name" <a
:class="{ 'prepend-left-default': model.isChildren }" v-if="!model.isFolder"
:href="environmentPath"> class="environment-name"
{{model.name}} :class="{ 'prepend-left-default': model.isChildren }"
</a> :href="environmentPath">
<span v-else {{model.name}}
class="folder-name" </a>
@click="onClickFolder" <span
role="button"> v-else
class="folder-name"
<span class="folder-icon"> @click="onClickFolder"
<i role="button">
v-show="model.isOpen"
class="fa fa-caret-down" <span class="folder-icon">
aria-hidden="true" /> <i
<i v-show="model.isOpen"
v-show="!model.isOpen" class="fa fa-caret-down"
class="fa fa-caret-right" aria-hidden="true" />
aria-hidden="true"/> <i
</span> v-show="!model.isOpen"
class="fa fa-caret-right"
<span class="folder-icon"> aria-hidden="true"/>
<i class="fa fa-folder" aria-hidden="true"></i>
</span>
<span>
{{model.folderName}}
</span>
<span class="badge">
{{model.size}}
</span>
</span> </span>
</td>
<td class="deployment-column"> <span class="folder-icon">
<span v-if="shouldRenderDeploymentID"> <i
{{deploymentInternalId}} class="fa fa-folder"
aria-hidden="true" />
</span> </span>
<span v-if="!model.isFolder && deploymentHasUser"> <span>
by {{model.folderName}}
<a :href="deploymentUser.web_url" class="js-deploy-user-container">
<img class="avatar has-tooltip s20"
:src="deploymentUser.avatar_url"
:alt="userImageAltDescription"
:title="deploymentUser.username" />
</a>
</span> </span>
</td>
<td class="environments-build-cell"> <span class="badge">
<a v-if="shouldRenderBuildName" {{model.size}}
class="build-link"
:href="buildPath">
{{buildName}}
</a>
</td>
<td>
<div v-if="!model.isFolder && hasLastDeploymentKey" class="js-commit-component">
<commit-component
:tag="commitTag"
:commit-ref="commitRef"
:commit-url="commitUrl"
:short-sha="commitShortSha"
:title="commitTitle"
:author="commitAuthor"/>
</div>
<p v-if="!model.isFolder && !hasLastDeploymentKey" class="commit-title">
No deployments yet
</p>
</td>
<td>
<span v-if="!model.isFolder && canShowDate"
class="environment-created-date-timeago">
{{createdDate}}
</span> </span>
</td> </span>
</td>
<td class="environments-actions">
<div v-if="!model.isFolder" class="btn-group pull-right" role="group"> <td class="deployment-column">
<actions-component v-if="hasManualActions && canCreateDeployment" <span v-if="shouldRenderDeploymentID">
:service="service" {{deploymentInternalId}}
:actions="manualActions"/> </span>
<external-url-component v-if="externalURL && canReadEnvironment" <span v-if="!model.isFolder && deploymentHasUser">
:external-url="externalURL"/> by
<a
<monitoring-button-component v-if="monitoringUrl && canReadEnvironment" :href="deploymentUser.web_url"
:monitoring-url="monitoringUrl"/> class="js-deploy-user-container">
<img
<terminal-button-component v-if="model && model.terminal_path" class="avatar has-tooltip s20"
:terminal-path="model.terminal_path"/> :src="deploymentUser.avatar_url"
:alt="userImageAltDescription"
<stop-component v-if="hasStopAction && canCreateDeployment" :title="deploymentUser.username" />
:stop-url="model.stop_path" </a>
:service="service"/> </span>
</td>
<rollback-component v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment" <td class="environments-build-cell">
:retry-url="retryUrl" <a
:service="service"/> v-if="shouldRenderBuildName"
</div> class="build-link"
</td> :href="buildPath">
</tr> {{buildName}}
`, </a>
}; </td>
<td>
<div
v-if="!model.isFolder && hasLastDeploymentKey"
class="js-commit-component">
<commit-component
:tag="commitTag"
:commit-ref="commitRef"
:commit-url="commitUrl"
:short-sha="commitShortSha"
:title="commitTitle"
:author="commitAuthor"/>
</div>
<p
v-if="!model.isFolder && !hasLastDeploymentKey"
class="commit-title">
No deployments yet
</p>
</td>
<td>
<span
v-if="!model.isFolder && canShowDate"
class="environment-created-date-timeago">
{{createdDate}}
</span>
</td>
<td class="environments-actions">
<div
v-if="!model.isFolder"
class="btn-group pull-right"
role="group">
<actions-component
v-if="hasManualActions && canCreateDeployment"
:service="service"
:actions="manualActions"/>
<external-url-component
v-if="externalURL && canReadEnvironment"
:external-url="externalURL"/>
<monitoring-button-component
v-if="monitoringUrl && canReadEnvironment"
:monitoring-url="monitoringUrl"/>
<terminal-button-component
v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"/>
<stop-component
v-if="hasStopAction && canCreateDeployment"
:stop-url="model.stop_path"
:service="service"/>
<rollback-component
v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl"
:service="service"/>
</div>
</td>
</tr>
</template>
<script>
/** /**
* Renders the Monitoring (Metrics) link in environments table. * Renders the Monitoring (Metrics) link in environments table.
*/ */
...@@ -5,7 +6,6 @@ export default { ...@@ -5,7 +6,6 @@ export default {
props: { props: {
monitoringUrl: { monitoringUrl: {
type: String, type: String,
default: '',
required: true, required: true,
}, },
}, },
...@@ -15,17 +15,19 @@ export default { ...@@ -15,17 +15,19 @@ export default {
return 'Monitoring'; return 'Monitoring';
}, },
}, },
template: `
<a
class="btn monitoring-url has-tooltip"
data-container="body"
:href="monitoringUrl"
target="_blank"
rel="noopener noreferrer nofollow"
:title="title"
:aria-label="title">
<i class="fa fa-area-chart" aria-hidden="true"></i>
</a>
`,
}; };
</script>
<template>
<a
class="btn monitoring-url has-tooltip"
data-container="body"
target="_blank"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"
:title="title"
:aria-label="title">
<i
class="fa fa-area-chart"
aria-hidden="true" />
</a>
</template>
<script>
/* global Flash */ /* global Flash */
/* eslint-disable no-new */ /* eslint-disable no-new */
/** /**
...@@ -36,6 +37,8 @@ export default { ...@@ -36,6 +37,8 @@ export default {
onClick() { onClick() {
this.isLoading = true; this.isLoading = true;
$(this.$el).tooltip('destroy');
this.service.postAction(this.retryUrl) this.service.postAction(this.retryUrl)
.then(() => { .then(() => {
this.isLoading = false; this.isLoading = false;
...@@ -47,21 +50,25 @@ export default { ...@@ -47,21 +50,25 @@ export default {
}); });
}, },
}, },
};
</script>
<template>
<button
type="button"
class="btn"
@click="onClick"
:disabled="isLoading">
template: ` <span v-if="isLastDeployment">
<button type="button" Re-deploy
class="btn" </span>
@click="onClick" <span v-else>
:disabled="isLoading"> Rollback
</span>
<span v-if="isLastDeployment">
Re-deploy
</span>
<span v-else>
Rollback
</span>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i> <i
</button> v-if="isLoading"
`, class="fa fa-spinner fa-spin"
}; aria-hidden="true" />
</button>
</template>
<script>
/* global Flash */ /* global Flash */
/* eslint-disable no-new, no-alert */ /* eslint-disable no-new, no-alert */
/** /**
...@@ -36,6 +37,8 @@ export default { ...@@ -36,6 +37,8 @@ export default {
if (confirm('Are you sure you want to stop this environment?')) { if (confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true; this.isLoading = true;
$(this.$el).tooltip('destroy');
this.service.postAction(this.retryUrl) this.service.postAction(this.retryUrl)
.then(() => { .then(() => {
this.isLoading = false; this.isLoading = false;
...@@ -48,17 +51,23 @@ export default { ...@@ -48,17 +51,23 @@ export default {
} }
}, },
}, },
template: `
<button type="button"
class="btn stop-env-link has-tooltip"
data-container="body"
@click="onClick"
:disabled="isLoading"
:title="title"
:aria-label="title">
<i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</button>
`,
}; };
</script>
<template>
<button
type="button"
class="btn stop-env-link has-tooltip"
data-container="body"
@click="onClick"
:disabled="isLoading"
:title="title"
:aria-label="title">
<i
class="fa fa-stop stop-env-icon"
aria-hidden="true" />
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
</button>
</template>
<script>
/** /**
* Renders a terminal button to open a web terminal. * Renders a terminal button to open a web terminal.
* Used in environments table. * Used in environments table.
...@@ -24,14 +25,15 @@ export default { ...@@ -24,14 +25,15 @@ export default {
return 'Terminal'; return 'Terminal';
}, },
}, },
template: `
<a class="btn terminal-button has-tooltip"
data-container="body"
:title="title"
:aria-label="title"
:href="terminalPath">
${terminalIconSvg}
</a>
`,
}; };
</script>
<template>
<a
class="btn terminal-button has-tooltip"
data-container="body"
:title="title"
:aria-label="title"
:href="terminalPath"
v-html="terminalIconSvg">
</a>
</template>
<script>
/** /**
* Render environments table. * Render environments table.
*/ */
import EnvironmentTableRowComponent from './environment_item'; import EnvironmentTableRowComponent from './environment_item.vue';
export default { export default {
components: { components: {
...@@ -44,54 +45,73 @@ export default { ...@@ -44,54 +45,73 @@ export default {
return `${window.location.pathname}/folders/${model.folderName}`; return `${window.location.pathname}/folders/${model.folderName}`;
}, },
}, },
};
</script>
<template>
<table class="table ci-table">
<thead>
<tr>
<th class="environments-name">
Environment
</th>
<th class="environments-deploy">
Last deployment
</th>
<th class="environments-build">
Job
</th>
<th class="environments-commit">
Commit
</th>
<th class="environments-date">
Updated
</th>
<th class="environments-actions"></th>
</tr>
</thead>
<tbody>
<template
v-for="model in environments"
v-bind:model="model">
<tr
is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:service="service" />
<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>
</tr>
template: ` <template v-else>
<table class="table ci-table"> <tr
<thead> is="environment-item"
<tr> v-for="children in model.children"
<th class="environments-name">Environment</th> :model="children"
<th class="environments-deploy">Last deployment</th> :can-create-deployment="canCreateDeployment"
<th class="environments-build">Job</th> :can-read-environment="canReadEnvironment"
<th class="environments-commit">Commit</th> :service="service" />
<th class="environments-date">Updated</th>
<th class="environments-actions"></th>
</tr>
</thead>
<tbody>
<template v-for="model in environments"
v-bind:model="model">
<tr is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:service="service"></tr>
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0"> <tr>
<tr v-if="isLoadingFolderContent"> <td
<td colspan="6" class="text-center"> colspan="6"
<i class="fa fa-spin fa-spinner fa-2x" aria-hidden="true"/> class="text-center">
<a
:href="folderUrl(model)"
class="btn btn-default">
Show all
</a>
</td> </td>
</tr> </tr>
<template v-else>
<tr is="environment-item"
v-for="children in model.children"
:model="children"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:service="service"></tr>
<tr>
<td colspan="6" class="text-center">
<a :href="folderUrl(model)" class="btn btn-default">
Show all
</a>
</td>
</tr>
</template>
</template> </template>
</template> </template>
</tbody> </template>
</table> </tbody>
`, </table>
}; </template>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/* global Flash */ /* global Flash */
import Vue from 'vue'; import Vue from 'vue';
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table'; import EnvironmentTable from '../components/environments_table.vue';
import EnvironmentsStore from '../stores/environments_store'; import EnvironmentsStore from '../stores/environments_store';
import TablePaginationComponent from '../../vue_shared/components/table_pagination'; import TablePaginationComponent from '../../vue_shared/components/table_pagination';
import '../../lib/utils/common_utils'; import '../../lib/utils/common_utils';
......
...@@ -2,82 +2,80 @@ import Filter from '~/droplab/plugins/filter'; ...@@ -2,82 +2,80 @@ import Filter from '~/droplab/plugins/filter';
require('./filtered_search_dropdown'); require('./filtered_search_dropdown');
(() => { class DropdownHint extends gl.FilteredSearchDropdown {
class DropdownHint extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input, filter) {
constructor(droplab, dropdown, input, filter) { super(droplab, dropdown, input, filter);
super(droplab, dropdown, input, filter); this.config = {
this.config = { Filter: {
Filter: { template: 'hint',
template: 'hint', filterFunction: gl.DropdownUtils.filterHint.bind(null, input),
filterFunction: gl.DropdownUtils.filterHint.bind(null, input), },
}, };
}; }
}
itemClicked(e) {
const { selected } = e.detail;
if (selected.tagName === 'LI') { itemClicked(e) {
if (selected.hasAttribute('data-value')) { const { selected } = e.detail;
this.dismissDropdown();
} else if (selected.getAttribute('data-action') === 'submit') {
this.dismissDropdown();
this.dispatchFormSubmitEvent();
} else {
const token = selected.querySelector('.js-filter-hint').innerText.trim();
const tag = selected.querySelector('.js-filter-tag').innerText.trim();
if (tag.length) { if (selected.tagName === 'LI') {
// Get previous input values in the input field and convert them into visual tokens if (selected.hasAttribute('data-value')) {
const previousInputValues = this.input.value.split(' '); this.dismissDropdown();
const searchTerms = []; } else if (selected.getAttribute('data-action') === 'submit') {
this.dismissDropdown();
this.dispatchFormSubmitEvent();
} else {
const token = selected.querySelector('.js-filter-hint').innerText.trim();
const tag = selected.querySelector('.js-filter-tag').innerText.trim();
previousInputValues.forEach((value, index) => { if (tag.length) {
searchTerms.push(value); // Get previous input values in the input field and convert them into visual tokens
const previousInputValues = this.input.value.split(' ');
const searchTerms = [];
if (index === previousInputValues.length - 1 previousInputValues.forEach((value, index) => {
&& token.indexOf(value.toLowerCase()) !== -1) { searchTerms.push(value);
searchTerms.pop();
}
});
if (searchTerms.length > 0) { if (index === previousInputValues.length - 1
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' ')); && token.indexOf(value.toLowerCase()) !== -1) {
searchTerms.pop();
} }
});
gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container); if (searchTerms.length > 0) {
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
} }
this.dismissDropdown();
this.dispatchInputEvent(); gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container);
} }
this.dismissDropdown();
this.dispatchInputEvent();
} }
} }
}
renderContent() { renderContent() {
const dropdownData = []; const dropdownData = [];
[].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => { [].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
const { icon, hint, tag, type } = dropdownMenu.dataset; const { icon, hint, tag, type } = dropdownMenu.dataset;
if (icon && hint && tag) { if (icon && hint && tag) {
dropdownData.push( dropdownData.push(
Object.assign({ Object.assign({
icon: `fa-${icon}`, icon: `fa-${icon}`,
hint, hint,
tag: `<${tag}>`, tag: `<${tag}>`,
}, type && { type }), }, type && { type }),
); );
} }
}); });
this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config); this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config);
this.droplab.setData(this.hookId, dropdownData); this.droplab.setData(this.hookId, dropdownData);
} }
init() { init() {
this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init(); this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init();
}
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.DropdownHint = DropdownHint; gl.DropdownHint = DropdownHint;
})();
...@@ -5,48 +5,46 @@ import Filter from '~/droplab/plugins/filter'; ...@@ -5,48 +5,46 @@ import Filter from '~/droplab/plugins/filter';
require('./filtered_search_dropdown'); require('./filtered_search_dropdown');
(() => { class DropdownNonUser extends gl.FilteredSearchDropdown {
class DropdownNonUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input, filter, endpoint, symbol) {
constructor(droplab, dropdown, input, filter, endpoint, symbol) { super(droplab, dropdown, input, filter);
super(droplab, dropdown, input, filter); this.symbol = symbol;
this.symbol = symbol; this.config = {
this.config = { Ajax: {
Ajax: { endpoint,
endpoint, method: 'setData',
method: 'setData', loadingTemplate: this.loadingTemplate,
loadingTemplate: this.loadingTemplate, onError() {
onError() { /* eslint-disable no-new */
/* eslint-disable no-new */ new Flash('An error occured fetching the dropdown data.');
new Flash('An error occured fetching the dropdown data.'); /* eslint-enable no-new */
/* eslint-enable no-new */
},
}, },
Filter: { },
filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input), Filter: {
template: 'title', filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
}, template: 'title',
}; },
} };
}
itemClicked(e) { itemClicked(e) {
super.itemClicked(e, (selected) => { super.itemClicked(e, (selected) => {
const title = selected.querySelector('.js-data-value').innerText.trim(); const title = selected.querySelector('.js-data-value').innerText.trim();
return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`; return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`;
}); });
} }
renderContent(forceShowList = false) { renderContent(forceShowList = false) {
this.droplab this.droplab
.changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config); .changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config);
super.renderContent(forceShowList); super.renderContent(forceShowList);
} }
init() { init() {
this.droplab this.droplab
.addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init(); .addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
}
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.DropdownNonUser = DropdownNonUser; gl.DropdownNonUser = DropdownNonUser;
})();
...@@ -4,69 +4,67 @@ import AjaxFilter from '~/droplab/plugins/ajax_filter'; ...@@ -4,69 +4,67 @@ import AjaxFilter from '~/droplab/plugins/ajax_filter';
require('./filtered_search_dropdown'); require('./filtered_search_dropdown');
(() => { class DropdownUser extends gl.FilteredSearchDropdown {
class DropdownUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input, filter) {
constructor(droplab, dropdown, input, filter) { super(droplab, dropdown, input, filter);
super(droplab, dropdown, input, filter); this.config = {
this.config = { AjaxFilter: {
AjaxFilter: { endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, searchKey: 'search',
searchKey: 'search', params: {
params: { per_page: 20,
per_page: 20, active: true,
active: true, project_id: this.getProjectId(),
project_id: this.getProjectId(), current_user: true,
current_user: true,
},
searchValueFunction: this.getSearchInput.bind(this),
loadingTemplate: this.loadingTemplate,
onError() {
/* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.');
/* eslint-enable no-new */
},
}, },
}; searchValueFunction: this.getSearchInput.bind(this),
} loadingTemplate: this.loadingTemplate,
onError() {
itemClicked(e) { /* eslint-disable no-new */
super.itemClicked(e, new Flash('An error occured fetching the dropdown data.');
selected => selected.querySelector('.dropdown-light-content').innerText.trim()); /* eslint-enable no-new */
} },
},
renderContent(forceShowList = false) { };
this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config); }
super.renderContent(forceShowList);
}
getProjectId() { itemClicked(e) {
return this.input.getAttribute('data-project-id'); super.itemClicked(e,
} selected => selected.querySelector('.dropdown-light-content').innerText.trim());
}
getSearchInput() { renderContent(forceShowList = false) {
const query = gl.DropdownUtils.getSearchInput(this.input); this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config);
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); super.renderContent(forceShowList);
}
let value = lastToken || ''; getProjectId() {
return this.input.getAttribute('data-project-id');
}
if (value[0] === '@') { getSearchInput() {
value = value.slice(1); const query = gl.DropdownUtils.getSearchInput(this.input);
} const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
// Removes the first character if it is a quotation so that we can search let value = lastToken || '';
// with multiple words
if (value[0] === '"' || value[0] === '\'') {
value = value.slice(1);
}
return value; if (value[0] === '@') {
value = value.slice(1);
} }
init() { // Removes the first character if it is a quotation so that we can search
this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init(); // with multiple words
if (value[0] === '"' || value[0] === '\'') {
value = value.slice(1);
} }
return value;
}
init() {
this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init();
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.DropdownUser = DropdownUser; gl.DropdownUser = DropdownUser;
})();
(() => { const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
class FilteredSearchDropdown {
class FilteredSearchDropdown { constructor(droplab, dropdown, input, filter) {
constructor(droplab, dropdown, input, filter) { this.droplab = droplab;
this.droplab = droplab; this.hookId = input && input.id;
this.hookId = input && input.id; this.input = input;
this.input = input; this.filter = filter;
this.filter = filter; this.dropdown = dropdown;
this.dropdown = dropdown; this.loadingTemplate = `<div class="filter-dropdown-loading">
this.loadingTemplate = `<div class="filter-dropdown-loading"> <i class="fa fa-spinner fa-spin"></i>
<i class="fa fa-spinner fa-spin"></i> </div>`;
</div>`; this.bindEvents();
this.bindEvents(); }
}
bindEvents() {
this.itemClickedWrapper = this.itemClicked.bind(this);
this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
}
unbindEvents() { bindEvents() {
this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); this.itemClickedWrapper = this.itemClicked.bind(this);
} this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
}
getCurrentHook() { unbindEvents() {
return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null; this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper);
} }
itemClicked(e, getValueFunction) { getCurrentHook() {
const { selected } = e.detail; return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null;
}
if (selected.tagName === 'LI' && selected.innerHTML) { itemClicked(e, getValueFunction) {
const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected); const { selected } = e.detail;
if (!dataValueSet) { if (selected.tagName === 'LI' && selected.innerHTML) {
const value = getValueFunction(selected); const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
}
this.resetFilters(); if (!dataValueSet) {
this.dismissDropdown(); const value = getValueFunction(selected);
this.dispatchInputEvent(); gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
} }
}
setAsDropdown() { this.resetFilters();
this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`); this.dismissDropdown();
this.dispatchInputEvent();
} }
}
setOffset(offset = 0) { setAsDropdown() {
if (window.innerWidth > 480) { this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`);
this.dropdown.style.left = `${offset}px`; }
} else {
this.dropdown.style.left = '0px'; setOffset(offset = 0) {
} if (window.innerWidth > 480) {
this.dropdown.style.left = `${offset}px`;
} else {
this.dropdown.style.left = '0px';
} }
}
renderContent(forceShowList = false) { renderContent(forceShowList = false) {
const currentHook = this.getCurrentHook(); const currentHook = this.getCurrentHook();
if (forceShowList && currentHook && currentHook.list.hidden) { if (forceShowList && currentHook && currentHook.list.hidden) {
currentHook.list.show(); currentHook.list.show();
}
} }
}
render(forceRenderContent = false, forceShowList = false) { render(forceRenderContent = false, forceShowList = false) {
this.setAsDropdown(); this.setAsDropdown();
const currentHook = this.getCurrentHook(); const currentHook = this.getCurrentHook();
const firstTimeInitialized = currentHook === null; const firstTimeInitialized = currentHook === null;
if (firstTimeInitialized || forceRenderContent) { if (firstTimeInitialized || forceRenderContent) {
this.renderContent(forceShowList); this.renderContent(forceShowList);
} else if (currentHook.list.list.id !== this.dropdown.id) { } else if (currentHook.list.list.id !== this.dropdown.id) {
this.renderContent(forceShowList); this.renderContent(forceShowList);
}
} }
}
dismissDropdown() { dismissDropdown() {
// Focusing on the input will dismiss dropdown // Focusing on the input will dismiss dropdown
// (default droplab functionality) // (default droplab functionality)
this.input.focus(); this.input.focus();
} }
dispatchInputEvent() { dispatchInputEvent() {
// Propogate input change to FilteredSearchDropdownManager // Propogate input change to FilteredSearchDropdownManager
// so that it can determine which dropdowns to open // so that it can determine which dropdowns to open
this.input.dispatchEvent(new CustomEvent('input', { this.input.dispatchEvent(new CustomEvent('input', {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
})); }));
} }
dispatchFormSubmitEvent() { dispatchFormSubmitEvent() {
// dispatchEvent() is necessary as form.submit() does not // dispatchEvent() is necessary as form.submit() does not
// trigger event handlers // trigger event handlers
this.input.form.dispatchEvent(new Event('submit')); this.input.form.dispatchEvent(new Event('submit'));
} }
hideDropdown() { hideDropdown() {
const currentHook = this.getCurrentHook(); const currentHook = this.getCurrentHook();
if (currentHook) { if (currentHook) {
currentHook.list.hide(); currentHook.list.hide();
}
} }
}
resetFilters() { resetFilters() {
const hook = this.getCurrentHook(); const hook = this.getCurrentHook();
if (hook) { if (hook) {
const data = hook.list.data || []; const data = hook.list.data || [];
const results = data.map((o) => { const results = data.map((o) => {
const updated = o; const updated = o;
updated.droplab_hidden = false; updated.droplab_hidden = false;
return updated; return updated;
}); });
hook.list.render(results); hook.list.render(results);
}
} }
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.FilteredSearchDropdown = FilteredSearchDropdown; gl.FilteredSearchDropdown = FilteredSearchDropdown;
})();
(() => { const tokenKeys = [{
const tokenKeys = [{ key: 'author',
key: 'author', type: 'string',
type: 'string', param: 'username',
param: 'username', symbol: '@',
symbol: '@', }, {
}, { key: 'assignee',
key: 'assignee', type: 'string',
type: 'string', param: 'username',
param: 'username', symbol: '@',
symbol: '@', }, {
}, { key: 'milestone',
key: 'milestone', type: 'string',
type: 'string', param: 'title',
param: 'title', symbol: '%',
symbol: '%', }, {
}, { key: 'label',
key: 'label', type: 'array',
type: 'array', param: 'name[]',
param: 'name[]', symbol: '~',
symbol: '~', }];
}];
const alternativeTokenKeys = [{ const alternativeTokenKeys = [{
key: 'label', key: 'label',
type: 'string', type: 'string',
param: 'name', param: 'name',
symbol: '~', symbol: '~',
}]; }];
const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys); const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys);
const conditions = [{ const conditions = [{
url: 'assignee_id=0', url: 'assignee_id=0',
tokenKey: 'assignee', tokenKey: 'assignee',
value: 'none', value: 'none',
}, { }, {
url: 'milestone_title=No+Milestone', url: 'milestone_title=No+Milestone',
tokenKey: 'milestone', tokenKey: 'milestone',
value: 'none', value: 'none',
}, { }, {
url: 'milestone_title=%23upcoming', url: 'milestone_title=%23upcoming',
tokenKey: 'milestone', tokenKey: 'milestone',
value: 'upcoming', value: 'upcoming',
}, { }, {
url: 'milestone_title=%23started', url: 'milestone_title=%23started',
tokenKey: 'milestone', tokenKey: 'milestone',
value: 'started', value: 'started',
}, { }, {
url: 'label_name[]=No+Label', url: 'label_name[]=No+Label',
tokenKey: 'label', tokenKey: 'label',
value: 'none', value: 'none',
}]; }];
class FilteredSearchTokenKeys { class FilteredSearchTokenKeys {
static get() { static get() {
return tokenKeys; return tokenKeys;
} }
static getAlternatives() { static getAlternatives() {
return alternativeTokenKeys; return alternativeTokenKeys;
} }
static getConditions() { static getConditions() {
return conditions; return conditions;
} }
static searchByKey(key) { static searchByKey(key) {
return tokenKeys.find(tokenKey => tokenKey.key === key) || null; return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
} }
static searchBySymbol(symbol) { static searchBySymbol(symbol) {
return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
} }
static searchByKeyParam(keyParam) { static searchByKeyParam(keyParam) {
return tokenKeysWithAlternative.find((tokenKey) => { return tokenKeysWithAlternative.find((tokenKey) => {
let tokenKeyParam = tokenKey.key; let tokenKeyParam = tokenKey.key;
if (tokenKey.param) { if (tokenKey.param) {
tokenKeyParam += `_${tokenKey.param}`; tokenKeyParam += `_${tokenKey.param}`;
} }
return keyParam === tokenKeyParam; return keyParam === tokenKeyParam;
}) || null; }) || null;
} }
static searchByConditionUrl(url) { static searchByConditionUrl(url) {
return conditions.find(condition => condition.url === url) || null; return conditions.find(condition => condition.url === url) || null;
} }
static searchByConditionKeyValue(key, value) { static searchByConditionKeyValue(key, value) {
return conditions return conditions
.find(condition => condition.tokenKey === key && condition.value === value) || null; .find(condition => condition.tokenKey === key && condition.value === value) || null;
}
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys; gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
})();
require('./filtered_search_token_keys'); require('./filtered_search_token_keys');
(() => { class FilteredSearchTokenizer {
class FilteredSearchTokenizer { static processTokens(input) {
static processTokens(input) { const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key);
const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key); // Regex extracts `(token):(symbol)(value)`
// Regex extracts `(token):(symbol)(value)` // Values that start with a double quote must end in a double quote (same for single)
// Values that start with a double quote must end in a double quote (same for single) const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g'); const tokens = [];
const tokens = []; const tokenIndexes = []; // stores key+value for simple search
const tokenIndexes = []; // stores key+value for simple search let lastToken = null;
let lastToken = null; const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { let tokenValue = v1 || v2 || v3;
let tokenValue = v1 || v2 || v3; let tokenSymbol = symbol;
let tokenSymbol = symbol; let tokenIndex = '';
let tokenIndex = '';
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { tokenSymbol = tokenValue;
tokenSymbol = tokenValue; tokenValue = '';
tokenValue = '';
}
tokenIndex = `${key}:${tokenValue}`;
// Prevent adding duplicates
if (tokenIndexes.indexOf(tokenIndex) === -1) {
tokenIndexes.push(tokenIndex);
tokens.push({
key,
value: tokenValue || '',
symbol: tokenSymbol || '',
});
}
return '';
}).replace(/\s{2,}/g, ' ').trim() || '';
if (tokens.length > 0) {
const last = tokens[tokens.length - 1];
const lastString = `${last.key}:${last.symbol}${last.value}`;
lastToken = input.lastIndexOf(lastString) ===
input.length - lastString.length ? last : searchToken;
} else {
lastToken = searchToken;
} }
return { tokenIndex = `${key}:${tokenValue}`;
tokens,
lastToken, // Prevent adding duplicates
searchToken, if (tokenIndexes.indexOf(tokenIndex) === -1) {
}; tokenIndexes.push(tokenIndex);
tokens.push({
key,
value: tokenValue || '',
symbol: tokenSymbol || '',
});
}
return '';
}).replace(/\s{2,}/g, ' ').trim() || '';
if (tokens.length > 0) {
const last = tokens[tokens.length - 1];
const lastString = `${last.key}:${last.symbol}${last.value}`;
lastToken = input.lastIndexOf(lastString) ===
input.length - lastString.length ? last : searchToken;
} else {
lastToken = searchToken;
} }
return {
tokens,
lastToken,
searchToken,
};
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.FilteredSearchTokenizer = FilteredSearchTokenizer; gl.FilteredSearchTokenizer = FilteredSearchTokenizer;
})();
/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var,
camelcase, one-var-declaration-per-line, quotes, object-shorthand,
prefer-arrow-callback, comma-dangle, consistent-return, yoda,
prefer-rest-params, prefer-spread, no-unused-vars, prefer-template,
promise/catch-or-return */
/* global Api */ /* global Api */
var slice = [].slice; var slice = [].slice;
......
...@@ -39,8 +39,9 @@ ...@@ -39,8 +39,9 @@
if ($issuableDueDate.length) { if ($issuableDueDate.length) {
calendar = new Pikaday({ calendar = new Pikaday({
field: $issuableDueDate.get(0), field: $issuableDueDate.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd', format: 'yyyy-mm-dd',
container: $issuableDueDate.parent().get(0),
onSelect: function(dateText) { onSelect: function(dateText) {
$issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); $issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
} }
......
...@@ -34,17 +34,6 @@ export default { ...@@ -34,17 +34,6 @@ export default {
}; };
}, },
methods: { methods: {
fetch() {
this.poll.makeRequest();
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
},
renderResponse(res) { renderResponse(res) {
const body = JSON.parse(res.body); const body = JSON.parse(res.body);
this.triggerAnimation(body); this.triggerAnimation(body);
...@@ -71,7 +60,17 @@ export default { ...@@ -71,7 +60,17 @@ export default {
}, },
}, },
created() { created() {
this.fetch(); if (!Visibility.hidden()) {
this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
}, },
}; };
</script> </script>
......
...@@ -332,6 +332,9 @@ ...@@ -332,6 +332,9 @@
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(label, $el, e, isMarking) { clicked: function(label, $el, e, isMarking) {
var isIssueIndex, isMRIndex, page, boardsModel; var isIssueIndex, isMRIndex, page, boardsModel;
var fadeOutLoader = () => {
$loading.fadeOut();
};
page = $('body').data('page'); page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
...@@ -396,9 +399,8 @@ ...@@ -396,9 +399,8 @@
$loading.fadeIn(); $loading.fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () { .then(fadeOutLoader)
$loading.fadeOut(); .catch(fadeOutLoader);
});
} }
else { else {
if ($dropdown.hasClass('js-multiselect')) { if ($dropdown.hasClass('js-multiselect')) {
......
...@@ -169,7 +169,10 @@ ...@@ -169,7 +169,10 @@
w.gl.utils.getSelectedFragment = () => { w.gl.utils.getSelectedFragment = () => {
const selection = window.getSelection(); const selection = window.getSelection();
if (selection.rangeCount === 0) return null; if (selection.rangeCount === 0) return null;
const documentFragment = selection.getRangeAt(0).cloneContents(); const documentFragment = document.createDocumentFragment();
for (let i = 0; i < selection.rangeCount; i += 1) {
documentFragment.appendChild(selection.getRangeAt(i).cloneContents());
}
if (documentFragment.textContent.length === 0) return null; if (documentFragment.textContent.length === 0) return null;
return documentFragment; return documentFragment;
...@@ -368,9 +371,9 @@ ...@@ -368,9 +371,9 @@
}); });
}; };
w.gl.utils.setFavicon = (iconName) => { w.gl.utils.setFavicon = (faviconPath) => {
if (faviconEl && iconName) { if (faviconEl && faviconPath) {
faviconEl.setAttribute('href', `/assets/${iconName}.ico`); faviconEl.setAttribute('href', faviconPath);
} }
}; };
...@@ -385,8 +388,8 @@ ...@@ -385,8 +388,8 @@
url: pageUrl, url: pageUrl,
dataType: 'json', dataType: 'json',
success: function(data) { success: function(data) {
if (data && data.icon) { if (data && data.favicon) {
gl.utils.setFavicon(`ci_favicons/${data.icon}`); gl.utils.setFavicon(data.favicon);
} else { } else {
gl.utils.resetFavicon(); gl.utils.resetFavicon();
} }
......
/* eslint-disable import/prefer-default-export */
export const BYTES_IN_KIB = 1024;
/* eslint-disable import/prefer-default-export */ import { BYTES_IN_KIB } from './constants';
/** /**
* Function that allows a number with an X amount of decimals * Function that allows a number with an X amount of decimals
...@@ -32,3 +32,13 @@ export function formatRelevantDigits(number) { ...@@ -32,3 +32,13 @@ export function formatRelevantDigits(number) {
} }
return formattedNumber; return formattedNumber;
} }
/**
* Utility function that calculates KiB of the given bytes.
*
* @param {Number} number bytes
* @return {Number} KiB
*/
export function bytesToKiB(number) {
return number / BYTES_IN_KIB;
}
...@@ -165,6 +165,7 @@ import './syntax_highlight'; ...@@ -165,6 +165,7 @@ import './syntax_highlight';
import './task_list'; import './task_list';
import './todos'; import './todos';
import './tree'; import './tree';
import './usage_ping';
import './user'; import './user';
import './user_tabs'; import './user_tabs';
import './username_validator'; import './username_validator';
...@@ -210,6 +211,14 @@ $(function () { ...@@ -210,6 +211,14 @@ $(function () {
} }
}); });
if (bootstrapBreakpoint === 'xs') {
const $rightSidebar = $('aside.right-sidebar, .page-with-sidebar');
$rightSidebar
.removeClass('right-sidebar-expanded')
.addClass('right-sidebar-collapsed');
}
// prevent default action for disabled buttons // prevent default action for disabled buttons
$('.btn').click(function(e) { $('.btn').click(function(e) {
if ($(this).hasClass('disabled')) { if ($(this).hasClass('disabled')) {
......
...@@ -18,9 +18,10 @@ ...@@ -18,9 +18,10 @@
const calendar = new Pikaday({ const calendar = new Pikaday({
field: $input.get(0), field: $input.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd', format: 'yyyy-mm-dd',
minDate: new Date(), minDate: new Date(),
container: $input.parent().get(0),
onSelect(dateText) { onSelect(dateText) {
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); $input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
......
...@@ -157,7 +157,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; ...@@ -157,7 +157,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
$('.ci-widget-fetching').show(); $('.ci-widget-fetching').show();
return $.getJSON(this.opts.ci_status_url, (function(_this) { return $.getJSON(this.opts.ci_status_url, (function(_this) {
return function(data) { return function(data) {
var message, status, title; var message, status, title, callback;
_this.status = data.status; _this.status = data.status;
_this.hasCi = data.has_ci; _this.hasCi = data.has_ci;
_this.updateMergeButton(_this.status, _this.hasCi); _this.updateMergeButton(_this.status, _this.hasCi);
...@@ -179,6 +179,12 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; ...@@ -179,6 +179,12 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
_this.opts.ci_sha = data.sha; _this.opts.ci_sha = data.sha;
_this.updateCommitUrls(data.sha); _this.updateCommitUrls(data.sha);
} }
if (data.status === "success" || data.status === "failed") {
callback = function() {
return _this.getMergeStatus();
};
return setTimeout(callback, 2000);
}
if (showNotification && data.status) { if (showNotification && data.status) {
status = _this.ciLabelForStatus(data.status); status = _this.ciLabelForStatus(data.status);
if (status === "preparing") { if (status === "preparing") {
......
...@@ -164,6 +164,9 @@ ...@@ -164,6 +164,9 @@
.then(function () { .then(function () {
$dropdown.trigger('loaded.gl.dropdown'); $dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut(); $loading.fadeOut();
})
.catch(() => {
$loading.fadeOut();
}); });
} else { } else {
selected = $selectbox.find('input[type="hidden"]').val(); selected = $selectbox.find('input[type="hidden"]').val();
......
...@@ -71,6 +71,8 @@ class PrometheusGraph { ...@@ -71,6 +71,8 @@ class PrometheusGraph {
this.transformData(metricsResponse); this.transformData(metricsResponse);
this.createGraph(); this.createGraph();
} }
}).catch(() => {
new Flash('An error occurred when trying to load metrics. Please try again.');
}); });
} }
......
...@@ -308,8 +308,10 @@ require('./task_list'); ...@@ -308,8 +308,10 @@ require('./task_list');
if (this.isNewNote(note)) { if (this.isNewNote(note)) {
this.note_ids.push(note.id); this.note_ids.push(note.id);
$notesList = $('ul.main-notes-list');
$notesList.append(note.html).syntaxHighlight(); $notesList = window.$('ul.main-notes-list');
Notes.animateAppendNote(note.html, $notesList);
// Update datetime format on the recent note // Update datetime format on the recent note
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false); gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
this.collapseLongCommitList(); this.collapseLongCommitList();
...@@ -348,7 +350,7 @@ require('./task_list'); ...@@ -348,7 +350,7 @@ require('./task_list');
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion? // is this the first note of discussion?
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); discussionContainer = window.$(`.notes[data-discussion-id="${note.discussion_id}"]`);
if (!discussionContainer.length) { if (!discussionContainer.length) {
discussionContainer = form.closest('.discussion').find('.notes'); discussionContainer = form.closest('.discussion').find('.notes');
} }
...@@ -370,14 +372,13 @@ require('./task_list'); ...@@ -370,14 +372,13 @@ require('./task_list');
row.find(contentContainerClass + ' .content').append($notes.closest('.content').children()); row.find(contentContainerClass + ' .content').append($notes.closest('.content').children());
} }
} }
// Init discussion on 'Discussion' page if it is merge request page // Init discussion on 'Discussion' page if it is merge request page
if ($('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) { if (window.$('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) {
$('ul.main-notes-list').append($(note.discussion_html).renderGFM()); Notes.animateAppendNote(note.discussion_html, window.$('ul.main-notes-list'));
} }
} else { } else {
// append new note to all matching discussions // append new note to all matching discussions
discussionContainer.append($(note.html).renderGFM()); Notes.animateAppendNote(note.html, discussionContainer);
} }
if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_resolvable) { if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_resolvable) {
...@@ -1063,6 +1064,13 @@ require('./task_list'); ...@@ -1063,6 +1064,13 @@ require('./task_list');
return $form; return $form;
}; };
Notes.animateAppendNote = function(noteHTML, $notesList) {
const $note = window.$(noteHTML);
$note.addClass('fade-in').renderGFM();
$notesList.append($note);
};
return Notes; return Notes;
})(); })();
}).call(window); }).call(window);
...@@ -65,6 +65,8 @@ export default { ...@@ -65,6 +65,8 @@ export default {
makeRequest() { makeRequest() {
this.isLoading = true; this.isLoading = true;
$(this.$el).tooltip('destroy');
this.service.postAction(this.endpoint) this.service.postAction(this.endpoint)
.then(() => { .then(() => {
this.isLoading = false; this.isLoading = false;
...@@ -88,9 +90,13 @@ export default { ...@@ -88,9 +90,13 @@ export default {
:aria-label="title" :aria-label="title"
data-container="body" data-container="body"
data-placement="top" data-placement="top"
:disabled="isLoading" :disabled="isLoading">
> <i
<i :class="iconClass" aria-hidden="true"></i> :class="iconClass"
<i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading"></i> aria-hidden="true" />
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
v-if="isLoading" />
</button> </button>
</template> </template>
...@@ -13,7 +13,7 @@ export default { ...@@ -13,7 +13,7 @@ export default {
</script> </script>
<template> <template>
<div class="row empty-state"> <div class="row empty-state js-empty-state">
<div class="col-xs-12"> <div class="col-xs-12">
<div class="svg-content" v-html="pipelinesEmptyStateSVG" /> <div class="svg-content" v-html="pipelinesEmptyStateSVG" />
</div> </div>
......
...@@ -28,6 +28,8 @@ export default { ...@@ -28,6 +28,8 @@ export default {
onClickAction(endpoint) { onClickAction(endpoint) {
this.isLoading = true; this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
this.service.postAction(endpoint) this.service.postAction(endpoint)
.then(() => { .then(() => {
this.isLoading = false; this.isLoading = false;
...@@ -57,6 +59,7 @@ export default { ...@@ -57,6 +59,7 @@ export default {
data-toggle="dropdown" data-toggle="dropdown"
data-placement="top" data-placement="top"
aria-label="Manual job" aria-label="Manual job"
ref="tooltip"
:disabled="isLoading"> :disabled="isLoading">
${playIconSvg} ${playIconSvg}
<i <i
......
/* global Flash */ /* global Flash */
import canceledSvg from 'icons/_icon_status_canceled_borderless.svg'; import StatusIconEntityMap from '../../ci_status_icons';
import createdSvg from 'icons/_icon_status_created_borderless.svg';
import failedSvg from 'icons/_icon_status_failed_borderless.svg';
import manualSvg from 'icons/_icon_status_manual_borderless.svg';
import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
import runningSvg from 'icons/_icon_status_running_borderless.svg';
import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
import successSvg from 'icons/_icon_status_success_borderless.svg';
import warningSvg from 'icons/_icon_status_warning_borderless.svg';
export default { export default {
data() { data() {
const svgsDictionary = {
icon_status_canceled: canceledSvg,
icon_status_created: createdSvg,
icon_status_failed: failedSvg,
icon_status_manual: manualSvg,
icon_status_pending: pendingSvg,
icon_status_running: runningSvg,
icon_status_skipped: skippedSvg,
icon_status_success: successSvg,
icon_status_warning: warningSvg,
};
return { return {
builds: '', builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>', spinner: '<span class="fa fa-spinner fa-spin"></span>',
svg: svgsDictionary[this.stage.status.icon],
}; };
}, },
...@@ -89,6 +68,9 @@ export default { ...@@ -89,6 +68,9 @@ export default {
triggerButtonClass() { triggerButtonClass() {
return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`; return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
}, },
svgHTML() {
return StatusIconEntityMap[this.stage.status.icon];
},
}, },
template: ` template: `
<div> <div>
...@@ -100,7 +82,7 @@ export default { ...@@ -100,7 +82,7 @@ export default {
data-toggle="dropdown" data-toggle="dropdown"
type="button" type="button"
:aria-label="stage.title"> :aria-label="stage.title">
<span v-html="svg" aria-hidden="true"></span> <span v-html="svgHTML" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i> <i class="fa fa-caret-down" aria-hidden="true"></i>
</button> </button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
......
...@@ -57,8 +57,11 @@ import findAndFollowLink from './shortcuts_dashboard_navigation'; ...@@ -57,8 +57,11 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
Shortcuts.prototype.toggleMarkdownPreview = function(e) { Shortcuts.prototype.toggleMarkdownPreview = function(e) {
// Check if short-cut was triggered while in Write Mode // Check if short-cut was triggered while in Write Mode
if ($(e.target).hasClass('js-note-text')) { const $target = $(e.target);
$('.js-md-preview-button').focus(); const $form = $target.closest('form');
if ($target.hasClass('js-note-text')) {
$('.js-md-preview-button', $form).focus();
} }
return $(document).triggerHandler('markdown-preview:toggle', [e]); return $(document).triggerHandler('markdown-preview:toggle', [e]);
}; };
......
/* eslint-disable class-methods-use-this */
/* global Mousetrap */
/* global ShortcutsNavigation */
import findAndFollowLink from './shortcuts_dashboard_navigation';
export default class ShortcutsWiki extends ShortcutsNavigation {
constructor() {
super();
Mousetrap.bind('e', this.editWiki);
}
editWiki() {
findAndFollowLink('.js-wiki-edit');
}
}
function UsagePing() {
const usageDataUrl = $('.usage-data').data('endpoint');
$.ajax({
type: 'GET',
url: usageDataUrl,
dataType: 'html',
success(html) {
$('.usage-data').html(html);
},
});
}
window.gl = window.gl || {};
window.gl.UsagePing = UsagePing;
This diff is collapsed.
...@@ -56,6 +56,9 @@ ...@@ -56,6 +56,9 @@
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () { .then(function () {
$loading.fadeOut(); $loading.fadeOut();
})
.catch(function () {
$loading.fadeOut();
}); });
}; };
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button.vue'; import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../vue_pipelines_index/components/pipelines_actions'; import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts'; import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
import PipelinesStatusComponent from '../../vue_pipelines_index/components/status'; import PipelinesStatusComponent from '../../pipelines/components/status';
import PipelinesStageComponent from '../../vue_pipelines_index/components/stage'; import PipelinesStageComponent from '../../pipelines/components/stage';
import PipelinesUrlComponent from '../../vue_pipelines_index/components/pipeline_url'; import PipelinesUrlComponent from '../../pipelines/components/pipeline_url';
import PipelinesTimeagoComponent from '../../vue_pipelines_index/components/time_ago'; import PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
import CommitComponent from './commit'; import CommitComponent from './commit';
/** /**
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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