Merge remote-tracking branch 'origin/28433-internationalise-cycle-analytics-page' into js-translations
|
@ -67,6 +67,7 @@ stages:
|
||||||
- export CI_NODE_TOTAL=${JOB_NAME[2]}
|
- export CI_NODE_TOTAL=${JOB_NAME[2]}
|
||||||
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
|
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
|
||||||
- export KNAPSACK_GENERATE_REPORT=true
|
- export KNAPSACK_GENERATE_REPORT=true
|
||||||
|
- export CACHE_CLASSES=true
|
||||||
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
|
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
|
||||||
- knapsack rspec "--color --format documentation"
|
- knapsack rspec "--color --format documentation"
|
||||||
artifacts:
|
artifacts:
|
||||||
|
@ -87,6 +88,7 @@ stages:
|
||||||
- export CI_NODE_TOTAL=${JOB_NAME[2]}
|
- export CI_NODE_TOTAL=${JOB_NAME[2]}
|
||||||
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
|
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
|
||||||
- export KNAPSACK_GENERATE_REPORT=true
|
- export KNAPSACK_GENERATE_REPORT=true
|
||||||
|
- export CACHE_CLASSES=true
|
||||||
- cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
|
- cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
|
||||||
- knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
|
- knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
|
||||||
artifacts:
|
artifacts:
|
||||||
|
@ -276,7 +278,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
|
||||||
|
@ -375,9 +376,6 @@ coverage:
|
||||||
|
|
||||||
lint:javascript:
|
lint:javascript:
|
||||||
<<: *dedicated-runner
|
<<: *dedicated-runner
|
||||||
cache:
|
|
||||||
paths:
|
|
||||||
- node_modules/
|
|
||||||
stage: test
|
stage: test
|
||||||
before_script: []
|
before_script: []
|
||||||
script:
|
script:
|
||||||
|
@ -385,9 +383,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:
|
||||||
|
|
13
PROCESS.md
|
@ -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
|
||||||
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
|
||||||
|
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
@ -1,3 +1,5 @@
|
||||||
|
/* global Flash */
|
||||||
|
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
import emojiMap from 'emojis/digests.json';
|
import emojiMap from 'emojis/digests.json';
|
||||||
|
@ -6,6 +8,7 @@ import { glEmojiTag } from './behaviors/gl_emoji';
|
||||||
import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid';
|
import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid';
|
||||||
|
|
||||||
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
|
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
|
||||||
|
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
|
||||||
const requestAnimationFrame = window.requestAnimationFrame ||
|
const requestAnimationFrame = window.requestAnimationFrame ||
|
||||||
window.webkitRequestAnimationFrame ||
|
window.webkitRequestAnimationFrame ||
|
||||||
window.mozRequestAnimationFrame ||
|
window.mozRequestAnimationFrame ||
|
||||||
|
@ -103,8 +106,9 @@ function AwardsHandler() {
|
||||||
const $glEmojiElement = $target.find('gl-emoji');
|
const $glEmojiElement = $target.find('gl-emoji');
|
||||||
const $spriteIconElement = $target.find('.icon');
|
const $spriteIconElement = $target.find('.icon');
|
||||||
const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
|
const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
|
||||||
|
|
||||||
$target.closest('.js-awards-block').addClass('current');
|
$target.closest('.js-awards-block').addClass('current');
|
||||||
return this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji);
|
this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,16 +128,18 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const $menu = $('.emoji-menu');
|
const $menu = $('.emoji-menu');
|
||||||
|
const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent();
|
||||||
|
const $userAuthored = this.isUserAuthored($addBtn);
|
||||||
if ($menu.length) {
|
if ($menu.length) {
|
||||||
if ($menu.is('.is-visible')) {
|
if ($menu.is('.is-visible')) {
|
||||||
$addBtn.removeClass('is-active');
|
$addBtn.removeClass('is-active');
|
||||||
$menu.removeClass('is-visible');
|
$menu.removeClass('is-visible');
|
||||||
$('#emoji_search').blur();
|
$('.js-emoji-menu-search').blur();
|
||||||
} else {
|
} else {
|
||||||
$addBtn.addClass('is-active');
|
$addBtn.addClass('is-active');
|
||||||
this.positionMenu($menu, $addBtn);
|
this.positionMenu($menu, $addBtn);
|
||||||
$menu.addClass('is-visible');
|
$menu.addClass('is-visible');
|
||||||
$('#emoji_search').focus();
|
$('.js-emoji-menu-search').focus();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$addBtn.addClass('is-loading is-active');
|
$addBtn.addClass('is-loading is-active');
|
||||||
|
@ -143,10 +149,12 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) {
|
||||||
this.positionMenu($createdMenu, $addBtn);
|
this.positionMenu($createdMenu, $addBtn);
|
||||||
return setTimeout(() => {
|
return setTimeout(() => {
|
||||||
$createdMenu.addClass('is-visible');
|
$createdMenu.addClass('is-visible');
|
||||||
$('#emoji_search').focus();
|
$('.js-emoji-menu-search').focus();
|
||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$thumbsBtn.toggleClass('disabled', $userAuthored);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the emoji menu with the first category of emojis.
|
// Create the emoji menu with the first category of emojis.
|
||||||
|
@ -174,7 +182,7 @@ AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) {
|
||||||
|
|
||||||
const emojiMenuMarkup = `
|
const emojiMenuMarkup = `
|
||||||
<div class="emoji-menu">
|
<div class="emoji-menu">
|
||||||
<input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" placeholder="Search emoji" />
|
<input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" />
|
||||||
|
|
||||||
<div class="emoji-menu-content">
|
<div class="emoji-menu-content">
|
||||||
${frequentlyUsedCatgegory}
|
${frequentlyUsedCatgegory}
|
||||||
|
@ -259,7 +267,8 @@ AwardsHandler.prototype.addAward = function addAward(
|
||||||
callback,
|
callback,
|
||||||
) {
|
) {
|
||||||
const normalizedEmoji = this.normalizeEmojiName(emoji);
|
const normalizedEmoji = this.normalizeEmojiName(emoji);
|
||||||
this.postEmoji(awardUrl, normalizedEmoji, () => {
|
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
|
||||||
|
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
|
||||||
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
|
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
|
||||||
return typeof callback === 'function' ? callback() : undefined;
|
return typeof callback === 'function' ? callback() : undefined;
|
||||||
});
|
});
|
||||||
|
@ -324,6 +333,10 @@ AwardsHandler.prototype.isActive = function isActive($emojiButton) {
|
||||||
return $emojiButton.hasClass('active');
|
return $emojiButton.hasClass('active');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
AwardsHandler.prototype.isUserAuthored = function isUserAuthored($button) {
|
||||||
|
return $button.hasClass('js-user-authored');
|
||||||
|
};
|
||||||
|
|
||||||
AwardsHandler.prototype.decrementCounter = function decrementCounter($emojiButton, emoji) {
|
AwardsHandler.prototype.decrementCounter = function decrementCounter($emojiButton, emoji) {
|
||||||
const counter = $('.js-counter', $emojiButton);
|
const counter = $('.js-counter', $emojiButton);
|
||||||
const counterNumber = parseInt(counter.text(), 10);
|
const counterNumber = parseInt(counter.text(), 10);
|
||||||
|
@ -428,20 +441,35 @@ AwardsHandler.prototype.createEmoji = function createEmoji(votesBlock, emoji) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
AwardsHandler.prototype.postEmoji = function postEmoji(awardUrl, emoji, callback) {
|
AwardsHandler.prototype.postEmoji = function postEmoji($emojiButton, awardUrl, emoji, callback) {
|
||||||
return $.post(awardUrl, {
|
if (this.isUserAuthored($emojiButton)) {
|
||||||
name: emoji,
|
this.userAuthored($emojiButton);
|
||||||
}, (data) => {
|
} else {
|
||||||
if (data.ok) {
|
$.post(awardUrl, {
|
||||||
callback();
|
name: emoji,
|
||||||
}
|
}, (data) => {
|
||||||
});
|
if (data.ok) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}).fail(() => new Flash('Something went wrong on our end.'));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
AwardsHandler.prototype.findEmojiIcon = function findEmojiIcon(votesBlock, emoji) {
|
AwardsHandler.prototype.findEmojiIcon = function findEmojiIcon(votesBlock, emoji) {
|
||||||
return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`);
|
return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
AwardsHandler.prototype.userAuthored = function userAuthored($emojiButton) {
|
||||||
|
const oldTitle = this.getAwardTooltip($emojiButton);
|
||||||
|
const newTitle = 'You cannot vote on your own issue, MR and note';
|
||||||
|
gl.utils.updateTooltipTitle($emojiButton, newTitle).tooltip('show');
|
||||||
|
// Restore tooltip back to award list
|
||||||
|
return setTimeout(() => {
|
||||||
|
$emojiButton.tooltip('hide');
|
||||||
|
gl.utils.updateTooltipTitle($emojiButton, oldTitle);
|
||||||
|
}, 2800);
|
||||||
|
};
|
||||||
|
|
||||||
AwardsHandler.prototype.scrollToAwards = function scrollToAwards() {
|
AwardsHandler.prototype.scrollToAwards = function scrollToAwards() {
|
||||||
const options = {
|
const options = {
|
||||||
scrollTop: $('.awards').offset().top - 110,
|
scrollTop: $('.awards').offset().top - 110,
|
||||||
|
@ -474,24 +502,41 @@ AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmoj
|
||||||
};
|
};
|
||||||
|
|
||||||
AwardsHandler.prototype.setupSearch = function setupSearch() {
|
AwardsHandler.prototype.setupSearch = function setupSearch() {
|
||||||
this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => {
|
const $search = $('.js-emoji-menu-search');
|
||||||
|
|
||||||
|
this.registerEventListener('on', $search, 'input', (e) => {
|
||||||
const term = $(e.target).val().trim();
|
const term = $(e.target).val().trim();
|
||||||
// Clean previous search results
|
this.searchEmojis(term);
|
||||||
$('ul.emoji-menu-search, h5.emoji-search-title').remove();
|
});
|
||||||
if (term.length > 0) {
|
|
||||||
// Generate a search result block
|
const $menu = $('.emoji-menu');
|
||||||
const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
|
this.registerEventListener('on', $menu, transitionEndEventString, (e) => {
|
||||||
const foundEmojis = this.searchEmojis(term).show();
|
if (e.target === e.currentTarget) {
|
||||||
const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
|
// Clear the search
|
||||||
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
|
this.searchEmojis('');
|
||||||
$('.emoji-menu-content').append(h5).append(ul);
|
|
||||||
} else {
|
|
||||||
$('.emoji-menu-content').children().show();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
AwardsHandler.prototype.searchEmojis = function searchEmojis(term) {
|
AwardsHandler.prototype.searchEmojis = function searchEmojis(term) {
|
||||||
|
const $search = $('.js-emoji-menu-search');
|
||||||
|
$search.val(term);
|
||||||
|
|
||||||
|
// Clean previous search results
|
||||||
|
$('ul.emoji-menu-search, h5.emoji-search-title').remove();
|
||||||
|
if (term.length > 0) {
|
||||||
|
// Generate a search result block
|
||||||
|
const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
|
||||||
|
const foundEmojis = this.findMatchingEmojiElements(term).show();
|
||||||
|
const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
|
||||||
|
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
|
||||||
|
$('.emoji-menu-content').append(h5).append(ul);
|
||||||
|
} else {
|
||||||
|
$('.emoji-menu-content').children().show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AwardsHandler.prototype.findMatchingEmojiElements = function findMatchingEmojiElements(term) {
|
||||||
const safeTerm = term.toLowerCase();
|
const safeTerm = term.toLowerCase();
|
||||||
|
|
||||||
const namesMatchingAlias = [];
|
const namesMatchingAlias = [];
|
||||||
|
|
|
@ -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 {
|
||||||
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());
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|
|
@ -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 () {
|
||||||
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 () {
|
||||||
|
|
||||||
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();
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
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,
|
||||||
|
};
|
|
@ -1,11 +1,11 @@
|
||||||
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';
|
||||||
|
|
|
@ -128,7 +128,7 @@ $(() => {
|
||||||
},
|
},
|
||||||
dismissOverviewDialog() {
|
dismissOverviewDialog() {
|
||||||
this.isOverviewDialogDismissed = true;
|
this.isOverviewDialogDismissed = true;
|
||||||
Cookies.set(OVERVIEW_DIALOG_COOKIE, '1');
|
Cookies.set(OVERVIEW_DIALOG_COOKIE, '1', { expires: 365 });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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() {
|
||||||
data() {
|
return {
|
||||||
return {
|
textareaIsEmpty: true,
|
||||||
textareaIsEmpty: true,
|
discussion: {},
|
||||||
discussion: {},
|
};
|
||||||
};
|
},
|
||||||
},
|
computed: {
|
||||||
computed: {
|
showButton: function () {
|
||||||
showButton: function () {
|
if (this.discussion) {
|
||||||
if (this.discussion) {
|
return this.discussion.isResolvable();
|
||||||
return this.discussion.isResolvable();
|
} else {
|
||||||
} else {
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isDiscussionResolved: function () {
|
|
||||||
return this.discussion.isResolved();
|
|
||||||
},
|
|
||||||
buttonText: function () {
|
|
||||||
if (this.isDiscussionResolved) {
|
|
||||||
if (this.textareaIsEmpty) {
|
|
||||||
return "Unresolve discussion";
|
|
||||||
} else {
|
|
||||||
return "Comment & unresolve discussion";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.textareaIsEmpty) {
|
|
||||||
return "Resolve discussion";
|
|
||||||
} else {
|
|
||||||
return "Comment & resolve discussion";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
isDiscussionResolved: function () {
|
||||||
if (this.discussionId) {
|
return this.discussion.isResolved();
|
||||||
this.discussion = CommentsStore.state[this.discussionId];
|
},
|
||||||
|
buttonText: function () {
|
||||||
|
if (this.isDiscussionResolved) {
|
||||||
|
if (this.textareaIsEmpty) {
|
||||||
|
return "Unresolve discussion";
|
||||||
|
} else {
|
||||||
|
return "Comment & unresolve discussion";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.textareaIsEmpty) {
|
||||||
|
return "Resolve discussion";
|
||||||
|
} else {
|
||||||
|
return "Comment & resolve discussion";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
mounted: function () {
|
|
||||||
if (!this.discussionId) return;
|
|
||||||
|
|
||||||
const $textarea = $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`);
|
|
||||||
this.textareaIsEmpty = $textarea.val() === '';
|
|
||||||
|
|
||||||
$textarea.on('input.comment-and-resolve-btn', () => {
|
|
||||||
this.textareaIsEmpty = $textarea.val() === '';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
destroyed: function () {
|
|
||||||
if (!this.discussionId) return;
|
|
||||||
|
|
||||||
$(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off('input.comment-and-resolve-btn');
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
created() {
|
||||||
|
if (this.discussionId) {
|
||||||
|
this.discussion = CommentsStore.state[this.discussionId];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted: function () {
|
||||||
|
if (!this.discussionId) return;
|
||||||
|
|
||||||
Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
|
const $textarea = $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`);
|
||||||
})(window);
|
this.textareaIsEmpty = $textarea.val() === '';
|
||||||
|
|
||||||
|
$textarea.on('input.comment-and-resolve-btn', () => {
|
||||||
|
this.textareaIsEmpty = $textarea.val() === '';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroyed: function () {
|
||||||
|
if (!this.discussionId) return;
|
||||||
|
|
||||||
|
$(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off('input.comment-and-resolve-btn');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
|
||||||
|
|
|
@ -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"
|
|
||||||
:title="note.authorName + ': ' + note.noteTruncated"
|
|
||||||
:src="note.authorAvatar"
|
|
||||||
@click="clickedAvatar($event)" />
|
|
||||||
<span v-if="notesCount > shownAvatars"
|
|
||||||
class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
|
|
||||||
data-container="body"
|
|
||||||
data-placement="top"
|
|
||||||
ref="extraComments"
|
|
||||||
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="note.authorName + ': ' + note.noteTruncated"
|
||||||
v-if="isVisible"
|
:src="note.authorAvatar"
|
||||||
v-html="collapseIcon">
|
@click="clickedAvatar($event)" />
|
||||||
</button>
|
<span v-if="notesCount > shownAvatars"
|
||||||
|
class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
|
||||||
|
data-container="body"
|
||||||
|
data-placement="top"
|
||||||
|
ref="extraComments"
|
||||||
|
role="button"
|
||||||
|
:data-line-type="lineType"
|
||||||
|
:title="extraNotesTitle"
|
||||||
|
@click="clickedAvatar($event)">{{ moreText }}</span>
|
||||||
</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.setDiscussionVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
notesSubset() {
|
||||||
|
let notes = [];
|
||||||
|
|
||||||
|
if (this.discussion) {
|
||||||
|
notes = Object.keys(this.discussion.notes)
|
||||||
|
.slice(0, this.shownAvatars)
|
||||||
|
.map(noteId => this.discussion.notes[noteId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return notes;
|
||||||
|
},
|
||||||
|
extraNotesTitle() {
|
||||||
|
if (this.discussion) {
|
||||||
|
const extra = this.discussion.notesCount() - this.shownAvatars;
|
||||||
|
|
||||||
|
return `${extra} more comment${extra > 1 ? 's' : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
discussion() {
|
||||||
|
return this.storeState[this.discussionId];
|
||||||
|
},
|
||||||
|
notesCount() {
|
||||||
|
if (this.discussion) {
|
||||||
|
return this.discussion.notesCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
moreText() {
|
||||||
|
const plusSign = this.notesCount < 100 ? '+' : '';
|
||||||
|
|
||||||
|
return `${plusSign}${this.notesCount - this.shownAvatars}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickedAvatar(e) {
|
||||||
|
notes.addDiffNote(e);
|
||||||
|
|
||||||
|
// Toggle the active state of the toggle all button
|
||||||
|
this.toggleDiscussionsToggleState();
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.addNoCommentClass();
|
|
||||||
this.setDiscussionVisible();
|
this.setDiscussionVisible();
|
||||||
|
|
||||||
this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
|
$('.has-tooltip', this.$el).tooltip('fixTitle');
|
||||||
});
|
$('.has-tooltip', this.$el).tooltip('hide');
|
||||||
|
|
||||||
$(document).on('toggle.comments', () => {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.setDiscussionVisible();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
destroyed() {
|
addNoCommentClass() {
|
||||||
$(document).off('toggle.comments');
|
const notesCount = this.notesCount;
|
||||||
|
|
||||||
|
$(this.$el).closest('.js-avatar-container')
|
||||||
|
.toggleClass('js-no-comment-btn', notesCount > 0)
|
||||||
|
.nextUntil('.js-avatar-container')
|
||||||
|
.toggleClass('js-no-comment-btn', notesCount > 0);
|
||||||
},
|
},
|
||||||
watch: {
|
toggleDiscussionsToggleState() {
|
||||||
storeState: {
|
const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
|
||||||
handler() {
|
const $visibleNotesHolders = $notesHolders.filter(':visible');
|
||||||
this.$nextTick(() => {
|
const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments');
|
||||||
$('.has-tooltip', this.$el).tooltip('fixTitle');
|
|
||||||
|
|
||||||
// We need to add/remove a class to an element that is outside the Vue instance
|
$toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length);
|
||||||
this.addNoCommentClass();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
deep: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
setDiscussionVisible() {
|
||||||
notesSubset() {
|
this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible');
|
||||||
let notes = [];
|
|
||||||
|
|
||||||
if (this.discussion) {
|
|
||||||
notes = Object.keys(this.discussion.notes)
|
|
||||||
.slice(0, this.shownAvatars)
|
|
||||||
.map(noteId => this.discussion.notes[noteId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return notes;
|
|
||||||
},
|
|
||||||
extraNotesTitle() {
|
|
||||||
if (this.discussion) {
|
|
||||||
const extra = this.discussion.notesCount() - this.shownAvatars;
|
|
||||||
|
|
||||||
return `${extra} more comment${extra > 1 ? 's' : ''}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
discussion() {
|
|
||||||
return this.storeState[this.discussionId];
|
|
||||||
},
|
|
||||||
notesCount() {
|
|
||||||
if (this.discussion) {
|
|
||||||
return this.discussion.notesCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
},
|
|
||||||
moreText() {
|
|
||||||
const plusSign = this.notesCount < 100 ? '+' : '';
|
|
||||||
|
|
||||||
return `${plusSign}${this.notesCount - this.shownAvatars}`;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
},
|
||||||
clickedAvatar(e) {
|
});
|
||||||
notes.addDiffNote(e);
|
|
||||||
|
|
||||||
// Toggle the active state of the toggle all button
|
Vue.component('diff-note-avatars', DiffNoteAvatars);
|
||||||
this.toggleDiscussionsToggleState();
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.setDiscussionVisible();
|
|
||||||
|
|
||||||
$('.has-tooltip', this.$el).tooltip('fixTitle');
|
|
||||||
$('.has-tooltip', this.$el).tooltip('hide');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
addNoCommentClass() {
|
|
||||||
const notesCount = this.notesCount;
|
|
||||||
|
|
||||||
$(this.$el).closest('.js-avatar-container')
|
|
||||||
.toggleClass('js-no-comment-btn', notesCount > 0)
|
|
||||||
.nextUntil('.js-avatar-container')
|
|
||||||
.toggleClass('js-no-comment-btn', notesCount > 0);
|
|
||||||
},
|
|
||||||
toggleDiscussionsToggleState() {
|
|
||||||
const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
|
|
||||||
const $visibleNotesHolders = $notesHolders.filter(':visible');
|
|
||||||
const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments');
|
|
||||||
|
|
||||||
$toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length);
|
|
||||||
},
|
|
||||||
setDiscussionVisible() {
|
|
||||||
this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Vue.component('diff-note-avatars', DiffNoteAvatars);
|
|
||||||
})();
|
|
||||||
|
|
|
@ -4,192 +4,190 @@
|
||||||
|
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
(() => {
|
const JumpToDiscussion = Vue.extend({
|
||||||
const JumpToDiscussion = Vue.extend({
|
mixins: [DiscussionMixins],
|
||||||
mixins: [DiscussionMixins],
|
props: {
|
||||||
props: {
|
discussionId: String
|
||||||
discussionId: String
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
discussions: CommentsStore.state,
|
||||||
|
discussion: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
allResolved: function () {
|
||||||
|
return this.unresolvedDiscussionCount === 0;
|
||||||
},
|
},
|
||||||
data: function () {
|
showButton: function () {
|
||||||
return {
|
if (this.discussionId) {
|
||||||
discussions: CommentsStore.state,
|
if (this.unresolvedDiscussionCount > 1) {
|
||||||
discussion: {},
|
return true;
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
allResolved: function () {
|
|
||||||
return this.unresolvedDiscussionCount === 0;
|
|
||||||
},
|
|
||||||
showButton: function () {
|
|
||||||
if (this.discussionId) {
|
|
||||||
if (this.unresolvedDiscussionCount > 1) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return this.discussionId !== this.lastResolvedId;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return this.unresolvedDiscussionCount >= 1;
|
return this.discussionId !== this.lastResolvedId;
|
||||||
}
|
}
|
||||||
},
|
} else {
|
||||||
lastResolvedId: function () {
|
return this.unresolvedDiscussionCount >= 1;
|
||||||
let lastId;
|
|
||||||
for (const discussionId in this.discussions) {
|
|
||||||
const discussion = this.discussions[discussionId];
|
|
||||||
|
|
||||||
if (!discussion.isResolved()) {
|
|
||||||
lastId = discussion.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lastId;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
lastResolvedId: function () {
|
||||||
jumpToNextUnresolvedDiscussion: function () {
|
let lastId;
|
||||||
let discussionsSelector;
|
for (const discussionId in this.discussions) {
|
||||||
let discussionIdsInScope;
|
const discussion = this.discussions[discussionId];
|
||||||
let firstUnresolvedDiscussionId;
|
|
||||||
let nextUnresolvedDiscussionId;
|
|
||||||
let activeTab = window.mrTabs.currentAction;
|
|
||||||
let hasDiscussionsToJumpTo = true;
|
|
||||||
let jumpToFirstDiscussion = !this.discussionId;
|
|
||||||
|
|
||||||
const discussionIdsForElements = function(elements) {
|
if (!discussion.isResolved()) {
|
||||||
return elements.map(function() {
|
lastId = discussion.id;
|
||||||
return $(this).attr('data-discussion-id');
|
|
||||||
}).toArray();
|
|
||||||
};
|
|
||||||
|
|
||||||
const discussions = this.discussions;
|
|
||||||
|
|
||||||
if (activeTab === 'diffs') {
|
|
||||||
discussionsSelector = '.diffs .notes[data-discussion-id]';
|
|
||||||
discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
|
|
||||||
|
|
||||||
let unresolvedDiscussionCount = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < discussionIdsInScope.length; i += 1) {
|
|
||||||
const discussionId = discussionIdsInScope[i];
|
|
||||||
const discussion = discussions[discussionId];
|
|
||||||
if (discussion && !discussion.isResolved()) {
|
|
||||||
unresolvedDiscussionCount += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.discussionId && !this.discussion.isResolved()) {
|
|
||||||
// If this is the last unresolved discussion on the diffs tab,
|
|
||||||
// there are no discussions to jump to.
|
|
||||||
if (unresolvedDiscussionCount === 1) {
|
|
||||||
hasDiscussionsToJumpTo = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If there are no unresolved discussions on the diffs tab at all,
|
|
||||||
// there are no discussions to jump to.
|
|
||||||
if (unresolvedDiscussionCount === 0) {
|
|
||||||
hasDiscussionsToJumpTo = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (activeTab !== 'notes') {
|
|
||||||
// If we are on the commits or builds tabs,
|
|
||||||
// there are no discussions to jump to.
|
|
||||||
hasDiscussionsToJumpTo = false;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return lastId;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
jumpToNextUnresolvedDiscussion: function () {
|
||||||
|
let discussionsSelector;
|
||||||
|
let discussionIdsInScope;
|
||||||
|
let firstUnresolvedDiscussionId;
|
||||||
|
let nextUnresolvedDiscussionId;
|
||||||
|
let activeTab = window.mrTabs.currentAction;
|
||||||
|
let hasDiscussionsToJumpTo = true;
|
||||||
|
let jumpToFirstDiscussion = !this.discussionId;
|
||||||
|
|
||||||
if (!hasDiscussionsToJumpTo) {
|
const discussionIdsForElements = function(elements) {
|
||||||
// If there are no discussions to jump to on the current page,
|
return elements.map(function() {
|
||||||
// switch to the notes tab and jump to the first disucssion there.
|
return $(this).attr('data-discussion-id');
|
||||||
window.mrTabs.activateTab('notes');
|
}).toArray();
|
||||||
activeTab = 'notes';
|
};
|
||||||
jumpToFirstDiscussion = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeTab === 'notes') {
|
const discussions = this.discussions;
|
||||||
discussionsSelector = '.discussion[data-discussion-id]';
|
|
||||||
discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
|
if (activeTab === 'diffs') {
|
||||||
}
|
discussionsSelector = '.diffs .notes[data-discussion-id]';
|
||||||
|
discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
|
||||||
|
|
||||||
|
let unresolvedDiscussionCount = 0;
|
||||||
|
|
||||||
let currentDiscussionFound = false;
|
|
||||||
for (let i = 0; i < discussionIdsInScope.length; i += 1) {
|
for (let i = 0; i < discussionIdsInScope.length; i += 1) {
|
||||||
const discussionId = discussionIdsInScope[i];
|
const discussionId = discussionIdsInScope[i];
|
||||||
const discussion = discussions[discussionId];
|
const discussion = discussions[discussionId];
|
||||||
|
if (discussion && !discussion.isResolved()) {
|
||||||
if (!discussion) {
|
unresolvedDiscussionCount += 1;
|
||||||
// Discussions for comments on commits in this MR don't have a resolved status.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!firstUnresolvedDiscussionId && !discussion.isResolved()) {
|
|
||||||
firstUnresolvedDiscussionId = discussionId;
|
|
||||||
|
|
||||||
if (jumpToFirstDiscussion) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!jumpToFirstDiscussion) {
|
|
||||||
if (currentDiscussionFound) {
|
|
||||||
if (!discussion.isResolved()) {
|
|
||||||
nextUnresolvedDiscussionId = discussionId;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (discussionId === this.discussionId) {
|
|
||||||
currentDiscussionFound = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId;
|
if (this.discussionId && !this.discussion.isResolved()) {
|
||||||
|
// If this is the last unresolved discussion on the diffs tab,
|
||||||
if (!nextUnresolvedDiscussionId) {
|
// there are no discussions to jump to.
|
||||||
return;
|
if (unresolvedDiscussionCount === 1) {
|
||||||
}
|
hasDiscussionsToJumpTo = false;
|
||||||
|
|
||||||
let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`);
|
|
||||||
|
|
||||||
if (activeTab === 'notes') {
|
|
||||||
$target = $target.closest('.note-discussion');
|
|
||||||
|
|
||||||
// If the next discussion is closed, toggle it open.
|
|
||||||
if ($target.find('.js-toggle-content').is(':hidden')) {
|
|
||||||
$target.find('.js-toggle-button i').trigger('click');
|
|
||||||
}
|
}
|
||||||
} else if (activeTab === 'diffs') {
|
} else {
|
||||||
// Resolved discussions are hidden in the diffs tab by default.
|
// If there are no unresolved discussions on the diffs tab at all,
|
||||||
// If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab.
|
// there are no discussions to jump to.
|
||||||
// When jumping between unresolved discussions on the diffs tab, we show them.
|
if (unresolvedDiscussionCount === 0) {
|
||||||
$target.closest(".content").show();
|
hasDiscussionsToJumpTo = false;
|
||||||
|
|
||||||
$target = $target.closest("tr.notes_holder");
|
|
||||||
$target.show();
|
|
||||||
|
|
||||||
// If we are on the diffs tab, we don't scroll to the discussion itself, but to
|
|
||||||
// 4 diff lines above it: the line the discussion was in response to + 3 context
|
|
||||||
let prevEl;
|
|
||||||
for (let i = 0; i < 4; i += 1) {
|
|
||||||
prevEl = $target.prev();
|
|
||||||
|
|
||||||
// If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
|
|
||||||
if (!prevEl.hasClass("line_holder")) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$target = prevEl;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (activeTab !== 'notes') {
|
||||||
$.scrollTo($target, {
|
// If we are on the commits or builds tabs,
|
||||||
offset: 0
|
// there are no discussions to jump to.
|
||||||
});
|
hasDiscussionsToJumpTo = false;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.discussion = this.discussions[this.discussionId];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Vue.component('jump-to-discussion', JumpToDiscussion);
|
if (!hasDiscussionsToJumpTo) {
|
||||||
})();
|
// If there are no discussions to jump to on the current page,
|
||||||
|
// switch to the notes tab and jump to the first disucssion there.
|
||||||
|
window.mrTabs.activateTab('notes');
|
||||||
|
activeTab = 'notes';
|
||||||
|
jumpToFirstDiscussion = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTab === 'notes') {
|
||||||
|
discussionsSelector = '.discussion[data-discussion-id]';
|
||||||
|
discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentDiscussionFound = false;
|
||||||
|
for (let i = 0; i < discussionIdsInScope.length; i += 1) {
|
||||||
|
const discussionId = discussionIdsInScope[i];
|
||||||
|
const discussion = discussions[discussionId];
|
||||||
|
|
||||||
|
if (!discussion) {
|
||||||
|
// Discussions for comments on commits in this MR don't have a resolved status.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstUnresolvedDiscussionId && !discussion.isResolved()) {
|
||||||
|
firstUnresolvedDiscussionId = discussionId;
|
||||||
|
|
||||||
|
if (jumpToFirstDiscussion) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jumpToFirstDiscussion) {
|
||||||
|
if (currentDiscussionFound) {
|
||||||
|
if (!discussion.isResolved()) {
|
||||||
|
nextUnresolvedDiscussionId = discussionId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (discussionId === this.discussionId) {
|
||||||
|
currentDiscussionFound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId;
|
||||||
|
|
||||||
|
if (!nextUnresolvedDiscussionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`);
|
||||||
|
|
||||||
|
if (activeTab === 'notes') {
|
||||||
|
$target = $target.closest('.note-discussion');
|
||||||
|
|
||||||
|
// If the next discussion is closed, toggle it open.
|
||||||
|
if ($target.find('.js-toggle-content').is(':hidden')) {
|
||||||
|
$target.find('.js-toggle-button i').trigger('click');
|
||||||
|
}
|
||||||
|
} else if (activeTab === 'diffs') {
|
||||||
|
// Resolved discussions are hidden in the diffs tab by default.
|
||||||
|
// If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab.
|
||||||
|
// When jumping between unresolved discussions on the diffs tab, we show them.
|
||||||
|
$target.closest(".content").show();
|
||||||
|
|
||||||
|
$target = $target.closest("tr.notes_holder");
|
||||||
|
$target.show();
|
||||||
|
|
||||||
|
// If we are on the diffs tab, we don't scroll to the discussion itself, but to
|
||||||
|
// 4 diff lines above it: the line the discussion was in response to + 3 context
|
||||||
|
let prevEl;
|
||||||
|
for (let i = 0; i < 4; i += 1) {
|
||||||
|
prevEl = $target.prev();
|
||||||
|
|
||||||
|
// If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
|
||||||
|
if (!prevEl.hasClass("line_holder")) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$target = prevEl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$.scrollTo($target, {
|
||||||
|
offset: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.discussion = this.discussions[this.discussionId];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Vue.component('jump-to-discussion', JumpToDiscussion);
|
||||||
|
|
|
@ -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,115 @@
|
||||||
|
|
||||||
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 () {
|
||||||
data: function () {
|
return {
|
||||||
return {
|
discussions: CommentsStore.state,
|
||||||
discussions: CommentsStore.state,
|
loading: false
|
||||||
loading: false,
|
};
|
||||||
note: {},
|
},
|
||||||
};
|
watch: {
|
||||||
},
|
'discussions': {
|
||||||
watch: {
|
handler: 'updateTooltip',
|
||||||
'discussions': {
|
deep: true
|
||||||
handler: 'updateTooltip',
|
|
||||||
deep: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
discussion: function () {
|
|
||||||
return this.discussions[this.discussionId];
|
|
||||||
},
|
|
||||||
buttonText: function () {
|
|
||||||
if (this.isResolved) {
|
|
||||||
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 () {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
$(this.$refs.button)
|
|
||||||
.tooltip('hide')
|
|
||||||
.tooltip('fixTitle');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
resolve: function () {
|
|
||||||
if (!this.canResolve) return;
|
|
||||||
|
|
||||||
let promise;
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
if (this.isResolved) {
|
|
||||||
promise = ResolveService
|
|
||||||
.unresolve(this.noteId);
|
|
||||||
} else {
|
|
||||||
promise = ResolveService
|
|
||||||
.resolve(this.noteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
promise.then((response) => {
|
|
||||||
this.loading = false;
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
|
||||||
const data = response.json();
|
|
||||||
const resolved_by = data ? data.resolved_by : null;
|
|
||||||
|
|
||||||
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
|
|
||||||
this.discussion.updateHeadline(data);
|
|
||||||
} else {
|
|
||||||
new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateTooltip();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.note = this.discussion.getNote(this.noteId);
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
computed: {
|
||||||
|
discussion: function () {
|
||||||
|
return this.discussions[this.discussionId];
|
||||||
|
},
|
||||||
|
note: function () {
|
||||||
|
return this.discussion ? this.discussion.getNote(this.noteId) : {};
|
||||||
|
},
|
||||||
|
buttonText: function () {
|
||||||
|
if (this.isResolved) {
|
||||||
|
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 () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
$(this.$refs.button)
|
||||||
|
.tooltip('hide')
|
||||||
|
.tooltip('fixTitle');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resolve: function () {
|
||||||
|
if (!this.canResolve) return;
|
||||||
|
|
||||||
Vue.component('resolve-btn', ResolveBtn);
|
let promise;
|
||||||
})();
|
this.loading = true;
|
||||||
|
|
||||||
|
if (this.isResolved) {
|
||||||
|
promise = ResolveService
|
||||||
|
.unresolve(this.noteId);
|
||||||
|
} else {
|
||||||
|
promise = ResolveService
|
||||||
|
.resolve(this.noteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then((response) => {
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
const data = response.json();
|
||||||
|
const resolved_by = data ? data.resolved_by : null;
|
||||||
|
|
||||||
|
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
|
||||||
|
this.discussion.updateHeadline(data);
|
||||||
|
} else {
|
||||||
|
new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateTooltip();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
|
|
@ -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 @@
|
||||||
|
|
||||||
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: {
|
||||||
computed: {
|
showButton: function () {
|
||||||
showButton: function () {
|
if (this.discussion) {
|
||||||
if (this.discussion) {
|
return this.discussion.isResolvable();
|
||||||
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: {
|
isDiscussionResolved: function () {
|
||||||
resolve: function () {
|
if (this.discussion) {
|
||||||
ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
|
return this.discussion.isResolved();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: function () {
|
buttonText: function () {
|
||||||
CommentsStore.createDiscussion(this.discussionId, this.canResolve);
|
if (this.isDiscussionResolved) {
|
||||||
|
return "Unresolve discussion";
|
||||||
this.discussion = CommentsStore.state[this.discussionId];
|
} else {
|
||||||
|
return "Resolve discussion";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loading: function () {
|
||||||
|
if (this.discussion) {
|
||||||
|
return this.discussion.loading;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
methods: {
|
||||||
|
resolve: function () {
|
||||||
|
ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
CommentsStore.createDiscussion(this.discussionId, this.canResolve);
|
||||||
|
|
||||||
Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
|
this.discussion = CommentsStore.state[this.discussionId];
|
||||||
})();
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
|
||||||
|
|
|
@ -1,37 +1,35 @@
|
||||||
/* 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;
|
|
||||||
},
|
|
||||||
unresolvedDiscussionCount: function () {
|
|
||||||
let unresolvedCount = 0;
|
|
||||||
|
|
||||||
for (const discussionId in this.discussions) {
|
|
||||||
const discussion = this.discussions[discussionId];
|
|
||||||
|
|
||||||
if (!discussion.isResolved()) {
|
|
||||||
unresolvedCount += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return unresolvedCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return resolvedCount;
|
||||||
|
},
|
||||||
|
unresolvedDiscussionCount: function () {
|
||||||
|
let unresolvedCount = 0;
|
||||||
|
|
||||||
|
for (const discussionId in this.discussions) {
|
||||||
|
const discussion = this.discussions[discussionId];
|
||||||
|
|
||||||
|
if (!discussion.isResolved()) {
|
||||||
|
unresolvedCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return unresolvedCount;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
})(window);
|
};
|
||||||
|
|
|
@ -9,76 +9,74 @@ 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) {
|
|
||||||
return this.noteResource.delete({ noteId }, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleResolveForDiscussion(mergeRequestId, discussionId) {
|
|
||||||
const discussion = CommentsStore.state[discussionId];
|
|
||||||
const isResolved = discussion.isResolved();
|
|
||||||
let promise;
|
|
||||||
|
|
||||||
if (isResolved) {
|
|
||||||
promise = this.unResolveAll(mergeRequestId, discussionId);
|
|
||||||
} else {
|
|
||||||
promise = this.resolveAll(mergeRequestId, discussionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
promise.then((response) => {
|
|
||||||
discussion.loading = false;
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
|
||||||
const data = response.json();
|
|
||||||
const resolved_by = data ? data.resolved_by : null;
|
|
||||||
|
|
||||||
if (isResolved) {
|
|
||||||
discussion.unResolveAllNotes();
|
|
||||||
} else {
|
|
||||||
discussion.resolveAllNotes(resolved_by);
|
|
||||||
}
|
|
||||||
|
|
||||||
discussion.updateHeadline(data);
|
|
||||||
} else {
|
|
||||||
new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
resolveAll(mergeRequestId, discussionId) {
|
|
||||||
const discussion = CommentsStore.state[discussionId];
|
|
||||||
|
|
||||||
discussion.loading = true;
|
|
||||||
|
|
||||||
return this.discussionResource.save({
|
|
||||||
mergeRequestId,
|
|
||||||
discussionId
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
unResolveAll(mergeRequestId, discussionId) {
|
|
||||||
const discussion = CommentsStore.state[discussionId];
|
|
||||||
|
|
||||||
discussion.loading = true;
|
|
||||||
|
|
||||||
return this.discussionResource.delete({
|
|
||||||
mergeRequestId,
|
|
||||||
discussionId
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gl.DiffNotesResolveServiceClass = ResolveServiceClass;
|
resolve(noteId) {
|
||||||
})();
|
return this.noteResource.save({ noteId }, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
unresolve(noteId) {
|
||||||
|
return this.noteResource.delete({ noteId }, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleResolveForDiscussion(mergeRequestId, discussionId) {
|
||||||
|
const discussion = CommentsStore.state[discussionId];
|
||||||
|
const isResolved = discussion.isResolved();
|
||||||
|
let promise;
|
||||||
|
|
||||||
|
if (isResolved) {
|
||||||
|
promise = this.unResolveAll(mergeRequestId, discussionId);
|
||||||
|
} else {
|
||||||
|
promise = this.resolveAll(mergeRequestId, discussionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then((response) => {
|
||||||
|
discussion.loading = false;
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
const data = response.json();
|
||||||
|
const resolved_by = data ? data.resolved_by : null;
|
||||||
|
|
||||||
|
if (isResolved) {
|
||||||
|
discussion.unResolveAllNotes();
|
||||||
|
} else {
|
||||||
|
discussion.resolveAllNotes(resolved_by);
|
||||||
|
}
|
||||||
|
|
||||||
|
discussion.updateHeadline(data);
|
||||||
|
} else {
|
||||||
|
new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveAll(mergeRequestId, discussionId) {
|
||||||
|
const discussion = CommentsStore.state[discussionId];
|
||||||
|
|
||||||
|
discussion.loading = true;
|
||||||
|
|
||||||
|
return this.discussionResource.save({
|
||||||
|
mergeRequestId,
|
||||||
|
discussionId
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
unResolveAll(mergeRequestId, discussionId) {
|
||||||
|
const discussion = CommentsStore.state[discussionId];
|
||||||
|
|
||||||
|
discussion.loading = true;
|
||||||
|
|
||||||
|
return this.discussionResource.delete({
|
||||||
|
mergeRequestId,
|
||||||
|
discussionId
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.DiffNotesResolveServiceClass = ResolveServiceClass;
|
||||||
|
|
|
@ -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) {
|
|
||||||
discussion.canResolve = canResolve;
|
|
||||||
}
|
|
||||||
|
|
||||||
return discussion;
|
|
||||||
},
|
|
||||||
create: function (noteObj) {
|
|
||||||
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 = [];
|
|
||||||
|
|
||||||
for (const discussionId in this.state) {
|
|
||||||
const discussion = this.state[discussionId];
|
|
||||||
|
|
||||||
if (!discussion.isResolved()) {
|
|
||||||
ids.push(discussion.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
})(window);
|
if (canResolve !== undefined) {
|
||||||
|
discussion.canResolve = canResolve;
|
||||||
|
}
|
||||||
|
|
||||||
|
return discussion;
|
||||||
|
},
|
||||||
|
create: function (noteObj) {
|
||||||
|
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 = [];
|
||||||
|
|
||||||
|
for (const discussionId in this.state) {
|
||||||
|
const discussion = this.state[discussionId];
|
||||||
|
|
||||||
|
if (!discussion.isResolved()) {
|
||||||
|
ids.push(discussion.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -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';
|
||||||
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');
|
||||||
|
|
||||||
|
@ -148,13 +150,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 +367,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 +421,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;
|
||||||
|
|
|
@ -2,10 +2,12 @@ const DATA_TRIGGER = 'data-dropdown-trigger';
|
||||||
const DATA_DROPDOWN = 'data-dropdown';
|
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 IGNORE_CLASS = 'droplab-item-ignore';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DATA_TRIGGER,
|
DATA_TRIGGER,
|
||||||
DATA_DROPDOWN,
|
DATA_DROPDOWN,
|
||||||
SELECTED_CLASS,
|
SELECTED_CLASS,
|
||||||
ACTIVE_CLASS,
|
ACTIVE_CLASS,
|
||||||
|
IGNORE_CLASS,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* 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, {
|
||||||
|
|
||||||
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() {
|
||||||
"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: "",
|
||||||
|
@ -66,7 +69,10 @@ window.DropzoneInput = (function() {
|
||||||
form_textarea.focus();
|
form_textarea.focus();
|
||||||
},
|
},
|
||||||
success: function(header, response) {
|
success: function(header, response) {
|
||||||
pasteText(response.link.markdown);
|
const processingFileCount = this.getQueuedFiles().length + this.getUploadingFiles().length;
|
||||||
|
const shouldPad = processingFileCount >= 1;
|
||||||
|
|
||||||
|
pasteText(response.link.markdown, shouldPad);
|
||||||
},
|
},
|
||||||
error: function(temp) {
|
error: function(temp) {
|
||||||
var checkIfMsgExists, errorAlert;
|
var checkIfMsgExists, errorAlert;
|
||||||
|
@ -123,16 +129,19 @@ window.DropzoneInput = (function() {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
pasteText = function(text) {
|
pasteText = function(text, shouldPad) {
|
||||||
var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
|
var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
|
||||||
var formattedText = text + "\n\n";
|
var formattedText = text;
|
||||||
caretStart = $(child)[0].selectionStart;
|
if (shouldPad) formattedText += "\n\n";
|
||||||
caretEnd = $(child)[0].selectionEnd;
|
const textarea = child.get(0);
|
||||||
|
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) {
|
||||||
|
@ -176,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) {
|
||||||
|
@ -211,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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,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;
|
||||||
|
@ -62,6 +64,7 @@ export default {
|
||||||
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
|
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
|
||||||
data-container="body"
|
data-container="body"
|
||||||
data-toggle="dropdown"
|
data-toggle="dropdown"
|
||||||
|
ref="tooltip"
|
||||||
:title="title"
|
:title="title"
|
||||||
:aria-label="title"
|
:aria-label="title"
|
||||||
:disabled="isLoading">
|
:disabled="isLoading">
|
||||||
|
|
|
@ -21,7 +21,6 @@ export default {
|
||||||
class="btn monitoring-url has-tooltip"
|
class="btn monitoring-url has-tooltip"
|
||||||
data-container="body"
|
data-container="body"
|
||||||
:href="monitoringUrl"
|
:href="monitoringUrl"
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer nofollow"
|
rel="noopener noreferrer nofollow"
|
||||||
:title="title"
|
:title="title"
|
||||||
:aria-label="title">
|
:aria-label="title">
|
||||||
|
|
|
@ -36,6 +36,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;
|
||||||
|
|
|
@ -36,6 +36,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;
|
||||||
|
|
|
@ -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) {
|
itemClicked(e) {
|
||||||
const { selected } = e.detail;
|
const { selected } = e.detail;
|
||||||
|
|
||||||
if (selected.tagName === 'LI') {
|
if (selected.tagName === 'LI') {
|
||||||
if (selected.hasAttribute('data-value')) {
|
if (selected.hasAttribute('data-value')) {
|
||||||
this.dismissDropdown();
|
this.dismissDropdown();
|
||||||
} else if (selected.getAttribute('data-action') === 'submit') {
|
} else if (selected.getAttribute('data-action') === 'submit') {
|
||||||
this.dismissDropdown();
|
this.dismissDropdown();
|
||||||
this.dispatchFormSubmitEvent();
|
this.dispatchFormSubmitEvent();
|
||||||
} else {
|
} else {
|
||||||
const token = selected.querySelector('.js-filter-hint').innerText.trim();
|
const token = selected.querySelector('.js-filter-hint').innerText.trim();
|
||||||
const tag = selected.querySelector('.js-filter-tag').innerText.trim();
|
const tag = selected.querySelector('.js-filter-tag').innerText.trim();
|
||||||
|
|
||||||
if (tag.length) {
|
if (tag.length) {
|
||||||
// Get previous input values in the input field and convert them into visual tokens
|
// Get previous input values in the input field and convert them into visual tokens
|
||||||
const previousInputValues = this.input.value.split(' ');
|
const previousInputValues = this.input.value.split(' ');
|
||||||
const searchTerms = [];
|
const searchTerms = [];
|
||||||
|
|
||||||
previousInputValues.forEach((value, index) => {
|
previousInputValues.forEach((value, index) => {
|
||||||
searchTerms.push(value);
|
searchTerms.push(value);
|
||||||
|
|
||||||
if (index === previousInputValues.length - 1
|
if (index === previousInputValues.length - 1
|
||||||
&& token.indexOf(value.toLowerCase()) !== -1) {
|
&& token.indexOf(value.toLowerCase()) !== -1) {
|
||||||
searchTerms.pop();
|
searchTerms.pop();
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (searchTerms.length > 0) {
|
|
||||||
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
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() {
|
|
||||||
const dropdownData = [];
|
|
||||||
|
|
||||||
[].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
|
|
||||||
const { icon, hint, tag, type } = dropdownMenu.dataset;
|
|
||||||
if (icon && hint && tag) {
|
|
||||||
dropdownData.push(
|
|
||||||
Object.assign({
|
|
||||||
icon: `fa-${icon}`,
|
|
||||||
hint,
|
|
||||||
tag: `<${tag}>`,
|
|
||||||
}, type && { type }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config);
|
|
||||||
this.droplab.setData(this.hookId, dropdownData);
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.gl = window.gl || {};
|
renderContent() {
|
||||||
gl.DropdownHint = DropdownHint;
|
const dropdownData = [];
|
||||||
})();
|
|
||||||
|
[].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
|
||||||
|
const { icon, hint, tag, type } = dropdownMenu.dataset;
|
||||||
|
if (icon && hint && tag) {
|
||||||
|
dropdownData.push(
|
||||||
|
Object.assign({
|
||||||
|
icon: `fa-${icon}`,
|
||||||
|
hint,
|
||||||
|
tag: `<${tag}>`,
|
||||||
|
}, type && { type }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config);
|
||||||
|
this.droplab.setData(this.hookId, dropdownData);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.gl = window.gl || {};
|
||||||
|
gl.DropdownHint = DropdownHint;
|
||||||
|
|
|
@ -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) {
|
|
||||||
super.itemClicked(e, (selected) => {
|
|
||||||
const title = selected.querySelector('.js-data-value').innerText.trim();
|
|
||||||
return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderContent(forceShowList = false) {
|
|
||||||
this.droplab
|
|
||||||
.changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config);
|
|
||||||
super.renderContent(forceShowList);
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.droplab
|
|
||||||
.addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.gl = window.gl || {};
|
itemClicked(e) {
|
||||||
gl.DropdownNonUser = DropdownNonUser;
|
super.itemClicked(e, (selected) => {
|
||||||
})();
|
const title = selected.querySelector('.js-data-value').innerText.trim();
|
||||||
|
return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent(forceShowList = false) {
|
||||||
|
this.droplab
|
||||||
|
.changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config);
|
||||||
|
super.renderContent(forceShowList);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.droplab
|
||||||
|
.addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.gl = window.gl || {};
|
||||||
|
gl.DropdownNonUser = DropdownNonUser;
|
||||||
|
|
|
@ -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() {
|
|
||||||
return this.input.getAttribute('data-project-id');
|
|
||||||
}
|
|
||||||
|
|
||||||
getSearchInput() {
|
|
||||||
const query = gl.DropdownUtils.getSearchInput(this.input);
|
|
||||||
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
|
|
||||||
|
|
||||||
let value = lastToken || '';
|
|
||||||
|
|
||||||
if (value[0] === '@') {
|
|
||||||
value = value.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removes the first character if it is a quotation so that we can search
|
|
||||||
// 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 || {};
|
itemClicked(e) {
|
||||||
gl.DropdownUser = DropdownUser;
|
super.itemClicked(e,
|
||||||
})();
|
selected => selected.querySelector('.dropdown-light-content').innerText.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent(forceShowList = false) {
|
||||||
|
this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config);
|
||||||
|
super.renderContent(forceShowList);
|
||||||
|
}
|
||||||
|
|
||||||
|
getProjectId() {
|
||||||
|
return this.input.getAttribute('data-project-id');
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearchInput() {
|
||||||
|
const query = gl.DropdownUtils.getSearchInput(this.input);
|
||||||
|
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
|
||||||
|
|
||||||
|
let value = lastToken || '';
|
||||||
|
|
||||||
|
if (value[0] === '@') {
|
||||||
|
value = value.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes the first character if it is a quotation so that we can search
|
||||||
|
// 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 || {};
|
||||||
|
gl.DropdownUser = DropdownUser;
|
||||||
|
|
|
@ -1,183 +1,181 @@
|
||||||
import FilteredSearchContainer from './container';
|
import FilteredSearchContainer from './container';
|
||||||
|
|
||||||
(() => {
|
class DropdownUtils {
|
||||||
class DropdownUtils {
|
static getEscapedText(text) {
|
||||||
static getEscapedText(text) {
|
let escapedText = text;
|
||||||
let escapedText = text;
|
const hasSpace = text.indexOf(' ') !== -1;
|
||||||
const hasSpace = text.indexOf(' ') !== -1;
|
const hasDoubleQuote = text.indexOf('"') !== -1;
|
||||||
const hasDoubleQuote = text.indexOf('"') !== -1;
|
|
||||||
|
|
||||||
// Encapsulate value with quotes if it has spaces
|
// Encapsulate value with quotes if it has spaces
|
||||||
// Known side effect: values's with both single and double quotes
|
// Known side effect: values's with both single and double quotes
|
||||||
// won't escape properly
|
// won't escape properly
|
||||||
if (hasSpace) {
|
if (hasSpace) {
|
||||||
if (hasDoubleQuote) {
|
if (hasDoubleQuote) {
|
||||||
escapedText = `'${text}'`;
|
escapedText = `'${text}'`;
|
||||||
} else {
|
} else {
|
||||||
// Encapsulate singleQuotes or if it hasSpace
|
// Encapsulate singleQuotes or if it hasSpace
|
||||||
escapedText = `"${text}"`;
|
escapedText = `"${text}"`;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return escapedText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static filterWithSymbol(filterSymbol, input, item) {
|
return escapedText;
|
||||||
const updatedItem = item;
|
|
||||||
const searchInput = gl.DropdownUtils.getSearchInput(input);
|
|
||||||
|
|
||||||
const title = updatedItem.title.toLowerCase();
|
|
||||||
let value = searchInput.toLowerCase();
|
|
||||||
let symbol = '';
|
|
||||||
|
|
||||||
// Remove the symbol for filter
|
|
||||||
if (value[0] === filterSymbol) {
|
|
||||||
symbol = value[0];
|
|
||||||
value = value.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removes the first character if it is a quotation so that we can search
|
|
||||||
// with multiple words
|
|
||||||
if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
|
|
||||||
value = value.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eg. filterSymbol = ~ for labels
|
|
||||||
const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
|
|
||||||
const match = title.indexOf(`${symbol}${value}`) !== -1;
|
|
||||||
|
|
||||||
updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
|
|
||||||
|
|
||||||
return updatedItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
static filterHint(input, item) {
|
|
||||||
const updatedItem = item;
|
|
||||||
const searchInput = gl.DropdownUtils.getSearchQuery(input);
|
|
||||||
const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput);
|
|
||||||
const lastKey = lastToken.key || lastToken || '';
|
|
||||||
const allowMultiple = item.type === 'array';
|
|
||||||
const itemInExistingTokens = tokens.some(t => t.key === item.hint);
|
|
||||||
|
|
||||||
if (!allowMultiple && itemInExistingTokens) {
|
|
||||||
updatedItem.droplab_hidden = true;
|
|
||||||
} else if (!lastKey || searchInput.split('').last() === ' ') {
|
|
||||||
updatedItem.droplab_hidden = false;
|
|
||||||
} else if (lastKey) {
|
|
||||||
const split = lastKey.split(':');
|
|
||||||
const tokenName = split[0].split(' ').last();
|
|
||||||
|
|
||||||
const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
|
|
||||||
updatedItem.droplab_hidden = tokenName ? match : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
static setDataValueIfSelected(filter, selected) {
|
|
||||||
const dataValue = selected.getAttribute('data-value');
|
|
||||||
|
|
||||||
if (dataValue) {
|
|
||||||
gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return boolean based on whether it was set
|
|
||||||
return dataValue !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determines the full search query (visual tokens + input)
|
|
||||||
static getSearchQuery(untilInput = false) {
|
|
||||||
const container = FilteredSearchContainer.container;
|
|
||||||
const tokens = [].slice.call(container.querySelectorAll('.tokens-container li'));
|
|
||||||
const values = [];
|
|
||||||
|
|
||||||
if (untilInput) {
|
|
||||||
const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token'));
|
|
||||||
// Add one to include input-token to the tokens array
|
|
||||||
tokens.splice(inputIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens.forEach((token) => {
|
|
||||||
if (token.classList.contains('js-visual-token')) {
|
|
||||||
const name = token.querySelector('.name');
|
|
||||||
const value = token.querySelector('.value');
|
|
||||||
const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
|
|
||||||
let valueText = '';
|
|
||||||
|
|
||||||
if (value && value.innerText) {
|
|
||||||
valueText = value.innerText;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token.className.indexOf('filtered-search-token') !== -1) {
|
|
||||||
values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`);
|
|
||||||
} else {
|
|
||||||
values.push(name.innerText);
|
|
||||||
}
|
|
||||||
} else if (token.classList.contains('input-token')) {
|
|
||||||
const { isLastVisualTokenValid } =
|
|
||||||
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
|
||||||
|
|
||||||
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
|
|
||||||
const inputValue = input && input.value;
|
|
||||||
|
|
||||||
if (isLastVisualTokenValid) {
|
|
||||||
values.push(inputValue);
|
|
||||||
} else {
|
|
||||||
const previous = values.pop();
|
|
||||||
values.push(`${previous}${inputValue}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return values
|
|
||||||
.map(value => value.trim())
|
|
||||||
.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
static getSearchInput(filteredSearchInput) {
|
|
||||||
const inputValue = filteredSearchInput.value;
|
|
||||||
const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
|
|
||||||
|
|
||||||
return inputValue.slice(0, right);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getInputSelectionPosition(input) {
|
|
||||||
const selectionStart = input.selectionStart;
|
|
||||||
let inputValue = input.value;
|
|
||||||
// Replace all spaces inside quote marks with underscores
|
|
||||||
// (will continue to match entire string until an end quote is found if any)
|
|
||||||
// This helps with matching the beginning & end of a token:key
|
|
||||||
inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_'));
|
|
||||||
|
|
||||||
// Get the right position for the word selected
|
|
||||||
// Regex matches first space
|
|
||||||
let right = inputValue.slice(selectionStart).search(/\s/);
|
|
||||||
|
|
||||||
if (right >= 0) {
|
|
||||||
right += selectionStart;
|
|
||||||
} else if (right < 0) {
|
|
||||||
right = inputValue.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the left position for the word selected
|
|
||||||
// Regex matches last non-whitespace character
|
|
||||||
let left = inputValue.slice(0, right).search(/\S+$/);
|
|
||||||
|
|
||||||
if (selectionStart === 0) {
|
|
||||||
left = 0;
|
|
||||||
} else if (selectionStart === inputValue.length && left < 0) {
|
|
||||||
left = inputValue.length;
|
|
||||||
} else if (left < 0) {
|
|
||||||
left = selectionStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
left,
|
|
||||||
right,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.gl = window.gl || {};
|
static filterWithSymbol(filterSymbol, input, item) {
|
||||||
gl.DropdownUtils = DropdownUtils;
|
const updatedItem = item;
|
||||||
})();
|
const searchInput = gl.DropdownUtils.getSearchInput(input);
|
||||||
|
|
||||||
|
const title = updatedItem.title.toLowerCase();
|
||||||
|
let value = searchInput.toLowerCase();
|
||||||
|
let symbol = '';
|
||||||
|
|
||||||
|
// Remove the symbol for filter
|
||||||
|
if (value[0] === filterSymbol) {
|
||||||
|
symbol = value[0];
|
||||||
|
value = value.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes the first character if it is a quotation so that we can search
|
||||||
|
// with multiple words
|
||||||
|
if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
|
||||||
|
value = value.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eg. filterSymbol = ~ for labels
|
||||||
|
const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
|
||||||
|
const match = title.indexOf(`${symbol}${value}`) !== -1;
|
||||||
|
|
||||||
|
updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
|
||||||
|
|
||||||
|
return updatedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
static filterHint(input, item) {
|
||||||
|
const updatedItem = item;
|
||||||
|
const searchInput = gl.DropdownUtils.getSearchQuery(input);
|
||||||
|
const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput);
|
||||||
|
const lastKey = lastToken.key || lastToken || '';
|
||||||
|
const allowMultiple = item.type === 'array';
|
||||||
|
const itemInExistingTokens = tokens.some(t => t.key === item.hint);
|
||||||
|
|
||||||
|
if (!allowMultiple && itemInExistingTokens) {
|
||||||
|
updatedItem.droplab_hidden = true;
|
||||||
|
} else if (!lastKey || searchInput.split('').last() === ' ') {
|
||||||
|
updatedItem.droplab_hidden = false;
|
||||||
|
} else if (lastKey) {
|
||||||
|
const split = lastKey.split(':');
|
||||||
|
const tokenName = split[0].split(' ').last();
|
||||||
|
|
||||||
|
const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
|
||||||
|
updatedItem.droplab_hidden = tokenName ? match : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
static setDataValueIfSelected(filter, selected) {
|
||||||
|
const dataValue = selected.getAttribute('data-value');
|
||||||
|
|
||||||
|
if (dataValue) {
|
||||||
|
gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return boolean based on whether it was set
|
||||||
|
return dataValue !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines the full search query (visual tokens + input)
|
||||||
|
static getSearchQuery(untilInput = false) {
|
||||||
|
const container = FilteredSearchContainer.container;
|
||||||
|
const tokens = [].slice.call(container.querySelectorAll('.tokens-container li'));
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
if (untilInput) {
|
||||||
|
const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token'));
|
||||||
|
// Add one to include input-token to the tokens array
|
||||||
|
tokens.splice(inputIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.forEach((token) => {
|
||||||
|
if (token.classList.contains('js-visual-token')) {
|
||||||
|
const name = token.querySelector('.name');
|
||||||
|
const value = token.querySelector('.value');
|
||||||
|
const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
|
||||||
|
let valueText = '';
|
||||||
|
|
||||||
|
if (value && value.innerText) {
|
||||||
|
valueText = value.innerText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.className.indexOf('filtered-search-token') !== -1) {
|
||||||
|
values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`);
|
||||||
|
} else {
|
||||||
|
values.push(name.innerText);
|
||||||
|
}
|
||||||
|
} else if (token.classList.contains('input-token')) {
|
||||||
|
const { isLastVisualTokenValid } =
|
||||||
|
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
||||||
|
|
||||||
|
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
|
||||||
|
const inputValue = input && input.value;
|
||||||
|
|
||||||
|
if (isLastVisualTokenValid) {
|
||||||
|
values.push(inputValue);
|
||||||
|
} else {
|
||||||
|
const previous = values.pop();
|
||||||
|
values.push(`${previous}${inputValue}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return values
|
||||||
|
.map(value => value.trim())
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
static getSearchInput(filteredSearchInput) {
|
||||||
|
const inputValue = filteredSearchInput.value;
|
||||||
|
const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
|
||||||
|
|
||||||
|
return inputValue.slice(0, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInputSelectionPosition(input) {
|
||||||
|
const selectionStart = input.selectionStart;
|
||||||
|
let inputValue = input.value;
|
||||||
|
// Replace all spaces inside quote marks with underscores
|
||||||
|
// (will continue to match entire string until an end quote is found if any)
|
||||||
|
// This helps with matching the beginning & end of a token:key
|
||||||
|
inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_'));
|
||||||
|
|
||||||
|
// Get the right position for the word selected
|
||||||
|
// Regex matches first space
|
||||||
|
let right = inputValue.slice(selectionStart).search(/\s/);
|
||||||
|
|
||||||
|
if (right >= 0) {
|
||||||
|
right += selectionStart;
|
||||||
|
} else if (right < 0) {
|
||||||
|
right = inputValue.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the left position for the word selected
|
||||||
|
// Regex matches last non-whitespace character
|
||||||
|
let left = inputValue.slice(0, right).search(/\S+$/);
|
||||||
|
|
||||||
|
if (selectionStart === 0) {
|
||||||
|
left = 0;
|
||||||
|
} else if (selectionStart === inputValue.length && left < 0) {
|
||||||
|
left = inputValue.length;
|
||||||
|
} else if (left < 0) {
|
||||||
|
left = selectionStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.gl = window.gl || {};
|
||||||
|
gl.DropdownUtils = DropdownUtils;
|
||||||
|
|
|
@ -1,124 +1,122 @@
|
||||||
(() => {
|
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() {
|
bindEvents() {
|
||||||
this.itemClickedWrapper = this.itemClicked.bind(this);
|
this.itemClickedWrapper = this.itemClicked.bind(this);
|
||||||
this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
|
this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
unbindEvents() {
|
unbindEvents() {
|
||||||
this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper);
|
this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentHook() {
|
getCurrentHook() {
|
||||||
return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null;
|
return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
itemClicked(e, getValueFunction) {
|
itemClicked(e, getValueFunction) {
|
||||||
const { selected } = e.detail;
|
const { selected } = e.detail;
|
||||||
|
|
||||||
if (selected.tagName === 'LI' && selected.innerHTML) {
|
if (selected.tagName === 'LI' && selected.innerHTML) {
|
||||||
const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
|
const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
|
||||||
|
|
||||||
if (!dataValueSet) {
|
if (!dataValueSet) {
|
||||||
const value = getValueFunction(selected);
|
const value = getValueFunction(selected);
|
||||||
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
|
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
|
||||||
}
|
|
||||||
|
|
||||||
this.resetFilters();
|
|
||||||
this.dismissDropdown();
|
|
||||||
this.dispatchInputEvent();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
setAsDropdown() {
|
this.resetFilters();
|
||||||
this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`);
|
this.dismissDropdown();
|
||||||
}
|
this.dispatchInputEvent();
|
||||||
|
|
||||||
setOffset(offset = 0) {
|
|
||||||
if (window.innerWidth > 480) {
|
|
||||||
this.dropdown.style.left = `${offset}px`;
|
|
||||||
} else {
|
|
||||||
this.dropdown.style.left = '0px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderContent(forceShowList = false) {
|
|
||||||
const currentHook = this.getCurrentHook();
|
|
||||||
if (forceShowList && currentHook && currentHook.list.hidden) {
|
|
||||||
currentHook.list.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render(forceRenderContent = false, forceShowList = false) {
|
|
||||||
this.setAsDropdown();
|
|
||||||
|
|
||||||
const currentHook = this.getCurrentHook();
|
|
||||||
const firstTimeInitialized = currentHook === null;
|
|
||||||
|
|
||||||
if (firstTimeInitialized || forceRenderContent) {
|
|
||||||
this.renderContent(forceShowList);
|
|
||||||
} else if (currentHook.list.list.id !== this.dropdown.id) {
|
|
||||||
this.renderContent(forceShowList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dismissDropdown() {
|
|
||||||
// Focusing on the input will dismiss dropdown
|
|
||||||
// (default droplab functionality)
|
|
||||||
this.input.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchInputEvent() {
|
|
||||||
// Propogate input change to FilteredSearchDropdownManager
|
|
||||||
// so that it can determine which dropdowns to open
|
|
||||||
this.input.dispatchEvent(new CustomEvent('input', {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchFormSubmitEvent() {
|
|
||||||
// dispatchEvent() is necessary as form.submit() does not
|
|
||||||
// trigger event handlers
|
|
||||||
this.input.form.dispatchEvent(new Event('submit'));
|
|
||||||
}
|
|
||||||
|
|
||||||
hideDropdown() {
|
|
||||||
const currentHook = this.getCurrentHook();
|
|
||||||
if (currentHook) {
|
|
||||||
currentHook.list.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetFilters() {
|
|
||||||
const hook = this.getCurrentHook();
|
|
||||||
|
|
||||||
if (hook) {
|
|
||||||
const data = hook.list.data || [];
|
|
||||||
const results = data.map((o) => {
|
|
||||||
const updated = o;
|
|
||||||
updated.droplab_hidden = false;
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
hook.list.render(results);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.gl = window.gl || {};
|
setAsDropdown() {
|
||||||
gl.FilteredSearchDropdown = FilteredSearchDropdown;
|
this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`);
|
||||||
})();
|
}
|
||||||
|
|
||||||
|
setOffset(offset = 0) {
|
||||||
|
if (window.innerWidth > 480) {
|
||||||
|
this.dropdown.style.left = `${offset}px`;
|
||||||
|
} else {
|
||||||
|
this.dropdown.style.left = '0px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent(forceShowList = false) {
|
||||||
|
const currentHook = this.getCurrentHook();
|
||||||
|
if (forceShowList && currentHook && currentHook.list.hidden) {
|
||||||
|
currentHook.list.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(forceRenderContent = false, forceShowList = false) {
|
||||||
|
this.setAsDropdown();
|
||||||
|
|
||||||
|
const currentHook = this.getCurrentHook();
|
||||||
|
const firstTimeInitialized = currentHook === null;
|
||||||
|
|
||||||
|
if (firstTimeInitialized || forceRenderContent) {
|
||||||
|
this.renderContent(forceShowList);
|
||||||
|
} else if (currentHook.list.list.id !== this.dropdown.id) {
|
||||||
|
this.renderContent(forceShowList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dismissDropdown() {
|
||||||
|
// Focusing on the input will dismiss dropdown
|
||||||
|
// (default droplab functionality)
|
||||||
|
this.input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchInputEvent() {
|
||||||
|
// Propogate input change to FilteredSearchDropdownManager
|
||||||
|
// so that it can determine which dropdowns to open
|
||||||
|
this.input.dispatchEvent(new CustomEvent('input', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchFormSubmitEvent() {
|
||||||
|
// dispatchEvent() is necessary as form.submit() does not
|
||||||
|
// trigger event handlers
|
||||||
|
this.input.form.dispatchEvent(new Event('submit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
hideDropdown() {
|
||||||
|
const currentHook = this.getCurrentHook();
|
||||||
|
if (currentHook) {
|
||||||
|
currentHook.list.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetFilters() {
|
||||||
|
const hook = this.getCurrentHook();
|
||||||
|
|
||||||
|
if (hook) {
|
||||||
|
const data = hook.list.data || [];
|
||||||
|
const results = data.map((o) => {
|
||||||
|
const updated = o;
|
||||||
|
updated.droplab_hidden = false;
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
hook.list.render(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.gl = window.gl || {};
|
||||||
|
gl.FilteredSearchDropdown = FilteredSearchDropdown;
|
||||||
|
|
|
@ -1,191 +1,189 @@
|
||||||
import DropLab from '~/droplab/drop_lab';
|
import DropLab from '~/droplab/drop_lab';
|
||||||
import FilteredSearchContainer from './container';
|
import FilteredSearchContainer from './container';
|
||||||
|
|
||||||
(() => {
|
class FilteredSearchDropdownManager {
|
||||||
class FilteredSearchDropdownManager {
|
constructor(baseEndpoint = '', page) {
|
||||||
constructor(baseEndpoint = '', page) {
|
this.container = FilteredSearchContainer.container;
|
||||||
this.container = FilteredSearchContainer.container;
|
this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
|
||||||
this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
|
this.tokenizer = gl.FilteredSearchTokenizer;
|
||||||
this.tokenizer = gl.FilteredSearchTokenizer;
|
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
|
||||||
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
|
this.filteredSearchInput = this.container.querySelector('.filtered-search');
|
||||||
this.filteredSearchInput = this.container.querySelector('.filtered-search');
|
this.page = page;
|
||||||
this.page = page;
|
|
||||||
|
|
||||||
this.setupMapping();
|
this.setupMapping();
|
||||||
|
|
||||||
this.cleanupWrapper = this.cleanup.bind(this);
|
this.cleanupWrapper = this.cleanup.bind(this);
|
||||||
document.addEventListener('beforeunload', this.cleanupWrapper);
|
document.addEventListener('beforeunload', this.cleanupWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
if (this.droplab) {
|
if (this.droplab) {
|
||||||
this.droplab.destroy();
|
|
||||||
this.droplab = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setupMapping();
|
|
||||||
|
|
||||||
document.removeEventListener('beforeunload', this.cleanupWrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupMapping() {
|
|
||||||
this.mapping = {
|
|
||||||
author: {
|
|
||||||
reference: null,
|
|
||||||
gl: 'DropdownUser',
|
|
||||||
element: this.container.querySelector('#js-dropdown-author'),
|
|
||||||
},
|
|
||||||
assignee: {
|
|
||||||
reference: null,
|
|
||||||
gl: 'DropdownUser',
|
|
||||||
element: this.container.querySelector('#js-dropdown-assignee'),
|
|
||||||
},
|
|
||||||
milestone: {
|
|
||||||
reference: null,
|
|
||||||
gl: 'DropdownNonUser',
|
|
||||||
extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
|
|
||||||
element: this.container.querySelector('#js-dropdown-milestone'),
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
reference: null,
|
|
||||||
gl: 'DropdownNonUser',
|
|
||||||
extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
|
|
||||||
element: this.container.querySelector('#js-dropdown-label'),
|
|
||||||
},
|
|
||||||
hint: {
|
|
||||||
reference: null,
|
|
||||||
gl: 'DropdownHint',
|
|
||||||
element: this.container.querySelector('#js-dropdown-hint'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static addWordToInput(tokenName, tokenValue = '', clicked = false) {
|
|
||||||
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
|
|
||||||
|
|
||||||
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
|
|
||||||
input.value = '';
|
|
||||||
|
|
||||||
if (clicked) {
|
|
||||||
gl.FilteredSearchVisualTokens.moveInputToTheRight();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurrentDropdownOffset() {
|
|
||||||
this.updateDropdownOffset(this.currentDropdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDropdownOffset(key) {
|
|
||||||
// Always align dropdown with the input field
|
|
||||||
let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
|
|
||||||
|
|
||||||
const maxInputWidth = 240;
|
|
||||||
const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
|
|
||||||
|
|
||||||
// Make sure offset never exceeds the input container
|
|
||||||
const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
|
|
||||||
if (offsetMaxWidth < offset) {
|
|
||||||
offset = offsetMaxWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mapping[key].reference.setOffset(offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
load(key, firstLoad = false) {
|
|
||||||
const mappingKey = this.mapping[key];
|
|
||||||
const glClass = mappingKey.gl;
|
|
||||||
const element = mappingKey.element;
|
|
||||||
let forceShowList = false;
|
|
||||||
|
|
||||||
if (!mappingKey.reference) {
|
|
||||||
const dl = this.droplab;
|
|
||||||
const defaultArguments = [null, dl, element, this.filteredSearchInput, key];
|
|
||||||
const glArguments = defaultArguments.concat(mappingKey.extraArguments || []);
|
|
||||||
|
|
||||||
// Passing glArguments to `new gl[glClass](<arguments>)`
|
|
||||||
mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstLoad) {
|
|
||||||
mappingKey.reference.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.currentDropdown === 'hint') {
|
|
||||||
// Force the dropdown to show if it was clicked from the hint dropdown
|
|
||||||
forceShowList = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateDropdownOffset(key);
|
|
||||||
mappingKey.reference.render(firstLoad, forceShowList);
|
|
||||||
|
|
||||||
this.currentDropdown = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadDropdown(dropdownName = '') {
|
|
||||||
let firstLoad = false;
|
|
||||||
|
|
||||||
if (!this.droplab) {
|
|
||||||
firstLoad = true;
|
|
||||||
this.droplab = new DropLab();
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
|
|
||||||
const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
|
|
||||||
&& this.mapping[match.key];
|
|
||||||
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
|
|
||||||
|
|
||||||
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
|
|
||||||
const key = match && match.key ? match.key : 'hint';
|
|
||||||
this.load(key, firstLoad);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setDropdown() {
|
|
||||||
const query = gl.DropdownUtils.getSearchQuery(true);
|
|
||||||
const { lastToken, searchToken } = this.tokenizer.processTokens(query);
|
|
||||||
|
|
||||||
if (this.currentDropdown) {
|
|
||||||
this.updateCurrentDropdownOffset();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastToken === searchToken && lastToken !== null) {
|
|
||||||
// Token is not fully initialized yet because it has no value
|
|
||||||
// Eg. token = 'label:'
|
|
||||||
|
|
||||||
const split = lastToken.split(':');
|
|
||||||
const dropdownName = split[0].split(' ').last();
|
|
||||||
this.loadDropdown(split.length > 1 ? dropdownName : '');
|
|
||||||
} else if (lastToken) {
|
|
||||||
// Token has been initialized into an object because it has a value
|
|
||||||
this.loadDropdown(lastToken.key);
|
|
||||||
} else {
|
|
||||||
this.loadDropdown('hint');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetDropdowns() {
|
|
||||||
if (!this.currentDropdown) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force current dropdown to hide
|
|
||||||
this.mapping[this.currentDropdown].reference.hideDropdown();
|
|
||||||
|
|
||||||
// Re-Load dropdown
|
|
||||||
this.setDropdown();
|
|
||||||
|
|
||||||
// Reset filters for current dropdown
|
|
||||||
this.mapping[this.currentDropdown].reference.resetFilters();
|
|
||||||
|
|
||||||
// Reposition dropdown so that it is aligned with cursor
|
|
||||||
this.updateDropdownOffset(this.currentDropdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyDroplab() {
|
|
||||||
this.droplab.destroy();
|
this.droplab.destroy();
|
||||||
|
this.droplab = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setupMapping();
|
||||||
|
|
||||||
|
document.removeEventListener('beforeunload', this.cleanupWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMapping() {
|
||||||
|
this.mapping = {
|
||||||
|
author: {
|
||||||
|
reference: null,
|
||||||
|
gl: 'DropdownUser',
|
||||||
|
element: this.container.querySelector('#js-dropdown-author'),
|
||||||
|
},
|
||||||
|
assignee: {
|
||||||
|
reference: null,
|
||||||
|
gl: 'DropdownUser',
|
||||||
|
element: this.container.querySelector('#js-dropdown-assignee'),
|
||||||
|
},
|
||||||
|
milestone: {
|
||||||
|
reference: null,
|
||||||
|
gl: 'DropdownNonUser',
|
||||||
|
extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
|
||||||
|
element: this.container.querySelector('#js-dropdown-milestone'),
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
reference: null,
|
||||||
|
gl: 'DropdownNonUser',
|
||||||
|
extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
|
||||||
|
element: this.container.querySelector('#js-dropdown-label'),
|
||||||
|
},
|
||||||
|
hint: {
|
||||||
|
reference: null,
|
||||||
|
gl: 'DropdownHint',
|
||||||
|
element: this.container.querySelector('#js-dropdown-hint'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static addWordToInput(tokenName, tokenValue = '', clicked = false) {
|
||||||
|
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
|
||||||
|
|
||||||
|
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
|
||||||
|
input.value = '';
|
||||||
|
|
||||||
|
if (clicked) {
|
||||||
|
gl.FilteredSearchVisualTokens.moveInputToTheRight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.gl = window.gl || {};
|
updateCurrentDropdownOffset() {
|
||||||
gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager;
|
this.updateDropdownOffset(this.currentDropdown);
|
||||||
})();
|
}
|
||||||
|
|
||||||
|
updateDropdownOffset(key) {
|
||||||
|
// Always align dropdown with the input field
|
||||||
|
let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
|
||||||
|
|
||||||
|
const maxInputWidth = 240;
|
||||||
|
const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
|
||||||
|
|
||||||
|
// Make sure offset never exceeds the input container
|
||||||
|
const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
|
||||||
|
if (offsetMaxWidth < offset) {
|
||||||
|
offset = offsetMaxWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mapping[key].reference.setOffset(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
load(key, firstLoad = false) {
|
||||||
|
const mappingKey = this.mapping[key];
|
||||||
|
const glClass = mappingKey.gl;
|
||||||
|
const element = mappingKey.element;
|
||||||
|
let forceShowList = false;
|
||||||
|
|
||||||
|
if (!mappingKey.reference) {
|
||||||
|
const dl = this.droplab;
|
||||||
|
const defaultArguments = [null, dl, element, this.filteredSearchInput, key];
|
||||||
|
const glArguments = defaultArguments.concat(mappingKey.extraArguments || []);
|
||||||
|
|
||||||
|
// Passing glArguments to `new gl[glClass](<arguments>)`
|
||||||
|
mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstLoad) {
|
||||||
|
mappingKey.reference.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentDropdown === 'hint') {
|
||||||
|
// Force the dropdown to show if it was clicked from the hint dropdown
|
||||||
|
forceShowList = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateDropdownOffset(key);
|
||||||
|
mappingKey.reference.render(firstLoad, forceShowList);
|
||||||
|
|
||||||
|
this.currentDropdown = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDropdown(dropdownName = '') {
|
||||||
|
let firstLoad = false;
|
||||||
|
|
||||||
|
if (!this.droplab) {
|
||||||
|
firstLoad = true;
|
||||||
|
this.droplab = new DropLab();
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
|
||||||
|
const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
|
||||||
|
&& this.mapping[match.key];
|
||||||
|
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
|
||||||
|
|
||||||
|
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
|
||||||
|
const key = match && match.key ? match.key : 'hint';
|
||||||
|
this.load(key, firstLoad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDropdown() {
|
||||||
|
const query = gl.DropdownUtils.getSearchQuery(true);
|
||||||
|
const { lastToken, searchToken } = this.tokenizer.processTokens(query);
|
||||||
|
|
||||||
|
if (this.currentDropdown) {
|
||||||
|
this.updateCurrentDropdownOffset();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastToken === searchToken && lastToken !== null) {
|
||||||
|
// Token is not fully initialized yet because it has no value
|
||||||
|
// Eg. token = 'label:'
|
||||||
|
|
||||||
|
const split = lastToken.split(':');
|
||||||
|
const dropdownName = split[0].split(' ').last();
|
||||||
|
this.loadDropdown(split.length > 1 ? dropdownName : '');
|
||||||
|
} else if (lastToken) {
|
||||||
|
// Token has been initialized into an object because it has a value
|
||||||
|
this.loadDropdown(lastToken.key);
|
||||||
|
} else {
|
||||||
|
this.loadDropdown('hint');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetDropdowns() {
|
||||||
|
if (!this.currentDropdown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force current dropdown to hide
|
||||||
|
this.mapping[this.currentDropdown].reference.hideDropdown();
|
||||||
|
|
||||||
|
// Re-Load dropdown
|
||||||
|
this.setDropdown();
|
||||||
|
|
||||||
|
// Reset filters for current dropdown
|
||||||
|
this.mapping[this.currentDropdown].reference.resetFilters();
|
||||||
|
|
||||||
|
// Reposition dropdown so that it is aligned with cursor
|
||||||
|
this.updateDropdownOffset(this.currentDropdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyDroplab() {
|
||||||
|
this.droplab.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.gl = window.gl || {};
|
||||||
|
gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager;
|
||||||
|
|
|
@ -6,489 +6,487 @@ import RecentSearchesStore from './stores/recent_searches_store';
|
||||||
import RecentSearchesService from './services/recent_searches_service';
|
import RecentSearchesService from './services/recent_searches_service';
|
||||||
import eventHub from './event_hub';
|
import eventHub from './event_hub';
|
||||||
|
|
||||||
(() => {
|
class FilteredSearchManager {
|
||||||
class FilteredSearchManager {
|
constructor(page) {
|
||||||
constructor(page) {
|
this.container = FilteredSearchContainer.container;
|
||||||
this.container = FilteredSearchContainer.container;
|
this.filteredSearchInput = this.container.querySelector('.filtered-search');
|
||||||
this.filteredSearchInput = this.container.querySelector('.filtered-search');
|
this.filteredSearchInputForm = this.filteredSearchInput.form;
|
||||||
this.filteredSearchInputForm = this.filteredSearchInput.form;
|
this.clearSearchButton = this.container.querySelector('.clear-search');
|
||||||
this.clearSearchButton = this.container.querySelector('.clear-search');
|
this.tokensContainer = this.container.querySelector('.tokens-container');
|
||||||
this.tokensContainer = this.container.querySelector('.tokens-container');
|
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
|
||||||
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
|
|
||||||
|
|
||||||
this.recentSearchesStore = new RecentSearchesStore();
|
this.recentSearchesStore = new RecentSearchesStore();
|
||||||
let recentSearchesKey = 'issue-recent-searches';
|
let recentSearchesKey = 'issue-recent-searches';
|
||||||
if (page === 'merge_requests') {
|
if (page === 'merge_requests') {
|
||||||
recentSearchesKey = 'merge-request-recent-searches';
|
recentSearchesKey = 'merge-request-recent-searches';
|
||||||
}
|
}
|
||||||
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
|
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
|
||||||
|
|
||||||
// Fetch recent searches from localStorage
|
// Fetch recent searches from localStorage
|
||||||
this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
|
this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// eslint-disable-next-line no-new
|
// eslint-disable-next-line no-new
|
||||||
new Flash('An error occured while parsing recent searches');
|
new Flash('An error occured while parsing recent searches');
|
||||||
// Gracefully fail to empty array
|
// Gracefully fail to empty array
|
||||||
return [];
|
return [];
|
||||||
})
|
})
|
||||||
.then((searches) => {
|
.then((searches) => {
|
||||||
// Put any searches that may have come in before
|
// Put any searches that may have come in before
|
||||||
// we fetched the saved searches ahead of the already saved ones
|
// we fetched the saved searches ahead of the already saved ones
|
||||||
const resultantSearches = this.recentSearchesStore.setRecentSearches(
|
const resultantSearches = this.recentSearchesStore.setRecentSearches(
|
||||||
this.recentSearchesStore.state.recentSearches.concat(searches),
|
this.recentSearchesStore.state.recentSearches.concat(searches),
|
||||||
);
|
|
||||||
this.recentSearchesService.save(resultantSearches);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.filteredSearchInput) {
|
|
||||||
this.tokenizer = gl.FilteredSearchTokenizer;
|
|
||||||
this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page);
|
|
||||||
|
|
||||||
this.recentSearchesRoot = new RecentSearchesRoot(
|
|
||||||
this.recentSearchesStore,
|
|
||||||
this.recentSearchesService,
|
|
||||||
document.querySelector('.js-filtered-search-history-dropdown'),
|
|
||||||
);
|
);
|
||||||
this.recentSearchesRoot.init();
|
this.recentSearchesService.save(resultantSearches);
|
||||||
|
});
|
||||||
|
|
||||||
this.bindEvents();
|
if (this.filteredSearchInput) {
|
||||||
this.loadSearchParamsFromURL();
|
this.tokenizer = gl.FilteredSearchTokenizer;
|
||||||
this.dropdownManager.setDropdown();
|
this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page);
|
||||||
|
|
||||||
this.cleanupWrapper = this.cleanup.bind(this);
|
this.recentSearchesRoot = new RecentSearchesRoot(
|
||||||
document.addEventListener('beforeunload', this.cleanupWrapper);
|
this.recentSearchesStore,
|
||||||
}
|
this.recentSearchesService,
|
||||||
|
document.querySelector('.js-filtered-search-history-dropdown'),
|
||||||
|
);
|
||||||
|
this.recentSearchesRoot.init();
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
this.loadSearchParamsFromURL();
|
||||||
|
this.dropdownManager.setDropdown();
|
||||||
|
|
||||||
|
this.cleanupWrapper = this.cleanup.bind(this);
|
||||||
|
document.addEventListener('beforeunload', this.cleanupWrapper);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
this.unbindEvents();
|
this.unbindEvents();
|
||||||
document.removeEventListener('beforeunload', this.cleanupWrapper);
|
document.removeEventListener('beforeunload', this.cleanupWrapper);
|
||||||
|
|
||||||
if (this.recentSearchesRoot) {
|
if (this.recentSearchesRoot) {
|
||||||
this.recentSearchesRoot.destroy();
|
this.recentSearchesRoot.destroy();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
this.handleFormSubmit = this.handleFormSubmit.bind(this);
|
this.handleFormSubmit = this.handleFormSubmit.bind(this);
|
||||||
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
|
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
|
||||||
this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
|
this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
|
||||||
this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this);
|
this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this);
|
||||||
this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
|
this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
|
||||||
this.checkForEnterWrapper = this.checkForEnter.bind(this);
|
this.checkForEnterWrapper = this.checkForEnter.bind(this);
|
||||||
this.onClearSearchWrapper = this.onClearSearch.bind(this);
|
this.onClearSearchWrapper = this.onClearSearch.bind(this);
|
||||||
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
|
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
|
||||||
this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
|
this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
|
||||||
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
|
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
|
||||||
this.editTokenWrapper = this.editToken.bind(this);
|
this.editTokenWrapper = this.editToken.bind(this);
|
||||||
this.tokenChange = this.tokenChange.bind(this);
|
this.tokenChange = this.tokenChange.bind(this);
|
||||||
this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
|
this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
|
||||||
this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
|
this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
|
||||||
this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
|
this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
|
||||||
|
|
||||||
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
|
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
|
||||||
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
|
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
|
||||||
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
|
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
|
||||||
this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper);
|
this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper);
|
||||||
this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper);
|
this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper);
|
||||||
this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
|
this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
|
||||||
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
|
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
|
||||||
this.filteredSearchInput.addEventListener('click', this.tokenChange);
|
this.filteredSearchInput.addEventListener('click', this.tokenChange);
|
||||||
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
|
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
|
||||||
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
|
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
|
||||||
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
|
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
|
||||||
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
|
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
|
||||||
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
|
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
|
||||||
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
|
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
|
||||||
document.addEventListener('click', this.unselectEditTokensWrapper);
|
document.addEventListener('click', this.unselectEditTokensWrapper);
|
||||||
document.addEventListener('click', this.removeInputContainerFocusWrapper);
|
document.addEventListener('click', this.removeInputContainerFocusWrapper);
|
||||||
document.addEventListener('keydown', this.removeSelectedTokenWrapper);
|
document.addEventListener('keydown', this.removeSelectedTokenWrapper);
|
||||||
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
|
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
unbindEvents() {
|
unbindEvents() {
|
||||||
this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit);
|
this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit);
|
||||||
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
|
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
|
||||||
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
|
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
|
||||||
this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
|
this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
|
||||||
this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper);
|
this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper);
|
||||||
this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
|
this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
|
||||||
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
|
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
|
||||||
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
|
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
|
||||||
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
|
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
|
||||||
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
|
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
|
||||||
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
|
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
|
||||||
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
|
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
|
||||||
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
|
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
|
||||||
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
|
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
|
||||||
document.removeEventListener('click', this.unselectEditTokensWrapper);
|
document.removeEventListener('click', this.unselectEditTokensWrapper);
|
||||||
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
|
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
|
||||||
document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
|
document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
|
||||||
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
|
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkForBackspace(e) {
|
checkForBackspace(e) {
|
||||||
// 8 = Backspace Key
|
// 8 = Backspace Key
|
||||||
// 46 = Delete Key
|
// 46 = Delete Key
|
||||||
if (e.keyCode === 8 || e.keyCode === 46) {
|
if (e.keyCode === 8 || e.keyCode === 46) {
|
||||||
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
||||||
|
|
||||||
if (this.filteredSearchInput.value === '' && lastVisualToken) {
|
if (this.filteredSearchInput.value === '' && lastVisualToken) {
|
||||||
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
|
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
|
||||||
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
|
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
|
||||||
}
|
|
||||||
|
|
||||||
// Reposition dropdown so that it is aligned with cursor
|
|
||||||
this.dropdownManager.updateCurrentDropdownOffset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkForEnter(e) {
|
|
||||||
if (e.keyCode === 38 || e.keyCode === 40) {
|
|
||||||
const selectionStart = this.filteredSearchInput.selectionStart;
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.keyCode === 13) {
|
// Reposition dropdown so that it is aligned with cursor
|
||||||
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
|
this.dropdownManager.updateCurrentDropdownOffset();
|
||||||
const dropdownEl = dropdown.element;
|
|
||||||
const activeElements = dropdownEl.querySelectorAll('.droplab-item-active');
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!activeElements.length) {
|
|
||||||
if (this.isHandledAsync) {
|
|
||||||
e.stopImmediatePropagation();
|
|
||||||
|
|
||||||
this.filteredSearchInput.blur();
|
|
||||||
this.dropdownManager.resetDropdowns();
|
|
||||||
} else {
|
|
||||||
// Prevent droplab from opening dropdown
|
|
||||||
this.dropdownManager.destroyDroplab();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.search();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addInputContainerFocus() {
|
checkForEnter(e) {
|
||||||
const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
|
if (e.keyCode === 38 || e.keyCode === 40) {
|
||||||
|
const selectionStart = this.filteredSearchInput.selectionStart;
|
||||||
|
|
||||||
if (inputContainer) {
|
|
||||||
inputContainer.classList.add('focus');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeInputContainerFocus(e) {
|
|
||||||
const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
|
|
||||||
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
|
|
||||||
const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
|
|
||||||
const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
|
|
||||||
|
|
||||||
if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
|
|
||||||
!isElementInStaticFilterDropdown && inputContainer) {
|
|
||||||
inputContainer.classList.remove('focus');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static selectToken(e) {
|
|
||||||
const button = e.target.closest('.selectable');
|
|
||||||
|
|
||||||
if (button) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
gl.FilteredSearchVisualTokens.selectToken(button);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unselectEditTokens(e) {
|
|
||||||
const inputContainer = this.container.querySelector('.filtered-search-box');
|
|
||||||
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
|
|
||||||
const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
|
|
||||||
const isElementTokensContainer = e.target.classList.contains('tokens-container');
|
|
||||||
|
|
||||||
if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
|
|
||||||
gl.FilteredSearchVisualTokens.moveInputToTheRight();
|
|
||||||
this.dropdownManager.resetDropdowns();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
editToken(e) {
|
|
||||||
const token = e.target.closest('.js-visual-token');
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
gl.FilteredSearchVisualTokens.editToken(token);
|
|
||||||
this.tokenChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleClearSearchButton() {
|
|
||||||
const query = gl.DropdownUtils.getSearchQuery();
|
|
||||||
const hidden = 'hidden';
|
|
||||||
const hasHidden = this.clearSearchButton.classList.contains(hidden);
|
|
||||||
|
|
||||||
if (query.length === 0 && !hasHidden) {
|
|
||||||
this.clearSearchButton.classList.add(hidden);
|
|
||||||
} else if (query.length && hasHidden) {
|
|
||||||
this.clearSearchButton.classList.remove(hidden);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleInputPlaceholder() {
|
|
||||||
const query = gl.DropdownUtils.getSearchQuery();
|
|
||||||
const placeholder = 'Search or filter results...';
|
|
||||||
const currentPlaceholder = this.filteredSearchInput.placeholder;
|
|
||||||
|
|
||||||
if (query.length === 0 && currentPlaceholder !== placeholder) {
|
|
||||||
this.filteredSearchInput.placeholder = placeholder;
|
|
||||||
} else if (query.length > 0 && currentPlaceholder !== '') {
|
|
||||||
this.filteredSearchInput.placeholder = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeSelectedToken(e) {
|
|
||||||
// 8 = Backspace Key
|
|
||||||
// 46 = Delete Key
|
|
||||||
if (e.keyCode === 8 || e.keyCode === 46) {
|
|
||||||
gl.FilteredSearchVisualTokens.removeSelectedToken();
|
|
||||||
this.handleInputPlaceholder();
|
|
||||||
this.toggleClearSearchButton();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClearSearch(e) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.clearSearch();
|
this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSearch() {
|
if (e.keyCode === 13) {
|
||||||
this.filteredSearchInput.value = '';
|
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
|
||||||
|
const dropdownEl = dropdown.element;
|
||||||
|
const activeElements = dropdownEl.querySelectorAll('.droplab-item-active');
|
||||||
|
|
||||||
const removeElements = [];
|
e.preventDefault();
|
||||||
|
|
||||||
[].forEach.call(this.tokensContainer.children, (t) => {
|
if (!activeElements.length) {
|
||||||
if (t.classList.contains('js-visual-token')) {
|
if (this.isHandledAsync) {
|
||||||
removeElements.push(t);
|
e.stopImmediatePropagation();
|
||||||
|
|
||||||
|
this.filteredSearchInput.blur();
|
||||||
|
this.dropdownManager.resetDropdowns();
|
||||||
|
} else {
|
||||||
|
// Prevent droplab from opening dropdown
|
||||||
|
this.dropdownManager.destroyDroplab();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
removeElements.forEach((el) => {
|
|
||||||
el.parentElement.removeChild(el);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.clearSearchButton.classList.add('hidden');
|
|
||||||
this.handleInputPlaceholder();
|
|
||||||
|
|
||||||
this.dropdownManager.resetDropdowns();
|
|
||||||
|
|
||||||
if (this.isHandledAsync) {
|
|
||||||
this.search();
|
this.search();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleInputVisualToken() {
|
addInputContainerFocus() {
|
||||||
const input = this.filteredSearchInput;
|
const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
|
||||||
const { tokens, searchToken }
|
|
||||||
= gl.FilteredSearchTokenizer.processTokens(input.value);
|
|
||||||
const { isLastVisualTokenValid }
|
|
||||||
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
|
||||||
|
|
||||||
if (isLastVisualTokenValid) {
|
if (inputContainer) {
|
||||||
tokens.forEach((t) => {
|
inputContainer.classList.add('focus');
|
||||||
input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
|
|
||||||
gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const fragments = searchToken.split(':');
|
|
||||||
if (fragments.length > 1) {
|
|
||||||
const inputValues = fragments[0].split(' ');
|
|
||||||
const tokenKey = inputValues.last();
|
|
||||||
|
|
||||||
if (inputValues.length > 1) {
|
|
||||||
inputValues.pop();
|
|
||||||
const searchTerms = inputValues.join(' ');
|
|
||||||
|
|
||||||
input.value = input.value.replace(searchTerms, '');
|
|
||||||
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
|
|
||||||
}
|
|
||||||
|
|
||||||
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey);
|
|
||||||
input.value = input.value.replace(`${tokenKey}:`, '');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Keep listening to token until we determine that the user is done typing the token value
|
|
||||||
const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g;
|
|
||||||
|
|
||||||
if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
|
|
||||||
gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
|
|
||||||
|
|
||||||
// Trim the last space as seen in the if statement above
|
|
||||||
input.value = input.value.replace(searchToken, '').trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleFormSubmit(e) {
|
removeInputContainerFocus(e) {
|
||||||
|
const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
|
||||||
|
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
|
||||||
|
const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
|
||||||
|
const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
|
||||||
|
|
||||||
|
if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
|
||||||
|
!isElementInStaticFilterDropdown && inputContainer) {
|
||||||
|
inputContainer.classList.remove('focus');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static selectToken(e) {
|
||||||
|
const button = e.target.closest('.selectable');
|
||||||
|
|
||||||
|
if (button) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.search();
|
e.stopPropagation();
|
||||||
|
gl.FilteredSearchVisualTokens.selectToken(button);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveCurrentSearchQuery() {
|
unselectEditTokens(e) {
|
||||||
// Don't save before we have fetched the already saved searches
|
const inputContainer = this.container.querySelector('.filtered-search-box');
|
||||||
this.fetchingRecentSearchesPromise.then(() => {
|
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
|
||||||
const searchQuery = gl.DropdownUtils.getSearchQuery();
|
const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
|
||||||
if (searchQuery.length > 0) {
|
const isElementTokensContainer = e.target.classList.contains('tokens-container');
|
||||||
const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
|
|
||||||
this.recentSearchesService.save(resultantSearches);
|
if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
|
||||||
}
|
gl.FilteredSearchVisualTokens.moveInputToTheRight();
|
||||||
});
|
this.dropdownManager.resetDropdowns();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loadSearchParamsFromURL() {
|
editToken(e) {
|
||||||
const params = gl.utils.getUrlParamsArray();
|
const token = e.target.closest('.js-visual-token');
|
||||||
const usernameParams = this.getUsernameParams();
|
|
||||||
let hasFilteredSearch = false;
|
|
||||||
|
|
||||||
params.forEach((p) => {
|
if (token) {
|
||||||
const split = p.split('=');
|
gl.FilteredSearchVisualTokens.editToken(token);
|
||||||
const keyParam = decodeURIComponent(split[0]);
|
this.tokenChange();
|
||||||
const value = split[1];
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if it matches edge conditions listed in this.filteredSearchTokenKeys
|
toggleClearSearchButton() {
|
||||||
const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
|
const query = gl.DropdownUtils.getSearchQuery();
|
||||||
|
const hidden = 'hidden';
|
||||||
|
const hasHidden = this.clearSearchButton.classList.contains(hidden);
|
||||||
|
|
||||||
if (condition) {
|
if (query.length === 0 && !hasHidden) {
|
||||||
hasFilteredSearch = true;
|
this.clearSearchButton.classList.add(hidden);
|
||||||
gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value);
|
} else if (query.length && hasHidden) {
|
||||||
} else {
|
this.clearSearchButton.classList.remove(hidden);
|
||||||
// Sanitize value since URL converts spaces into +
|
}
|
||||||
// Replace before decode so that we know what was originally + versus the encoded +
|
}
|
||||||
const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
|
|
||||||
const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
|
|
||||||
|
|
||||||
if (match) {
|
handleInputPlaceholder() {
|
||||||
const indexOf = keyParam.indexOf('_');
|
const query = gl.DropdownUtils.getSearchQuery();
|
||||||
const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
|
const placeholder = 'Search or filter results...';
|
||||||
const symbol = match.symbol;
|
const currentPlaceholder = this.filteredSearchInput.placeholder;
|
||||||
let quotationsToUse = '';
|
|
||||||
|
|
||||||
if (sanitizedValue.indexOf(' ') !== -1) {
|
if (query.length === 0 && currentPlaceholder !== placeholder) {
|
||||||
// Prefer ", but use ' if required
|
this.filteredSearchInput.placeholder = placeholder;
|
||||||
quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
|
} else if (query.length > 0 && currentPlaceholder !== '') {
|
||||||
}
|
this.filteredSearchInput.placeholder = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
hasFilteredSearch = true;
|
removeSelectedToken(e) {
|
||||||
gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
|
// 8 = Backspace Key
|
||||||
} else if (!match && keyParam === 'assignee_id') {
|
// 46 = Delete Key
|
||||||
const id = parseInt(value, 10);
|
if (e.keyCode === 8 || e.keyCode === 46) {
|
||||||
if (usernameParams[id]) {
|
gl.FilteredSearchVisualTokens.removeSelectedToken();
|
||||||
hasFilteredSearch = true;
|
this.handleInputPlaceholder();
|
||||||
gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
|
this.toggleClearSearchButton();
|
||||||
}
|
}
|
||||||
} else if (!match && keyParam === 'author_id') {
|
}
|
||||||
const id = parseInt(value, 10);
|
|
||||||
if (usernameParams[id]) {
|
|
||||||
hasFilteredSearch = true;
|
|
||||||
gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
|
|
||||||
}
|
|
||||||
} else if (!match && keyParam === 'search') {
|
|
||||||
hasFilteredSearch = true;
|
|
||||||
this.filteredSearchInput.value = sanitizedValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.saveCurrentSearchQuery();
|
onClearSearch(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.clearSearch();
|
||||||
|
}
|
||||||
|
|
||||||
if (hasFilteredSearch) {
|
clearSearch() {
|
||||||
this.clearSearchButton.classList.remove('hidden');
|
this.filteredSearchInput.value = '';
|
||||||
this.handleInputPlaceholder();
|
|
||||||
|
const removeElements = [];
|
||||||
|
|
||||||
|
[].forEach.call(this.tokensContainer.children, (t) => {
|
||||||
|
if (t.classList.contains('js-visual-token')) {
|
||||||
|
removeElements.push(t);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
search() {
|
removeElements.forEach((el) => {
|
||||||
const paths = [];
|
el.parentElement.removeChild(el);
|
||||||
const searchQuery = gl.DropdownUtils.getSearchQuery();
|
});
|
||||||
|
|
||||||
this.saveCurrentSearchQuery();
|
this.clearSearchButton.classList.add('hidden');
|
||||||
|
this.handleInputPlaceholder();
|
||||||
|
|
||||||
const { tokens, searchToken }
|
this.dropdownManager.resetDropdowns();
|
||||||
= this.tokenizer.processTokens(searchQuery);
|
|
||||||
const currentState = gl.utils.getParameterByName('state') || 'opened';
|
|
||||||
paths.push(`state=${currentState}`);
|
|
||||||
|
|
||||||
tokens.forEach((token) => {
|
if (this.isHandledAsync) {
|
||||||
const condition = this.filteredSearchTokenKeys
|
|
||||||
.searchByConditionKeyValue(token.key, token.value.toLowerCase());
|
|
||||||
const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
|
|
||||||
const keyParam = param ? `${token.key}_${param}` : token.key;
|
|
||||||
let tokenPath = '';
|
|
||||||
|
|
||||||
if (condition) {
|
|
||||||
tokenPath = condition.url;
|
|
||||||
} else {
|
|
||||||
let tokenValue = token.value;
|
|
||||||
|
|
||||||
if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
|
|
||||||
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
|
|
||||||
tokenValue = tokenValue.slice(1, tokenValue.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
paths.push(tokenPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (searchToken) {
|
|
||||||
const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
|
|
||||||
paths.push(`search=${sanitized}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`;
|
|
||||||
|
|
||||||
if (this.updateObject) {
|
|
||||||
this.updateObject(parameterizedUrl);
|
|
||||||
} else {
|
|
||||||
gl.utils.visitUrl(parameterizedUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getUsernameParams() {
|
|
||||||
const usernamesById = {};
|
|
||||||
try {
|
|
||||||
const attribute = this.filteredSearchInput.getAttribute('data-username-params');
|
|
||||||
JSON.parse(attribute).forEach((user) => {
|
|
||||||
usernamesById[user.id] = user.username;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
return usernamesById;
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenChange() {
|
|
||||||
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
|
|
||||||
|
|
||||||
if (dropdown) {
|
|
||||||
const currentDropdownRef = dropdown.reference;
|
|
||||||
|
|
||||||
this.setDropdownWrapper();
|
|
||||||
currentDropdownRef.dispatchInputEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onrecentSearchesItemSelected(text) {
|
|
||||||
this.clearSearch();
|
|
||||||
this.filteredSearchInput.value = text;
|
|
||||||
this.filteredSearchInput.dispatchEvent(new CustomEvent('input'));
|
|
||||||
this.search();
|
this.search();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.gl = window.gl || {};
|
handleInputVisualToken() {
|
||||||
gl.FilteredSearchManager = FilteredSearchManager;
|
const input = this.filteredSearchInput;
|
||||||
})();
|
const { tokens, searchToken }
|
||||||
|
= gl.FilteredSearchTokenizer.processTokens(input.value);
|
||||||
|
const { isLastVisualTokenValid }
|
||||||
|
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
||||||
|
|
||||||
|
if (isLastVisualTokenValid) {
|
||||||
|
tokens.forEach((t) => {
|
||||||
|
input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
|
||||||
|
gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fragments = searchToken.split(':');
|
||||||
|
if (fragments.length > 1) {
|
||||||
|
const inputValues = fragments[0].split(' ');
|
||||||
|
const tokenKey = inputValues.last();
|
||||||
|
|
||||||
|
if (inputValues.length > 1) {
|
||||||
|
inputValues.pop();
|
||||||
|
const searchTerms = inputValues.join(' ');
|
||||||
|
|
||||||
|
input.value = input.value.replace(searchTerms, '');
|
||||||
|
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey);
|
||||||
|
input.value = input.value.replace(`${tokenKey}:`, '');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Keep listening to token until we determine that the user is done typing the token value
|
||||||
|
const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g;
|
||||||
|
|
||||||
|
if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
|
||||||
|
gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
|
||||||
|
|
||||||
|
// Trim the last space as seen in the if statement above
|
||||||
|
input.value = input.value.replace(searchToken, '').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFormSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.search();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCurrentSearchQuery() {
|
||||||
|
// Don't save before we have fetched the already saved searches
|
||||||
|
this.fetchingRecentSearchesPromise.then(() => {
|
||||||
|
const searchQuery = gl.DropdownUtils.getSearchQuery();
|
||||||
|
if (searchQuery.length > 0) {
|
||||||
|
const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
|
||||||
|
this.recentSearchesService.save(resultantSearches);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSearchParamsFromURL() {
|
||||||
|
const params = gl.utils.getUrlParamsArray();
|
||||||
|
const usernameParams = this.getUsernameParams();
|
||||||
|
let hasFilteredSearch = false;
|
||||||
|
|
||||||
|
params.forEach((p) => {
|
||||||
|
const split = p.split('=');
|
||||||
|
const keyParam = decodeURIComponent(split[0]);
|
||||||
|
const value = split[1];
|
||||||
|
|
||||||
|
// Check if it matches edge conditions listed in this.filteredSearchTokenKeys
|
||||||
|
const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
|
||||||
|
|
||||||
|
if (condition) {
|
||||||
|
hasFilteredSearch = true;
|
||||||
|
gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value);
|
||||||
|
} else {
|
||||||
|
// Sanitize value since URL converts spaces into +
|
||||||
|
// Replace before decode so that we know what was originally + versus the encoded +
|
||||||
|
const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
|
||||||
|
const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const indexOf = keyParam.indexOf('_');
|
||||||
|
const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
|
||||||
|
const symbol = match.symbol;
|
||||||
|
let quotationsToUse = '';
|
||||||
|
|
||||||
|
if (sanitizedValue.indexOf(' ') !== -1) {
|
||||||
|
// Prefer ", but use ' if required
|
||||||
|
quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFilteredSearch = true;
|
||||||
|
gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
|
||||||
|
} else if (!match && keyParam === 'assignee_id') {
|
||||||
|
const id = parseInt(value, 10);
|
||||||
|
if (usernameParams[id]) {
|
||||||
|
hasFilteredSearch = true;
|
||||||
|
gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
|
||||||
|
}
|
||||||
|
} else if (!match && keyParam === 'author_id') {
|
||||||
|
const id = parseInt(value, 10);
|
||||||
|
if (usernameParams[id]) {
|
||||||
|
hasFilteredSearch = true;
|
||||||
|
gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
|
||||||
|
}
|
||||||
|
} else if (!match && keyParam === 'search') {
|
||||||
|
hasFilteredSearch = true;
|
||||||
|
this.filteredSearchInput.value = sanitizedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.saveCurrentSearchQuery();
|
||||||
|
|
||||||
|
if (hasFilteredSearch) {
|
||||||
|
this.clearSearchButton.classList.remove('hidden');
|
||||||
|
this.handleInputPlaceholder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
search() {
|
||||||
|
const paths = [];
|
||||||
|
const searchQuery = gl.DropdownUtils.getSearchQuery();
|
||||||
|
|
||||||
|
this.saveCurrentSearchQuery();
|
||||||
|
|
||||||
|
const { tokens, searchToken }
|
||||||
|
= this.tokenizer.processTokens(searchQuery);
|
||||||
|
const currentState = gl.utils.getParameterByName('state') || 'opened';
|
||||||
|
paths.push(`state=${currentState}`);
|
||||||
|
|
||||||
|
tokens.forEach((token) => {
|
||||||
|
const condition = this.filteredSearchTokenKeys
|
||||||
|
.searchByConditionKeyValue(token.key, token.value.toLowerCase());
|
||||||
|
const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
|
||||||
|
const keyParam = param ? `${token.key}_${param}` : token.key;
|
||||||
|
let tokenPath = '';
|
||||||
|
|
||||||
|
if (condition) {
|
||||||
|
tokenPath = condition.url;
|
||||||
|
} else {
|
||||||
|
let tokenValue = token.value;
|
||||||
|
|
||||||
|
if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
|
||||||
|
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
|
||||||
|
tokenValue = tokenValue.slice(1, tokenValue.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
paths.push(tokenPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (searchToken) {
|
||||||
|
const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
|
||||||
|
paths.push(`search=${sanitized}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`;
|
||||||
|
|
||||||
|
if (this.updateObject) {
|
||||||
|
this.updateObject(parameterizedUrl);
|
||||||
|
} else {
|
||||||
|
gl.utils.visitUrl(parameterizedUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsernameParams() {
|
||||||
|
const usernamesById = {};
|
||||||
|
try {
|
||||||
|
const attribute = this.filteredSearchInput.getAttribute('data-username-params');
|
||||||
|
JSON.parse(attribute).forEach((user) => {
|
||||||
|
usernamesById[user.id] = user.username;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
return usernamesById;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenChange() {
|
||||||
|
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
|
||||||
|
|
||||||
|
if (dropdown) {
|
||||||
|
const currentDropdownRef = dropdown.reference;
|
||||||
|
|
||||||
|
this.setDropdownWrapper();
|
||||||
|
currentDropdownRef.dispatchInputEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onrecentSearchesItemSelected(text) {
|
||||||
|
this.clearSearch();
|
||||||
|
this.filteredSearchInput.value = text;
|
||||||
|
this.filteredSearchInput.dispatchEvent(new CustomEvent('input'));
|
||||||
|
this.search();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.gl = window.gl || {};
|
||||||
|
gl.FilteredSearchManager = FilteredSearchManager;
|
||||||
|
|
|
@ -1,100 +1,98 @@
|
||||||
(() => {
|
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() {
|
|
||||||
return alternativeTokenKeys;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getConditions() {
|
|
||||||
return conditions;
|
|
||||||
}
|
|
||||||
|
|
||||||
static searchByKey(key) {
|
|
||||||
return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static searchBySymbol(symbol) {
|
|
||||||
return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static searchByKeyParam(keyParam) {
|
|
||||||
return tokenKeysWithAlternative.find((tokenKey) => {
|
|
||||||
let tokenKeyParam = tokenKey.key;
|
|
||||||
|
|
||||||
if (tokenKey.param) {
|
|
||||||
tokenKeyParam += `_${tokenKey.param}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return keyParam === tokenKeyParam;
|
|
||||||
}) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static searchByConditionUrl(url) {
|
|
||||||
return conditions.find(condition => condition.url === url) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static searchByConditionKeyValue(key, value) {
|
|
||||||
return conditions
|
|
||||||
.find(condition => condition.tokenKey === key && condition.value === value) || null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.gl = window.gl || {};
|
static getAlternatives() {
|
||||||
gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
|
return alternativeTokenKeys;
|
||||||
})();
|
}
|
||||||
|
|
||||||
|
static getConditions() {
|
||||||
|
return conditions;
|
||||||
|
}
|
||||||
|
|
||||||
|
static searchByKey(key) {
|
||||||
|
return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static searchBySymbol(symbol) {
|
||||||
|
return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static searchByKeyParam(keyParam) {
|
||||||
|
return tokenKeysWithAlternative.find((tokenKey) => {
|
||||||
|
let tokenKeyParam = tokenKey.key;
|
||||||
|
|
||||||
|
if (tokenKey.param) {
|
||||||
|
tokenKeyParam += `_${tokenKey.param}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyParam === tokenKeyParam;
|
||||||
|
}) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static searchByConditionUrl(url) {
|
||||||
|
return conditions.find(condition => condition.url === url) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static searchByConditionKeyValue(key, value) {
|
||||||
|
return conditions
|
||||||
|
.find(condition => condition.tokenKey === key && condition.value === value) || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.gl = window.gl || {};
|
||||||
|
gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
|
||||||
|
|
|
@ -1,58 +1,56 @@
|
||||||
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,
|
|
||||||
searchToken,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.gl = window.gl || {};
|
// Prevent adding duplicates
|
||||||
gl.FilteredSearchTokenizer = FilteredSearchTokenizer;
|
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 || {};
|
||||||
|
gl.FilteredSearchTokenizer = FilteredSearchTokenizer;
|
||||||
|
|
|
@ -1,26 +1,20 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import IssueTitle from './issue_title';
|
import IssueTitle from './issue_title.vue';
|
||||||
import '../vue_shared/vue_resource_interceptor';
|
import '../vue_shared/vue_resource_interceptor';
|
||||||
|
|
||||||
const vueOptions = () => ({
|
(() => {
|
||||||
el: '.issue-title-entrypoint',
|
const issueTitleData = document.querySelector('.issue-title-data').dataset;
|
||||||
components: {
|
const { initialTitle, endpoint } = issueTitleData;
|
||||||
IssueTitle,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
const issueTitleData = document.querySelector('.issue-title-data').dataset;
|
|
||||||
|
|
||||||
return {
|
const vm = new Vue({
|
||||||
initialTitle: issueTitleData.initialTitle,
|
el: '.issue-title-entrypoint',
|
||||||
endpoint: issueTitleData.endpoint,
|
render: createElement => createElement(IssueTitle, {
|
||||||
};
|
props: {
|
||||||
},
|
initialTitle,
|
||||||
template: `
|
endpoint,
|
||||||
<IssueTitle
|
},
|
||||||
:initialTitle="initialTitle"
|
}),
|
||||||
:endpoint="endpoint"
|
});
|
||||||
/>
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
|
|
||||||
(() => new Vue(vueOptions()))();
|
return vm;
|
||||||
|
})();
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
<script>
|
||||||
import Visibility from 'visibilityjs';
|
import Visibility from 'visibilityjs';
|
||||||
import Poll from './../lib/utils/poll';
|
import Poll from './../lib/utils/poll';
|
||||||
import Service from './services/index';
|
import Service from './services/index';
|
||||||
|
@ -72,7 +73,9 @@ export default {
|
||||||
created() {
|
created() {
|
||||||
this.fetch();
|
this.fetch();
|
||||||
},
|
},
|
||||||
template: `
|
|
||||||
<h2 class='title' v-html='title'></h2>
|
|
||||||
`,
|
|
||||||
};
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h2 class="title" v-html="title"></h2>
|
||||||
|
</template>
|
|
@ -47,6 +47,10 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) {
|
||||||
|
return $tooltipEl.attr('title', newTitle).tooltip('fixTitle');
|
||||||
|
};
|
||||||
|
|
||||||
w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) {
|
w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) {
|
||||||
event_name = event_name || 'input';
|
event_name = event_name || 'input';
|
||||||
var closest_submit, field, that;
|
var closest_submit, field, that;
|
||||||
|
@ -364,9 +368,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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -381,8 +385,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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
export const BYTES_IN_KIB = 1024;
|
|
@ -1,4 +1,4 @@
|
||||||
/* 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) {
|
||||||
}
|
}
|
||||||
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';
|
||||||
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 () {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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')) {
|
||||||
|
|
|
@ -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');
|
||||||
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');
|
||||||
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');
|
||||||
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 {
|
||||||
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 {
|
||||||
: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>
|
|
@ -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 {
|
||||||
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
|
|
@ -1,32 +1,11 @@
|
||||||
/* 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 {
|
||||||
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 {
|
||||||
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">
|
|
@ -0,0 +1,16 @@
|
||||||
|
/* 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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
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;
|
|
@ -18,7 +18,7 @@ export default class UserCallout {
|
||||||
dismissCallout(e) {
|
dismissCallout(e) {
|
||||||
const $currentTarget = $(e.currentTarget);
|
const $currentTarget = $(e.currentTarget);
|
||||||
|
|
||||||
Cookies.set(USER_CALLOUT_COOKIE, 'true');
|
Cookies.set(USER_CALLOUT_COOKIE, 'true', { expires: 365 });
|
||||||
|
|
||||||
if ($currentTarget.hasClass('close')) {
|
if ($currentTarget.hasClass('close')) {
|
||||||
this.userCalloutBody.remove();
|
this.userCalloutBody.remove();
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
/* 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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -145,3 +145,17 @@ a {
|
||||||
.dropdown-menu-nav a {
|
.dropdown-menu-nav a {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn $fade-in-duration 1;
|
||||||
|
}
|
||||||
|
|
|
@ -38,6 +38,15 @@
|
||||||
height: 300px;
|
height: 300px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-search {
|
.emoji-search {
|
||||||
|
@ -154,6 +163,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.user-authored {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.65;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active {
|
||||||
|
background-color: $white-light;
|
||||||
|
border-color: $border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.btn {
|
&.btn {
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
|
|
|
@ -40,6 +40,10 @@
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
|
@ -564,3 +564,7 @@
|
||||||
color: $gl-text-color-secondary;
|
color: $gl-text-color-secondary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.droplab-item-ignore {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
|
@ -155,7 +155,7 @@ header {
|
||||||
|
|
||||||
.header-logo {
|
.header-logo {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 7px 0 2px;
|
margin: 0 12px 0 2px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
transition-duration: .3s;
|
transition-duration: .3s;
|
||||||
|
@ -186,7 +186,7 @@ header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
padding-top: (($header-height - 19) / 2);
|
padding-top: 14px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,6 +331,14 @@ header {
|
||||||
.dropdown-menu-nav {
|
.dropdown-menu-nav {
|
||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
margin-top: -5px;
|
margin-top: -5px;
|
||||||
|
|
||||||
|
.current-user {
|
||||||
|
padding: 5px 18px;
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -457,6 +457,11 @@ $label-inverse-bg: #333;
|
||||||
$label-remove-border: rgba(0, 0, 0, .1);
|
$label-remove-border: rgba(0, 0, 0, .1);
|
||||||
$label-border-radius: 100px;
|
$label-border-radius: 100px;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Animation
|
||||||
|
*/
|
||||||
|
$fade-in-duration: 200ms;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Lint
|
* Lint
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -61,8 +61,9 @@
|
||||||
.truncated-info {
|
.truncated-info {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-bottom: 1px solid;
|
border-bottom: 1px solid;
|
||||||
background-color: $black-transparent;
|
background-color: $black;
|
||||||
height: 45px;
|
height: 45px;
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
&.affix {
|
&.affix {
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -87,6 +88,16 @@
|
||||||
right: 5px;
|
right: 5px;
|
||||||
left: 5px;
|
left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.truncated-info-size {
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raw-link {
|
||||||
|
color: inherit;
|
||||||
|
margin-left: 5px;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -135,7 +135,7 @@
|
||||||
|
|
||||||
.text-expander {
|
.text-expander {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: $gray-light;
|
background: $white-light;
|
||||||
color: $gl-text-color-secondary;
|
color: $gl-text-color-secondary;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -146,6 +146,11 @@
|
||||||
line-height: $gl-font-size;
|
line-height: $gl-font-size;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
background: $gray-light;
|
||||||
|
box-shadow: inset 0 0 2px rgba($black, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: darken($gray-light, 10%);
|
background-color: darken($gray-light, 10%);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
@ -106,6 +106,10 @@
|
||||||
span {
|
span {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -210,10 +210,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bold {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light {
|
.light {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,6 +123,9 @@ ul.notes {
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-emoji-button {
|
.note-emoji-button {
|
||||||
|
position: relative;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
.fa-spinner {
|
.fa-spinner {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -352,6 +355,15 @@ ul.notes {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-header-info {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.note-headline-light {
|
.note-headline-light {
|
||||||
display: inline;
|
display: inline;
|
||||||
|
|
||||||
|
@ -371,21 +383,27 @@ ul.notes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-headline-meta {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Actions for Discussions/Notes
|
* Actions for Discussions/Notes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.discussion-actions,
|
.discussion-actions {
|
||||||
.note-actions {
|
|
||||||
float: right;
|
float: right;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
color: $gray-darkest;
|
color: $gray-darkest;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-actions {
|
.note-actions {
|
||||||
position: absolute;
|
flex-shrink: 0;
|
||||||
right: 0;
|
// For PhantomJS that does not support flex
|
||||||
top: 0;
|
float: right;
|
||||||
|
margin-left: 10px;
|
||||||
|
color: $gray-darkest;
|
||||||
|
|
||||||
.note-action-button {
|
.note-action-button {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
@ -428,7 +446,8 @@ ul.notes {
|
||||||
.award-control-icon-positive,
|
.award-control-icon-positive,
|
||||||
.award-control-icon-super-positive {
|
.award-control-icon-super-positive {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin-left: -20px;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -289,8 +289,12 @@ table.u2f-registrations {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
.bordered-box {
|
.bordered-box {
|
||||||
border: 1px solid $border-color;
|
border: 1px solid $blue-300;
|
||||||
border-radius: $border-radius-default;
|
border-radius: $border-radius-default;
|
||||||
|
background-color: $blue-25;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.landing {
|
.landing {
|
||||||
|
@ -298,28 +302,59 @@ table.u2f-registrations {
|
||||||
margin-bottom: $gl-padding;
|
margin-bottom: $gl-padding;
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
margin-right: 20px;
|
position: absolute;
|
||||||
}
|
right: 20px;
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
.dismiss-icon {
|
.dismiss-icon {
|
||||||
float: right;
|
float: right;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: $cycle-analytics-dismiss-icon-color;
|
color: $blue-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
.dismiss-icon {
|
||||||
|
color: $blue-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.svg-container {
|
.svg-container {
|
||||||
text-align: center;
|
margin-right: 30px;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 136px;
|
height: 110px;
|
||||||
height: 136px;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-callout-copy {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(max-width: $screen-xs-max) {
|
@media(max-width: $screen-xs-max) {
|
||||||
.inner-content {
|
text-align: center;
|
||||||
padding-left: 30px;
|
|
||||||
|
.bordered-box {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing {
|
||||||
|
.svg-container,
|
||||||
|
.user-callout-copy {
|
||||||
|
margin: 0;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 75px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -596,6 +596,10 @@ pre.light-well {
|
||||||
|
|
||||||
.avatar-container {
|
.avatar-container {
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
|
|
||||||
|
> a {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-details {
|
.project-details {
|
||||||
|
@ -929,27 +933,23 @@ pre.light-well {
|
||||||
}
|
}
|
||||||
|
|
||||||
.variable-key {
|
.variable-key {
|
||||||
width: 300px;
|
max-width: 120px;
|
||||||
max-width: 300px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
white-space: nowrap;
|
||||||
// override bootstrap
|
text-overflow: ellipsis;
|
||||||
white-space: normal!important;
|
|
||||||
|
|
||||||
@media (max-width: $screen-sm-max) {
|
|
||||||
width: 150px;
|
|
||||||
max-width: 150px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.variable-value {
|
.variable-value {
|
||||||
@media(max-width: $screen-xs-max) {
|
max-width: 150px;
|
||||||
width: 150px;
|
overflow: hidden;
|
||||||
max-width: 150px;
|
word-wrap: break-word;
|
||||||
overflow: hidden;
|
white-space: nowrap;
|
||||||
word-wrap: break-word;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.variable-menu {
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,18 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def usage_data
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
usage_data = Gitlab::UsageData.data
|
||||||
|
usage_data_json = params[:pretty] ? JSON.pretty_generate(usage_data) : usage_data.to_json
|
||||||
|
|
||||||
|
render html: Gitlab::Highlight.highlight('payload.json', usage_data_json)
|
||||||
|
end
|
||||||
|
format.json { render json: Gitlab::UsageData.to_json }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def reset_runners_token
|
def reset_runners_token
|
||||||
@application_setting.reset_runners_registration_token!
|
@application_setting.reset_runners_registration_token!
|
||||||
flash[:notice] = 'New runners registration token has been generated!'
|
flash[:notice] = 'New runners registration token has been generated!'
|
||||||
|
@ -135,6 +147,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
||||||
:version_check_enabled,
|
:version_check_enabled,
|
||||||
:terminal_max_session_time,
|
:terminal_max_session_time,
|
||||||
:polling_interval_multiplier,
|
:polling_interval_multiplier,
|
||||||
|
:usage_ping_enabled,
|
||||||
|
|
||||||
disabled_oauth_sign_in_sources: [],
|
disabled_oauth_sign_in_sources: [],
|
||||||
import_sources: [],
|
import_sources: [],
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
class Admin::CohortsController < Admin::ApplicationController
|
||||||
|
def index
|
||||||
|
if current_application_settings.usage_ping_enabled
|
||||||
|
cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
|
||||||
|
CohortsService.new.execute
|
||||||
|
end
|
||||||
|
|
||||||
|
@cohorts = CohortsSerializer.new.represent(cohorts_results)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,7 +7,7 @@ class Admin::SpamLogsController < Admin::ApplicationController
|
||||||
spam_log = SpamLog.find(params[:id])
|
spam_log = SpamLog.find(params[:id])
|
||||||
|
|
||||||
if params[:remove_user]
|
if params[:remove_user]
|
||||||
spam_log.remove_user
|
spam_log.remove_user(deleted_by: current_user)
|
||||||
redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed."
|
redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed."
|
||||||
else
|
else
|
||||||
spam_log.destroy
|
spam_log.destroy
|
||||||
|
|
|
@ -269,6 +269,7 @@ class ApplicationController < ActionController::Base
|
||||||
def set_locale
|
def set_locale
|
||||||
requested_locale = current_user&.preferred_language || request.env['HTTP_ACCEPT_LANGUAGE'] || I18n.default_locale
|
requested_locale = current_user&.preferred_language || request.env['HTTP_ACCEPT_LANGUAGE'] || I18n.default_locale
|
||||||
locale = FastGettext.set_locale(requested_locale)
|
locale = FastGettext.set_locale(requested_locale)
|
||||||
|
|
||||||
I18n.locale = locale
|
I18n.locale = locale
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,17 +1,29 @@
|
||||||
module CreatesCommit
|
module CreatesCommit
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def set_start_branch_to_branch_name
|
||||||
|
branch_exists = @repository.find_branch(@branch_name)
|
||||||
|
@start_branch = @branch_name if branch_exists
|
||||||
|
end
|
||||||
|
|
||||||
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
|
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
|
||||||
set_commit_variables
|
if can?(current_user, :push_code, @project)
|
||||||
|
@project_to_commit_into = @project
|
||||||
|
@branch_name ||= @ref
|
||||||
|
else
|
||||||
|
@project_to_commit_into = current_user.fork_of(@project)
|
||||||
|
@branch_name ||= @project_to_commit_into.repository.next_branch('patch')
|
||||||
|
end
|
||||||
|
|
||||||
|
@start_branch ||= @ref || @branch_name
|
||||||
|
|
||||||
commit_params = @commit_params.merge(
|
commit_params = @commit_params.merge(
|
||||||
start_project: @mr_target_project,
|
start_project: @project,
|
||||||
start_branch: @mr_target_branch,
|
start_branch: @start_branch,
|
||||||
target_branch: @mr_source_branch
|
branch_name: @branch_name
|
||||||
)
|
)
|
||||||
|
|
||||||
result = service.new(
|
result = service.new(@project_to_commit_into, current_user, commit_params).execute
|
||||||
@mr_source_project, current_user, commit_params).execute
|
|
||||||
|
|
||||||
if result[:status] == :success
|
if result[:status] == :success
|
||||||
update_flash_notice(success_notice)
|
update_flash_notice(success_notice)
|
||||||
|
@ -72,30 +84,30 @@ module CreatesCommit
|
||||||
|
|
||||||
def new_merge_request_path
|
def new_merge_request_path
|
||||||
new_namespace_project_merge_request_path(
|
new_namespace_project_merge_request_path(
|
||||||
@mr_source_project.namespace,
|
@project_to_commit_into.namespace,
|
||||||
@mr_source_project,
|
@project_to_commit_into,
|
||||||
merge_request: {
|
merge_request: {
|
||||||
source_project_id: @mr_source_project.id,
|
source_project_id: @project_to_commit_into.id,
|
||||||
target_project_id: @mr_target_project.id,
|
target_project_id: @project.id,
|
||||||
source_branch: @mr_source_branch,
|
source_branch: @branch_name,
|
||||||
target_branch: @mr_target_branch
|
target_branch: @start_branch
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def existing_merge_request_path
|
def existing_merge_request_path
|
||||||
namespace_project_merge_request_path(@mr_target_project.namespace, @mr_target_project, @merge_request)
|
namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_request_exists?
|
def merge_request_exists?
|
||||||
return @merge_request if defined?(@merge_request)
|
return @merge_request if defined?(@merge_request)
|
||||||
|
|
||||||
@merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened.
|
@merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.
|
||||||
find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch, source_project_id: @mr_source_project)
|
find_by(source_project_id: @project_to_commit_into, source_branch: @branch_name, target_branch: @start_branch)
|
||||||
end
|
end
|
||||||
|
|
||||||
def different_project?
|
def different_project?
|
||||||
@mr_source_project != @mr_target_project
|
@project_to_commit_into != @project
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_merge_request?
|
def create_merge_request?
|
||||||
|
@ -103,22 +115,6 @@ module CreatesCommit
|
||||||
# as the target branch in the same project,
|
# as the target branch in the same project,
|
||||||
# we don't want to create a merge request.
|
# we don't want to create a merge request.
|
||||||
params[:create_merge_request].present? &&
|
params[:create_merge_request].present? &&
|
||||||
(different_project? || @mr_target_branch != @mr_source_branch)
|
(different_project? || @start_branch != @branch_name)
|
||||||
end
|
|
||||||
|
|
||||||
def set_commit_variables
|
|
||||||
if can?(current_user, :push_code, @project)
|
|
||||||
@mr_source_project = @project
|
|
||||||
@target_branch ||= @ref
|
|
||||||
else
|
|
||||||
@mr_source_project = current_user.fork_of(@project)
|
|
||||||
@target_branch ||= @mr_source_project.repository.next_branch('patch')
|
|
||||||
end
|
|
||||||
|
|
||||||
# Merge request to this project
|
|
||||||
@mr_target_project = @project
|
|
||||||
@mr_target_branch ||= @ref || @target_branch
|
|
||||||
|
|
||||||
@mr_source_branch = @target_branch
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -89,9 +89,4 @@ class Projects::ApplicationController < ApplicationController
|
||||||
def builds_enabled
|
def builds_enabled
|
||||||
return render_404 unless @project.feature_available?(:builds, current_user)
|
return render_404 unless @project.feature_available?(:builds, current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_ref
|
|
||||||
branch_exists = @repository.find_branch(@target_branch)
|
|
||||||
@ref = @target_branch if branch_exists
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,10 +25,10 @@ class Projects::BlobController < Projects::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
update_ref
|
set_start_branch_to_branch_name
|
||||||
|
|
||||||
create_commit(Files::CreateService, success_notice: "The file has been successfully created.",
|
create_commit(Files::CreateService, success_notice: "The file has been successfully created.",
|
||||||
success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) },
|
success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@branch_name, @file_path)) },
|
||||||
failure_view: :new,
|
failure_view: :new,
|
||||||
failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref))
|
failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref))
|
||||||
end
|
end
|
||||||
|
@ -69,10 +69,10 @@ class Projects::BlobController < Projects::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
create_commit(Files::DestroyService, success_notice: "The file has been successfully deleted.",
|
create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.",
|
||||||
success_path: -> { namespace_project_tree_path(@project.namespace, @project, @target_branch) },
|
success_path: -> { namespace_project_tree_path(@project.namespace, @project, @branch_name) },
|
||||||
failure_view: :show,
|
failure_view: :show,
|
||||||
failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
|
failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def diff
|
def diff
|
||||||
|
@ -127,16 +127,16 @@ class Projects::BlobController < Projects::ApplicationController
|
||||||
|
|
||||||
def after_edit_path
|
def after_edit_path
|
||||||
from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid])
|
from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid])
|
||||||
if from_merge_request && @target_branch == @ref
|
if from_merge_request && @branch_name == @ref
|
||||||
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
|
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
|
||||||
"##{hexdigest(@path)}"
|
"##{hexdigest(@path)}"
|
||||||
else
|
else
|
||||||
namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
|
namespace_project_blob_path(@project.namespace, @project, File.join(@branch_name, @path))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def editor_variables
|
def editor_variables
|
||||||
@target_branch = params[:target_branch]
|
@branch_name = params[:branch_name]
|
||||||
|
|
||||||
@file_path =
|
@file_path =
|
||||||
if action_name.to_s == 'create'
|
if action_name.to_s == 'create'
|
||||||
|
|
|
@ -56,9 +56,7 @@ class Projects::CommitController < Projects::ApplicationController
|
||||||
|
|
||||||
return render_404 if @start_branch.blank?
|
return render_404 if @start_branch.blank?
|
||||||
|
|
||||||
@target_branch = create_new_branch? ? @commit.revert_branch_name : @start_branch
|
@branch_name = create_new_branch? ? @commit.revert_branch_name : @start_branch
|
||||||
|
|
||||||
@mr_target_branch = @start_branch
|
|
||||||
|
|
||||||
create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.",
|
create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.",
|
||||||
success_path: -> { successful_change_path }, failure_path: failed_change_path)
|
success_path: -> { successful_change_path }, failure_path: failed_change_path)
|
||||||
|
@ -69,9 +67,7 @@ class Projects::CommitController < Projects::ApplicationController
|
||||||
|
|
||||||
return render_404 if @start_branch.blank?
|
return render_404 if @start_branch.blank?
|
||||||
|
|
||||||
@target_branch = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch
|
@branch_name = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch
|
||||||
|
|
||||||
@mr_target_branch = @start_branch
|
|
||||||
|
|
||||||
create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.",
|
create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.",
|
||||||
success_path: -> { successful_change_path }, failure_path: failed_change_path)
|
success_path: -> { successful_change_path }, failure_path: failed_change_path)
|
||||||
|
@ -84,7 +80,7 @@ class Projects::CommitController < Projects::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def successful_change_path
|
def successful_change_path
|
||||||
referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch)
|
referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @branch_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def failed_change_path
|
def failed_change_path
|
||||||
|
|
|
@ -5,6 +5,8 @@ class Projects::GitHttpController < Projects::GitHttpClientController
|
||||||
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
|
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
|
||||||
def info_refs
|
def info_refs
|
||||||
if upload_pack? && upload_pack_allowed?
|
if upload_pack? && upload_pack_allowed?
|
||||||
|
log_user_activity
|
||||||
|
|
||||||
render_ok
|
render_ok
|
||||||
elsif receive_pack? && receive_pack_allowed?
|
elsif receive_pack? && receive_pack_allowed?
|
||||||
render_ok
|
render_ok
|
||||||
|
@ -106,4 +108,8 @@ class Projects::GitHttpController < Projects::GitHttpClientController
|
||||||
def access_klass
|
def access_klass
|
||||||
@access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
|
@access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def log_user_activity
|
||||||
|
Users::ActivityService.new(user, 'pull').execute
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,16 +34,16 @@ class Projects::TreeController < Projects::ApplicationController
|
||||||
def create_dir
|
def create_dir
|
||||||
return render_404 unless @commit_params.values.all?
|
return render_404 unless @commit_params.values.all?
|
||||||
|
|
||||||
update_ref
|
set_start_branch_to_branch_name
|
||||||
create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.",
|
create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.",
|
||||||
success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@target_branch, @dir_name)),
|
success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@branch_name, @dir_name)),
|
||||||
failure_path: namespace_project_tree_path(@project.namespace, @project, @ref))
|
failure_path: namespace_project_tree_path(@project.namespace, @project, @ref))
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def assign_dir_vars
|
def assign_dir_vars
|
||||||
@target_branch = params[:target_branch]
|
@branch_name = params[:branch_name]
|
||||||
|
|
||||||
@dir_name = File.join(@path, params[:dir_name])
|
@dir_name = File.join(@path, params[:dir_name])
|
||||||
@commit_params = {
|
@commit_params = {
|
||||||
|
|
|
@ -35,6 +35,7 @@ class SessionsController < Devise::SessionsController
|
||||||
# hide the signed-in notification
|
# hide the signed-in notification
|
||||||
flash[:notice] = nil
|
flash[:notice] = nil
|
||||||
log_audit_event(current_user, with: authentication_method)
|
log_audit_event(current_user, with: authentication_method)
|
||||||
|
log_user_activity(current_user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -123,6 +124,10 @@ class SessionsController < Devise::SessionsController
|
||||||
for_authentication.security_event
|
for_authentication.security_event
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def log_user_activity(user)
|
||||||
|
Users::ActivityService.new(user, 'login').execute
|
||||||
|
end
|
||||||
|
|
||||||
def load_recaptcha
|
def load_recaptcha
|
||||||
Gitlab::Recaptcha.load_configurations!
|
Gitlab::Recaptcha.load_configurations!
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,7 +34,7 @@ module BlobHelper
|
||||||
if !on_top_of_branch?(project, ref)
|
if !on_top_of_branch?(project, ref)
|
||||||
button_tag 'Edit', class: "#{common_classes} disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
|
button_tag 'Edit', class: "#{common_classes} disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
|
||||||
# This condition applies to anonymous or users who can edit directly
|
# This condition applies to anonymous or users who can edit directly
|
||||||
elsif !current_user || (current_user && can_edit_blob?(blob, project, ref))
|
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
|
||||||
link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
|
link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
|
||||||
elsif current_user && can?(current_user, :fork_project, project)
|
elsif current_user && can?(current_user, :fork_project, project)
|
||||||
button_tag 'Edit', class: "#{common_classes} js-edit-blob-link-fork-toggler"
|
button_tag 'Edit', class: "#{common_classes} js-edit-blob-link-fork-toggler"
|
||||||
|
@ -52,7 +52,7 @@ module BlobHelper
|
||||||
button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
|
button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
|
||||||
elsif blob.lfs_pointer?
|
elsif blob.lfs_pointer?
|
||||||
button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
|
button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
|
||||||
elsif can_edit_blob?(blob, project, ref)
|
elsif can_modify_blob?(blob, project, ref)
|
||||||
button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
|
button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
|
||||||
elsif can?(current_user, :fork_project, project)
|
elsif can?(current_user, :fork_project, project)
|
||||||
continue_params = {
|
continue_params = {
|
||||||
|
@ -90,7 +90,7 @@ module BlobHelper
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_edit_blob?(blob, project = @project, ref = @ref)
|
def can_modify_blob?(blob, project = @project, ref = @ref)
|
||||||
!blob.lfs_pointer? && can_edit_tree?(project, ref)
|
!blob.lfs_pointer? && can_edit_tree?(project, ref)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -110,6 +110,14 @@ module IssuesHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def award_user_authored_class(award)
|
||||||
|
if award == 'thumbsdown' || award == 'thumbsup'
|
||||||
|
'user-authored js-user-authored'
|
||||||
|
else
|
||||||
|
''
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def awards_sort(awards)
|
def awards_sort(awards)
|
||||||
awards.sort_by do |award, notes|
|
awards.sort_by do |award, notes|
|
||||||
if award == "thumbsup"
|
if award == "thumbsup"
|
||||||
|
|