Commit 6c3038e8 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 30985-cancel-pipelines

* master:
  Remove special naming of pipelines folder
  remove changelog (not needed)
  Fix active user count
  add spec and changelog
  Add migration to remove orphaned notification settings
  Improve container registry repository path specs
  Fix duplicated container repository names
  update textarea height and refocus when attaching files
  Remove IIFEs in filtered_search_bundle.js
  Remove IIFEs from diff_notes_bundle.js
parents dbbcb32c a9da3743
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';
......
...@@ -3,65 +3,63 @@ ...@@ -3,65 +3,63 @@
import Vue from 'vue'; import Vue from 'vue';
(() => { const CommentAndResolveBtn = Vue.extend({
const CommentAndResolveBtn = Vue.extend({ props: {
props: { discussionId: String,
discussionId: String, },
data() {
return {
textareaIsEmpty: true,
discussion: {},
};
},
computed: {
showButton: function () {
if (this.discussion) {
return this.discussion.isResolvable();
} else {
return false;
}
}, },
data() { isDiscussionResolved: function () {
return { return this.discussion.isResolved();
textareaIsEmpty: true,
discussion: {},
};
}, },
computed: { buttonText: function () {
showButton: function () { if (this.isDiscussionResolved) {
if (this.discussion) { if (this.textareaIsEmpty) {
return this.discussion.isResolvable(); return "Unresolve discussion";
} else { } else {
return false; return "Comment & unresolve discussion";
} }
}, } else {
isDiscussionResolved: function () { if (this.textareaIsEmpty) {
return this.discussion.isResolved(); return "Resolve discussion";
},
buttonText: function () {
if (this.isDiscussionResolved) {
if (this.textareaIsEmpty) {
return "Unresolve discussion";
} else {
return "Comment & unresolve discussion";
}
} else { } else {
if (this.textareaIsEmpty) { return "Comment & resolve discussion";
return "Resolve discussion";
} else {
return "Comment & resolve discussion";
}
} }
} }
}, }
created() { },
if (this.discussionId) { created() {
this.discussion = CommentsStore.state[this.discussionId]; if (this.discussionId) {
} this.discussion = CommentsStore.state[this.discussionId];
}, }
mounted: function () { },
if (!this.discussionId) return; mounted: function () {
if (!this.discussionId) return;
const $textarea = $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`); const $textarea = $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`);
this.textareaIsEmpty = $textarea.val() === ''; this.textareaIsEmpty = $textarea.val() === '';
$textarea.on('input.comment-and-resolve-btn', () => { $textarea.on('input.comment-and-resolve-btn', () => {
this.textareaIsEmpty = $textarea.val() === ''; this.textareaIsEmpty = $textarea.val() === '';
}); });
}, },
destroyed: function () { destroyed: function () {
if (!this.discussionId) return; if (!this.discussionId) return;
$(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off('input.comment-and-resolve-btn'); $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off('input.comment-and-resolve-btn');
} }
}); });
Vue.component('comment-and-resolve-btn', CommentAndResolveBtn); Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
})(window);
...@@ -4,155 +4,153 @@ ...@@ -4,155 +4,153 @@
import Vue from 'vue'; import Vue from 'vue';
import collapseIcon from '../icons/collapse_icon.svg'; import collapseIcon from '../icons/collapse_icon.svg';
(() => { const DiffNoteAvatars = Vue.extend({
const DiffNoteAvatars = Vue.extend({ props: ['discussionId'],
props: ['discussionId'], data() {
data() { return {
return { isVisible: false,
isVisible: false, lineType: '',
lineType: '', storeState: CommentsStore.state,
storeState: CommentsStore.state, shownAvatars: 3,
shownAvatars: 3, collapseIcon,
collapseIcon, };
}; },
}, template: `
template: ` <div class="diff-comment-avatar-holders"
<div class="diff-comment-avatar-holders" v-show="notesCount !== 0">
v-show="notesCount !== 0"> <div v-if="!isVisible">
<div v-if="!isVisible"> <img v-for="note in notesSubset"
<img v-for="note in notesSubset" class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar"
class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar" width="19"
width="19" height="19"
height="19" role="button"
role="button" data-container="body"
data-container="body" data-placement="top"
data-placement="top" data-html="true"
data-html="true" :data-line-type="lineType"
:data-line-type="lineType" :title="note.authorName + ': ' + note.noteTruncated"
:title="note.authorName + ': ' + note.noteTruncated" :src="note.authorAvatar"
:src="note.authorAvatar" @click="clickedAvatar($event)" />
@click="clickedAvatar($event)" /> <span v-if="notesCount > shownAvatars"
<span v-if="notesCount > shownAvatars" class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
class="diff-comments-more-count has-tooltip js-diff-comment-avatar" data-container="body"
data-container="body" data-placement="top"
data-placement="top" ref="extraComments"
ref="extraComments" role="button"
role="button"
:data-line-type="lineType"
:title="extraNotesTitle"
@click="clickedAvatar($event)">{{ moreText }}</span>
</div>
<button class="diff-notes-collapse js-diff-comment-avatar"
type="button"
aria-label="Show comments"
:data-line-type="lineType" :data-line-type="lineType"
@click="clickedAvatar($event)" :title="extraNotesTitle"
v-if="isVisible" @click="clickedAvatar($event)">{{ moreText }}</span>
v-html="collapseIcon">
</button>
</div> </div>
`, <button class="diff-notes-collapse js-diff-comment-avatar"
mounted() { type="button"
aria-label="Show comments"
:data-line-type="lineType"
@click="clickedAvatar($event)"
v-if="isVisible"
v-html="collapseIcon">
</button>
</div>
`,
mounted() {
this.$nextTick(() => {
this.addNoCommentClass();
this.setDiscussionVisible();
this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
});
$(document).on('toggle.comments', () => {
this.$nextTick(() => { this.$nextTick(() => {
this.addNoCommentClass();
this.setDiscussionVisible(); this.setDiscussionVisible();
this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
}); });
});
$(document).on('toggle.comments', () => { },
destroyed() {
$(document).off('toggle.comments');
},
watch: {
storeState: {
handler() {
this.$nextTick(() => { this.$nextTick(() => {
this.setDiscussionVisible(); $('.has-tooltip', this.$el).tooltip('fixTitle');
// We need to add/remove a class to an element that is outside the Vue instance
this.addNoCommentClass();
}); });
});
},
destroyed() {
$(document).off('toggle.comments');
},
watch: {
storeState: {
handler() {
this.$nextTick(() => {
$('.has-tooltip', this.$el).tooltip('fixTitle');
// We need to add/remove a class to an element that is outside the Vue instance
this.addNoCommentClass();
});
},
deep: true,
}, },
deep: true,
}, },
computed: { },
notesSubset() { computed: {
let notes = []; notesSubset() {
let notes = [];
if (this.discussion) {
notes = Object.keys(this.discussion.notes) if (this.discussion) {
.slice(0, this.shownAvatars) notes = Object.keys(this.discussion.notes)
.map(noteId => this.discussion.notes[noteId]); .slice(0, this.shownAvatars)
} .map(noteId => this.discussion.notes[noteId]);
}
return notes;
}, return notes;
extraNotesTitle() { },
if (this.discussion) { extraNotesTitle() {
const extra = this.discussion.notesCount() - this.shownAvatars; if (this.discussion) {
const extra = this.discussion.notesCount() - this.shownAvatars;
return `${extra} more comment${extra > 1 ? 's' : ''}`; return `${extra} more comment${extra > 1 ? 's' : ''}`;
} }
return ''; return '';
}, },
discussion() { discussion() {
return this.storeState[this.discussionId]; return this.storeState[this.discussionId];
}, },
notesCount() { notesCount() {
if (this.discussion) { if (this.discussion) {
return this.discussion.notesCount(); return this.discussion.notesCount();
} }
return 0; return 0;
}, },
moreText() { moreText() {
const plusSign = this.notesCount < 100 ? '+' : ''; const plusSign = this.notesCount < 100 ? '+' : '';
return `${plusSign}${this.notesCount - this.shownAvatars}`; return `${plusSign}${this.notesCount - this.shownAvatars}`;
},
}, },
methods: { },
clickedAvatar(e) { methods: {
notes.addDiffNote(e); clickedAvatar(e) {
notes.addDiffNote(e);
// Toggle the active state of the toggle all button // Toggle the active state of the toggle all button
this.toggleDiscussionsToggleState(); this.toggleDiscussionsToggleState();
this.$nextTick(() => { this.$nextTick(() => {
this.setDiscussionVisible(); this.setDiscussionVisible();
$('.has-tooltip', this.$el).tooltip('fixTitle'); $('.has-tooltip', this.$el).tooltip('fixTitle');
$('.has-tooltip', this.$el).tooltip('hide'); $('.has-tooltip', this.$el).tooltip('hide');
}); });
}, },
addNoCommentClass() { addNoCommentClass() {
const notesCount = this.notesCount; const notesCount = this.notesCount;
$(this.$el).closest('.js-avatar-container') $(this.$el).closest('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0) .toggleClass('js-no-comment-btn', notesCount > 0)
.nextUntil('.js-avatar-container') .nextUntil('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0); .toggleClass('js-no-comment-btn', notesCount > 0);
}, },
toggleDiscussionsToggleState() { toggleDiscussionsToggleState() {
const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
const $visibleNotesHolders = $notesHolders.filter(':visible'); const $visibleNotesHolders = $notesHolders.filter(':visible');
const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments'); const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments');
$toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length); $toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length);
}, },
setDiscussionVisible() { setDiscussionVisible() {
this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible'); this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible');
},
}, },
}); },
});
Vue.component('diff-note-avatars', DiffNoteAvatars); Vue.component('diff-note-avatars', DiffNoteAvatars);
})();
...@@ -4,192 +4,190 @@ ...@@ -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) {
return elements.map(function() {
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 (!discussion.isResolved()) {
// If this is the last unresolved discussion on the diffs tab, lastId = discussion.id;
// 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;
const discussionIdsForElements = function(elements) {
return elements.map(function() {
return $(this).attr('data-discussion-id');
}).toArray();
};
if (!hasDiscussionsToJumpTo) { const discussions = this.discussions;
// 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') { if (activeTab === 'diffs') {
discussionsSelector = '.discussion[data-discussion-id]'; discussionsSelector = '.diffs .notes[data-discussion-id]';
discussionIdsInScope = discussionIdsForElements($(discussionsSelector)); 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()) {
unresolvedDiscussionCount += 1;
}
}
if (!discussion) { if (this.discussionId && !this.discussion.isResolved()) {
// Discussions for comments on commits in this MR don't have a resolved status. // If this is the last unresolved discussion on the diffs tab,
continue; // 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;
}
if (!firstUnresolvedDiscussionId && !discussion.isResolved()) { if (!hasDiscussionsToJumpTo) {
firstUnresolvedDiscussionId = discussionId; // 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 (jumpToFirstDiscussion) { if (activeTab === 'notes') {
break; 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 (!jumpToFirstDiscussion) {
if (currentDiscussionFound) { if (currentDiscussionFound) {
if (!discussion.isResolved()) { if (!discussion.isResolved()) {
nextUnresolvedDiscussionId = discussionId; nextUnresolvedDiscussionId = discussionId;
break; break;
}
else {
continue;
}
} }
else {
if (discussionId === this.discussionId) { continue;
currentDiscussionFound = true;
} }
} }
if (discussionId === this.discussionId) {
currentDiscussionFound = true;
}
} }
}
nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId; nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId;
if (!nextUnresolvedDiscussionId) { if (!nextUnresolvedDiscussionId) {
return; return;
} }
let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`); let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`);
if (activeTab === 'notes') { if (activeTab === 'notes') {
$target = $target.closest('.note-discussion'); $target = $target.closest('.note-discussion');
// If the next discussion is closed, toggle it open. // If the next discussion is closed, toggle it open.
if ($target.find('.js-toggle-content').is(':hidden')) { if ($target.find('.js-toggle-content').is(':hidden')) {
$target.find('.js-toggle-button i').trigger('click'); $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;
} }
} 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; $target = prevEl;
}
} }
$.scrollTo($target, {
offset: 0
});
} }
},
created() {
this.discussion = this.discussions[this.discussionId];
},
});
Vue.component('jump-to-discussion', JumpToDiscussion); $.scrollTo($target, {
})(); offset: 0
});
}
},
created() {
this.discussion = this.discussions[this.discussionId];
},
});
Vue.component('jump-to-discussion', JumpToDiscussion);
...@@ -2,29 +2,27 @@ ...@@ -2,29 +2,27 @@
import Vue from 'vue'; import Vue from 'vue';
(() => { const NewIssueForDiscussion = Vue.extend({
const NewIssueForDiscussion = Vue.extend({ props: {
props: { discussionId: {
discussionId: { type: String,
type: String, required: true,
required: true,
},
}, },
data() { },
return { data() {
discussions: CommentsStore.state, return {
}; discussions: CommentsStore.state,
};
},
computed: {
discussion() {
return this.discussions[this.discussionId];
}, },
computed: { showButton() {
discussion() { if (this.discussion) return !this.discussion.isResolved();
return this.discussions[this.discussionId]; return false;
},
showButton() {
if (this.discussion) return !this.discussion.isResolved();
return false;
},
}, },
}); },
});
Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion); Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion);
})();
...@@ -5,117 +5,115 @@ ...@@ -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 () {
return {
discussions: CommentsStore.state,
loading: false
};
},
watch: {
'discussions': {
handler: 'updateTooltip',
deep: true
}
},
computed: {
discussion: function () {
return this.discussions[this.discussionId];
}, },
data: function () { note: function () {
return { return this.discussion ? this.discussion.getNote(this.noteId) : {};
discussions: CommentsStore.state,
loading: false
};
}, },
watch: { buttonText: function () {
'discussions': { if (this.isResolved) {
handler: 'updateTooltip', return `Resolved by ${this.resolvedByName}`;
deep: true } else if (this.canResolve) {
return 'Mark as resolved';
} else {
return 'Unable to resolve';
} }
}, },
computed: { isResolved: function () {
discussion: function () { if (this.note) {
return this.discussions[this.discussionId]; return this.note.resolved;
}, } else {
note: function () { return false;
return this.discussion ? this.discussion.getNote(this.noteId) : {}; }
}, },
buttonText: function () { resolvedByName: function () {
if (this.isResolved) { return this.note.resolved_by;
return `Resolved by ${this.resolvedByName}`;
} else if (this.canResolve) {
return 'Mark as resolved';
} else {
return 'Unable to resolve';
}
},
isResolved: function () {
if (this.note) {
return this.note.resolved;
} else {
return false;
}
},
resolvedByName: function () {
return this.note.resolved_by;
},
}, },
methods: { },
updateTooltip: function () { methods: {
this.$nextTick(() => { updateTooltip: function () {
$(this.$refs.button) this.$nextTick(() => {
.tooltip('hide') $(this.$refs.button)
.tooltip('fixTitle'); .tooltip('hide')
}); .tooltip('fixTitle');
}, });
resolve: function () { },
if (!this.canResolve) return; resolve: function () {
if (!this.canResolve) return;
let promise; let promise;
this.loading = true; this.loading = true;
if (this.isResolved) { if (this.isResolved) {
promise = ResolveService promise = ResolveService
.unresolve(this.noteId); .unresolve(this.noteId);
} else { } else {
promise = ResolveService promise = ResolveService
.resolve(this.noteId); .resolve(this.noteId);
} }
promise.then((response) => { promise.then((response) => {
this.loading = false; this.loading = false;
if (response.status === 200) { if (response.status === 200) {
const data = response.json(); const data = response.json();
const resolved_by = data ? data.resolved_by : null; const resolved_by = data ? data.resolved_by : null;
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by); CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
this.discussion.updateHeadline(data); this.discussion.updateHeadline(data);
} else { } else {
new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert'); new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
} }
this.updateTooltip(); 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,
}); });
} }
}); },
mounted: function () {
$(this.$refs.button).tooltip({
container: 'body'
});
},
beforeDestroy: function () {
CommentsStore.delete(this.discussionId, this.noteId);
},
created: function () {
CommentsStore.create({
discussionId: this.discussionId,
noteId: this.noteId,
canResolve: this.canResolve,
resolved: this.resolved,
resolvedBy: this.resolvedBy,
authorName: this.authorName,
authorAvatar: this.authorAvatar,
noteTruncated: this.noteTruncated,
});
}
});
Vue.component('resolve-btn', ResolveBtn); Vue.component('resolve-btn', ResolveBtn);
})();
...@@ -4,24 +4,22 @@ ...@@ -4,24 +4,22 @@
import Vue from 'vue'; import Vue from 'vue';
((w) => { window.ResolveCount = Vue.extend({
w.ResolveCount = Vue.extend({ mixins: [DiscussionMixins],
mixins: [DiscussionMixins], props: {
props: { loggedOut: Boolean
loggedOut: Boolean },
data: function () {
return {
discussions: CommentsStore.state
};
},
computed: {
allResolved: function () {
return this.resolvedDiscussionCount === this.discussionCount;
}, },
data: function () { resolvedCountText() {
return { return this.discussionCount === 1 ? 'discussion' : 'discussions';
discussions: CommentsStore.state
};
},
computed: {
allResolved: function () {
return this.resolvedDiscussionCount === this.discussionCount;
},
resolvedCountText() {
return this.discussionCount === 1 ? 'discussion' : 'discussions';
}
} }
}); }
})(window); });
...@@ -4,59 +4,57 @@ ...@@ -4,59 +4,57 @@
import Vue from 'vue'; import Vue from 'vue';
(() => { const ResolveDiscussionBtn = Vue.extend({
const ResolveDiscussionBtn = Vue.extend({ props: {
props: { discussionId: String,
discussionId: String, mergeRequestId: Number,
mergeRequestId: Number, canResolve: Boolean,
canResolve: Boolean, },
}, data: function() {
data: function() { return {
return { discussion: {},
discussion: {}, };
}; },
computed: {
showButton: function () {
if (this.discussion) {
return this.discussion.isResolvable();
} else {
return false;
}
}, },
computed: { isDiscussionResolved: function () {
showButton: function () { if (this.discussion) {
if (this.discussion) { return this.discussion.isResolved();
return this.discussion.isResolvable(); } else {
} else { return false;
return false;
}
},
isDiscussionResolved: function () {
if (this.discussion) {
return this.discussion.isResolved();
} else {
return false;
}
},
buttonText: function () {
if (this.isDiscussionResolved) {
return "Unresolve discussion";
} else {
return "Resolve discussion";
}
},
loading: function () {
if (this.discussion) {
return this.discussion.loading;
} else {
return false;
}
} }
}, },
methods: { buttonText: function () {
resolve: function () { if (this.isDiscussionResolved) {
ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId); return "Unresolve discussion";
} else {
return "Resolve discussion";
} }
}, },
created: function () { loading: function () {
CommentsStore.createDiscussion(this.discussionId, this.canResolve); if (this.discussion) {
return this.discussion.loading;
this.discussion = CommentsStore.state[this.discussionId]; } else {
return false;
}
}
},
methods: {
resolve: function () {
ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
} }
}); },
created: function () {
CommentsStore.createDiscussion(this.discussionId, this.canResolve);
this.discussion = CommentsStore.state[this.discussionId];
}
});
Vue.component('resolve-discussion-btn', ResolveDiscussionBtn); Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
})();
/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */ /* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */
((w) => { window.DiscussionMixins = {
w.DiscussionMixins = { computed: {
computed: { discussionCount: function () {
discussionCount: function () { return Object.keys(this.discussions).length;
return Object.keys(this.discussions).length; },
}, resolvedDiscussionCount: function () {
resolvedDiscussionCount: function () { let resolvedCount = 0;
let resolvedCount = 0;
for (const discussionId in this.discussions) { for (const discussionId in this.discussions) {
const discussion = this.discussions[discussionId]; const discussion = this.discussions[discussionId];
if (discussion.isResolved()) { if (discussion.isResolved()) {
resolvedCount += 1; resolvedCount += 1;
}
} }
}
return resolvedCount; return resolvedCount;
}, },
unresolvedDiscussionCount: function () { unresolvedDiscussionCount: function () {
let unresolvedCount = 0; let unresolvedCount = 0;
for (const discussionId in this.discussions) { for (const discussionId in this.discussions) {
const discussion = this.discussions[discussionId]; const discussion = this.discussions[discussionId];
if (!discussion.isResolved()) { if (!discussion.isResolved()) {
unresolvedCount += 1; unresolvedCount += 1;
}
} }
return unresolvedCount;
} }
return unresolvedCount;
} }
}; }
})(window); };
...@@ -9,76 +9,74 @@ require('../../vue_shared/vue_resource_interceptor'); ...@@ -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) { resolve(noteId) {
return this.noteResource.delete({ noteId }, {}); return this.noteResource.save({ noteId }, {});
} }
toggleResolveForDiscussion(mergeRequestId, discussionId) { unresolve(noteId) {
const discussion = CommentsStore.state[discussionId]; return this.noteResource.delete({ noteId }, {});
const isResolved = discussion.isResolved(); }
let promise;
if (isResolved) { toggleResolveForDiscussion(mergeRequestId, discussionId) {
promise = this.unResolveAll(mergeRequestId, discussionId); const discussion = CommentsStore.state[discussionId];
} else { const isResolved = discussion.isResolved();
promise = this.resolveAll(mergeRequestId, discussionId); let promise;
}
promise.then((response) => { if (isResolved) {
discussion.loading = false; promise = this.unResolveAll(mergeRequestId, discussionId);
} else {
promise = this.resolveAll(mergeRequestId, discussionId);
}
if (response.status === 200) { promise.then((response) => {
const data = response.json(); discussion.loading = false;
const resolved_by = data ? data.resolved_by : null;
if (isResolved) { if (response.status === 200) {
discussion.unResolveAllNotes(); const data = response.json();
} else { const resolved_by = data ? data.resolved_by : null;
discussion.resolveAllNotes(resolved_by);
}
discussion.updateHeadline(data); if (isResolved) {
discussion.unResolveAllNotes();
} else { } else {
new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert'); discussion.resolveAllNotes(resolved_by);
} }
});
}
resolveAll(mergeRequestId, discussionId) { discussion.updateHeadline(data);
const discussion = CommentsStore.state[discussionId]; } else {
new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
}
});
}
discussion.loading = true; resolveAll(mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
return this.discussionResource.save({ discussion.loading = true;
mergeRequestId,
discussionId return this.discussionResource.save({
}, {}); mergeRequestId,
} discussionId
}, {});
}
unResolveAll(mergeRequestId, discussionId) { unResolveAll(mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId]; const discussion = CommentsStore.state[discussionId];
discussion.loading = true; discussion.loading = true;
return this.discussionResource.delete({ return this.discussionResource.delete({
mergeRequestId, mergeRequestId,
discussionId discussionId
}, {}); }, {});
}
} }
}
gl.DiffNotesResolveServiceClass = ResolveServiceClass; gl.DiffNotesResolveServiceClass = ResolveServiceClass;
})();
...@@ -3,56 +3,54 @@ ...@@ -3,56 +3,54 @@
import Vue from 'vue'; import Vue from 'vue';
((w) => { window.CommentsStore = {
w.CommentsStore = { state: {},
state: {}, get: function (discussionId, noteId) {
get: function (discussionId, noteId) { return this.state[discussionId].getNote(noteId);
return this.state[discussionId].getNote(noteId); },
}, createDiscussion: function (discussionId, canResolve) {
createDiscussion: function (discussionId, canResolve) { let discussion = this.state[discussionId];
let discussion = this.state[discussionId]; if (!this.state[discussionId]) {
if (!this.state[discussionId]) { discussion = new DiscussionModel(discussionId);
discussion = new DiscussionModel(discussionId); Vue.set(this.state, discussionId, discussion);
Vue.set(this.state, discussionId, discussion); }
}
if (canResolve !== undefined) { if (canResolve !== undefined) {
discussion.canResolve = canResolve; discussion.canResolve = canResolve;
} }
return discussion; return discussion;
}, },
create: function (noteObj) { create: function (noteObj) {
const discussion = this.createDiscussion(noteObj.discussionId); const discussion = this.createDiscussion(noteObj.discussionId);
discussion.createNote(noteObj);
},
update: function (discussionId, noteId, resolved, resolved_by) {
const discussion = this.state[discussionId];
const note = discussion.getNote(noteId);
note.resolved = resolved;
note.resolved_by = resolved_by;
},
delete: function (discussionId, noteId) {
const discussion = this.state[discussionId];
discussion.deleteNote(noteId);
if (discussion.notesCount() === 0) {
Vue.delete(this.state, discussionId);
}
},
unresolvedDiscussionIds: function () {
const ids = [];
discussion.createNote(noteObj); for (const discussionId in this.state) {
},
update: function (discussionId, noteId, resolved, resolved_by) {
const discussion = this.state[discussionId];
const note = discussion.getNote(noteId);
note.resolved = resolved;
note.resolved_by = resolved_by;
},
delete: function (discussionId, noteId) {
const discussion = this.state[discussionId]; const discussion = this.state[discussionId];
discussion.deleteNote(noteId);
if (discussion.notesCount() === 0) { if (!discussion.isResolved()) {
Vue.delete(this.state, discussionId); ids.push(discussion.id);
} }
},
unresolvedDiscussionIds: function () {
const ids = [];
for (const discussionId in this.state) {
const discussion = this.state[discussionId];
if (!discussion.isResolved()) {
ids.push(discussion.id);
}
}
return ids;
} }
};
})(window); return ids;
}
};
...@@ -130,13 +130,15 @@ window.DropzoneInput = (function() { ...@@ -130,13 +130,15 @@ window.DropzoneInput = (function() {
var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
var formattedText = text; var formattedText = text;
if (shouldPad) formattedText += "\n\n"; if (shouldPad) formattedText += "\n\n";
caretStart = $(child)[0].selectionStart; const textarea = child.get(0);
caretEnd = $(child)[0].selectionEnd; caretStart = textarea.selectionStart;
caretEnd = textarea.selectionEnd;
textEnd = $(child).val().length; textEnd = $(child).val().length;
beforeSelection = $(child).val().substring(0, caretStart); beforeSelection = $(child).val().substring(0, caretStart);
afterSelection = $(child).val().substring(caretEnd, textEnd); afterSelection = $(child).val().substring(caretEnd, textEnd);
$(child).val(beforeSelection + formattedText + afterSelection); $(child).val(beforeSelection + formattedText + afterSelection);
child.get(0).setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
textarea.style.height = `${textarea.scrollHeight}px`;
return form_textarea.trigger("input"); return form_textarea.trigger("input");
}; };
getFilename = function(e) { getFilename = function(e) {
...@@ -180,7 +182,7 @@ window.DropzoneInput = (function() { ...@@ -180,7 +182,7 @@ window.DropzoneInput = (function() {
}; };
insertToTextArea = function(filename, url) { insertToTextArea = function(filename, url) {
return $(child).val(function(index, val) { return $(child).val(function(index, val) {
return val.replace("{{" + filename + "}}", url + "\n"); return val.replace("{{" + filename + "}}", url);
}); });
}; };
appendToTextArea = function(url) { appendToTextArea = function(url) {
...@@ -215,6 +217,7 @@ window.DropzoneInput = (function() { ...@@ -215,6 +217,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();
}); });
} }
......
...@@ -2,82 +2,80 @@ import Filter from '~/droplab/plugins/filter'; ...@@ -2,82 +2,80 @@ import Filter from '~/droplab/plugins/filter';
require('./filtered_search_dropdown'); require('./filtered_search_dropdown');
(() => { class DropdownHint extends gl.FilteredSearchDropdown {
class DropdownHint extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input, filter) {
constructor(droplab, dropdown, input, filter) { super(droplab, dropdown, input, filter);
super(droplab, dropdown, input, filter); this.config = {
this.config = { Filter: {
Filter: { template: 'hint',
template: 'hint', filterFunction: gl.DropdownUtils.filterHint.bind(null, input),
filterFunction: gl.DropdownUtils.filterHint.bind(null, input), },
}, };
}; }
}
itemClicked(e) {
const { selected } = e.detail;
if (selected.tagName === 'LI') { itemClicked(e) {
if (selected.hasAttribute('data-value')) { const { selected } = e.detail;
this.dismissDropdown();
} else if (selected.getAttribute('data-action') === 'submit') {
this.dismissDropdown();
this.dispatchFormSubmitEvent();
} else {
const token = selected.querySelector('.js-filter-hint').innerText.trim();
const tag = selected.querySelector('.js-filter-tag').innerText.trim();
if (tag.length) { if (selected.tagName === 'LI') {
// Get previous input values in the input field and convert them into visual tokens if (selected.hasAttribute('data-value')) {
const previousInputValues = this.input.value.split(' '); this.dismissDropdown();
const searchTerms = []; } else if (selected.getAttribute('data-action') === 'submit') {
this.dismissDropdown();
this.dispatchFormSubmitEvent();
} else {
const token = selected.querySelector('.js-filter-hint').innerText.trim();
const tag = selected.querySelector('.js-filter-tag').innerText.trim();
previousInputValues.forEach((value, index) => { if (tag.length) {
searchTerms.push(value); // Get previous input values in the input field and convert them into visual tokens
const previousInputValues = this.input.value.split(' ');
const searchTerms = [];
if (index === previousInputValues.length - 1 previousInputValues.forEach((value, index) => {
&& token.indexOf(value.toLowerCase()) !== -1) { searchTerms.push(value);
searchTerms.pop();
}
});
if (searchTerms.length > 0) { if (index === previousInputValues.length - 1
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' ')); && token.indexOf(value.toLowerCase()) !== -1) {
searchTerms.pop();
} }
});
gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container); if (searchTerms.length > 0) {
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
} }
this.dismissDropdown();
this.dispatchInputEvent(); gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container);
} }
this.dismissDropdown();
this.dispatchInputEvent();
} }
} }
}
renderContent() { renderContent() {
const dropdownData = []; const dropdownData = [];
[].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => { [].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
const { icon, hint, tag, type } = dropdownMenu.dataset; const { icon, hint, tag, type } = dropdownMenu.dataset;
if (icon && hint && tag) { if (icon && hint && tag) {
dropdownData.push( dropdownData.push(
Object.assign({ Object.assign({
icon: `fa-${icon}`, icon: `fa-${icon}`,
hint, hint,
tag: `&lt;${tag}&gt;`, tag: `&lt;${tag}&gt;`,
}, type && { type }), }, type && { type }),
); );
} }
}); });
this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config); this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config);
this.droplab.setData(this.hookId, dropdownData); this.droplab.setData(this.hookId, dropdownData);
} }
init() { init() {
this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init(); this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init();
}
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.DropdownHint = DropdownHint; gl.DropdownHint = DropdownHint;
})();
...@@ -5,48 +5,46 @@ import Filter from '~/droplab/plugins/filter'; ...@@ -5,48 +5,46 @@ import Filter from '~/droplab/plugins/filter';
require('./filtered_search_dropdown'); require('./filtered_search_dropdown');
(() => { class DropdownNonUser extends gl.FilteredSearchDropdown {
class DropdownNonUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input, filter, endpoint, symbol) {
constructor(droplab, dropdown, input, filter, endpoint, symbol) { super(droplab, dropdown, input, filter);
super(droplab, dropdown, input, filter); this.symbol = symbol;
this.symbol = symbol; this.config = {
this.config = { Ajax: {
Ajax: { endpoint,
endpoint, method: 'setData',
method: 'setData', loadingTemplate: this.loadingTemplate,
loadingTemplate: this.loadingTemplate, onError() {
onError() { /* eslint-disable no-new */
/* eslint-disable no-new */ new Flash('An error occured fetching the dropdown data.');
new Flash('An error occured fetching the dropdown data.'); /* eslint-enable no-new */
/* eslint-enable no-new */
},
}, },
Filter: { },
filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input), Filter: {
template: 'title', filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
}, template: 'title',
}; },
} };
}
itemClicked(e) { itemClicked(e) {
super.itemClicked(e, (selected) => { super.itemClicked(e, (selected) => {
const title = selected.querySelector('.js-data-value').innerText.trim(); const title = selected.querySelector('.js-data-value').innerText.trim();
return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`; return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`;
}); });
} }
renderContent(forceShowList = false) { renderContent(forceShowList = false) {
this.droplab this.droplab
.changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config); .changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config);
super.renderContent(forceShowList); super.renderContent(forceShowList);
} }
init() { init() {
this.droplab this.droplab
.addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init(); .addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
}
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.DropdownNonUser = DropdownNonUser; gl.DropdownNonUser = DropdownNonUser;
})();
...@@ -4,69 +4,67 @@ import AjaxFilter from '~/droplab/plugins/ajax_filter'; ...@@ -4,69 +4,67 @@ import AjaxFilter from '~/droplab/plugins/ajax_filter';
require('./filtered_search_dropdown'); require('./filtered_search_dropdown');
(() => { class DropdownUser extends gl.FilteredSearchDropdown {
class DropdownUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input, filter) {
constructor(droplab, dropdown, input, filter) { super(droplab, dropdown, input, filter);
super(droplab, dropdown, input, filter); this.config = {
this.config = { AjaxFilter: {
AjaxFilter: { endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, searchKey: 'search',
searchKey: 'search', params: {
params: { per_page: 20,
per_page: 20, active: true,
active: true, project_id: this.getProjectId(),
project_id: this.getProjectId(), current_user: true,
current_user: true,
},
searchValueFunction: this.getSearchInput.bind(this),
loadingTemplate: this.loadingTemplate,
onError() {
/* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.');
/* eslint-enable no-new */
},
}, },
}; searchValueFunction: this.getSearchInput.bind(this),
} loadingTemplate: this.loadingTemplate,
onError() {
itemClicked(e) { /* eslint-disable no-new */
super.itemClicked(e, new Flash('An error occured fetching the dropdown data.');
selected => selected.querySelector('.dropdown-light-content').innerText.trim()); /* eslint-enable no-new */
} },
},
renderContent(forceShowList = false) { };
this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config); }
super.renderContent(forceShowList);
}
getProjectId() { itemClicked(e) {
return this.input.getAttribute('data-project-id'); super.itemClicked(e,
} selected => selected.querySelector('.dropdown-light-content').innerText.trim());
}
getSearchInput() { renderContent(forceShowList = false) {
const query = gl.DropdownUtils.getSearchInput(this.input); this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config);
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); super.renderContent(forceShowList);
}
let value = lastToken || ''; getProjectId() {
return this.input.getAttribute('data-project-id');
}
if (value[0] === '@') { getSearchInput() {
value = value.slice(1); const query = gl.DropdownUtils.getSearchInput(this.input);
} const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
// Removes the first character if it is a quotation so that we can search let value = lastToken || '';
// with multiple words
if (value[0] === '"' || value[0] === '\'') {
value = value.slice(1);
}
return value; if (value[0] === '@') {
value = value.slice(1);
} }
init() { // Removes the first character if it is a quotation so that we can search
this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init(); // with multiple words
if (value[0] === '"' || value[0] === '\'') {
value = value.slice(1);
} }
return value;
}
init() {
this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init();
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.DropdownUser = DropdownUser; gl.DropdownUser = DropdownUser;
})();
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);
static filterWithSymbol(filterSymbol, input, item) {
const updatedItem = item;
const searchInput = gl.DropdownUtils.getSearchInput(input);
const title = updatedItem.title.toLowerCase(); const title = updatedItem.title.toLowerCase();
let value = searchInput.toLowerCase(); let value = searchInput.toLowerCase();
let symbol = ''; let symbol = '';
// Remove the symbol for filter // Remove the symbol for filter
if (value[0] === filterSymbol) { if (value[0] === filterSymbol) {
symbol = value[0]; symbol = value[0];
value = value.slice(1); value = value.slice(1);
} }
// Removes the first character if it is a quotation so that we can search // Removes the first character if it is a quotation so that we can search
// with multiple words // with multiple words
if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
value = value.slice(1); value = value.slice(1);
} }
// Eg. filterSymbol = ~ for labels
const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
const match = title.indexOf(`${symbol}${value}`) !== -1;
// Eg. filterSymbol = ~ for labels updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
const match = title.indexOf(`${symbol}${value}`) !== -1;
updatedItem.droplab_hidden = !match && !matchWithoutSymbol; return updatedItem;
}
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;
} }
static filterHint(input, item) { return updatedItem;
const updatedItem = item; }
const searchInput = gl.DropdownUtils.getSearchQuery(input);
const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput); static setDataValueIfSelected(filter, selected) {
const lastKey = lastToken.key || lastToken || ''; const dataValue = selected.getAttribute('data-value');
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; if (dataValue) {
gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
} }
static setDataValueIfSelected(filter, selected) { // Return boolean based on whether it was set
const dataValue = selected.getAttribute('data-value'); return dataValue !== null;
}
if (dataValue) { // Determines the full search query (visual tokens + input)
gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true); static getSearchQuery(untilInput = false) {
} const container = FilteredSearchContainer.container;
const tokens = [].slice.call(container.querySelectorAll('.tokens-container li'));
const values = [];
// Return boolean based on whether it was set if (untilInput) {
return dataValue !== null; const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token'));
// Add one to include input-token to the tokens array
tokens.splice(inputIndex + 1);
} }
// Determines the full search query (visual tokens + input) tokens.forEach((token) => {
static getSearchQuery(untilInput = false) { if (token.classList.contains('js-visual-token')) {
const container = FilteredSearchContainer.container; const name = token.querySelector('.name');
const tokens = [].slice.call(container.querySelectorAll('.tokens-container li')); const value = token.querySelector('.value');
const values = []; const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
let valueText = '';
if (untilInput) { if (value && value.innerText) {
const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token')); valueText = value.innerText;
// Add one to include input-token to the tokens array }
tokens.splice(inputIndex + 1);
}
tokens.forEach((token) => { if (token.className.indexOf('filtered-search-token') !== -1) {
if (token.classList.contains('js-visual-token')) { values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`);
const name = token.querySelector('.name'); } else {
const value = token.querySelector('.value'); values.push(name.innerText);
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}`);
}
} }
}); } else if (token.classList.contains('input-token')) {
const { isLastVisualTokenValid } =
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
return values const input = FilteredSearchContainer.container.querySelector('.filtered-search');
.map(value => value.trim()) const inputValue = input && input.value;
.join(' ');
}
static getSearchInput(filteredSearchInput) { if (isLastVisualTokenValid) {
const inputValue = filteredSearchInput.value; values.push(inputValue);
const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput); } else {
const previous = values.pop();
values.push(`${previous}${inputValue}`);
}
}
});
return inputValue.slice(0, right); return values
} .map(value => value.trim())
.join(' ');
}
static getInputSelectionPosition(input) { static getSearchInput(filteredSearchInput) {
const selectionStart = input.selectionStart; const inputValue = filteredSearchInput.value;
let inputValue = input.value; const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
// 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 return inputValue.slice(0, right);
// Regex matches last non-whitespace character }
let left = inputValue.slice(0, right).search(/\S+$/);
if (selectionStart === 0) { static getInputSelectionPosition(input) {
left = 0; const selectionStart = input.selectionStart;
} else if (selectionStart === inputValue.length && left < 0) { let inputValue = input.value;
left = inputValue.length; // Replace all spaces inside quote marks with underscores
} else if (left < 0) { // (will continue to match entire string until an end quote is found if any)
left = selectionStart; // 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+$/);
return { if (selectionStart === 0) {
left, left = 0;
right, } else if (selectionStart === inputValue.length && left < 0) {
}; left = inputValue.length;
} else if (left < 0) {
left = selectionStart;
} }
return {
left,
right,
};
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.DropdownUtils = DropdownUtils; gl.DropdownUtils = DropdownUtils;
})();
(() => { const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
class FilteredSearchDropdown {
class FilteredSearchDropdown { constructor(droplab, dropdown, input, filter) {
constructor(droplab, dropdown, input, filter) { this.droplab = droplab;
this.droplab = droplab; this.hookId = input && input.id;
this.hookId = input && input.id; this.input = input;
this.input = input; this.filter = filter;
this.filter = filter; this.dropdown = dropdown;
this.dropdown = dropdown; this.loadingTemplate = `<div class="filter-dropdown-loading">
this.loadingTemplate = `<div class="filter-dropdown-loading"> <i class="fa fa-spinner fa-spin"></i>
<i class="fa fa-spinner fa-spin"></i> </div>`;
</div>`; this.bindEvents();
this.bindEvents(); }
}
bindEvents() {
this.itemClickedWrapper = this.itemClicked.bind(this);
this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
}
unbindEvents() { bindEvents() {
this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); this.itemClickedWrapper = this.itemClicked.bind(this);
} this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
}
getCurrentHook() { unbindEvents() {
return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null; this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper);
} }
itemClicked(e, getValueFunction) { getCurrentHook() {
const { selected } = e.detail; return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null;
}
if (selected.tagName === 'LI' && selected.innerHTML) { itemClicked(e, getValueFunction) {
const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected); const { selected } = e.detail;
if (!dataValueSet) { if (selected.tagName === 'LI' && selected.innerHTML) {
const value = getValueFunction(selected); const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
}
this.resetFilters(); if (!dataValueSet) {
this.dismissDropdown(); const value = getValueFunction(selected);
this.dispatchInputEvent(); gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
} }
}
setAsDropdown() { this.resetFilters();
this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`); this.dismissDropdown();
this.dispatchInputEvent();
} }
}
setOffset(offset = 0) { setAsDropdown() {
if (window.innerWidth > 480) { this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`);
this.dropdown.style.left = `${offset}px`; }
} else {
this.dropdown.style.left = '0px'; setOffset(offset = 0) {
} if (window.innerWidth > 480) {
this.dropdown.style.left = `${offset}px`;
} else {
this.dropdown.style.left = '0px';
} }
}
renderContent(forceShowList = false) { renderContent(forceShowList = false) {
const currentHook = this.getCurrentHook(); const currentHook = this.getCurrentHook();
if (forceShowList && currentHook && currentHook.list.hidden) { if (forceShowList && currentHook && currentHook.list.hidden) {
currentHook.list.show(); currentHook.list.show();
}
} }
}
render(forceRenderContent = false, forceShowList = false) { render(forceRenderContent = false, forceShowList = false) {
this.setAsDropdown(); this.setAsDropdown();
const currentHook = this.getCurrentHook(); const currentHook = this.getCurrentHook();
const firstTimeInitialized = currentHook === null; const firstTimeInitialized = currentHook === null;
if (firstTimeInitialized || forceRenderContent) { if (firstTimeInitialized || forceRenderContent) {
this.renderContent(forceShowList); this.renderContent(forceShowList);
} else if (currentHook.list.list.id !== this.dropdown.id) { } else if (currentHook.list.list.id !== this.dropdown.id) {
this.renderContent(forceShowList); this.renderContent(forceShowList);
}
} }
}
dismissDropdown() { dismissDropdown() {
// Focusing on the input will dismiss dropdown // Focusing on the input will dismiss dropdown
// (default droplab functionality) // (default droplab functionality)
this.input.focus(); this.input.focus();
} }
dispatchInputEvent() { dispatchInputEvent() {
// Propogate input change to FilteredSearchDropdownManager // Propogate input change to FilteredSearchDropdownManager
// so that it can determine which dropdowns to open // so that it can determine which dropdowns to open
this.input.dispatchEvent(new CustomEvent('input', { this.input.dispatchEvent(new CustomEvent('input', {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
})); }));
} }
dispatchFormSubmitEvent() { dispatchFormSubmitEvent() {
// dispatchEvent() is necessary as form.submit() does not // dispatchEvent() is necessary as form.submit() does not
// trigger event handlers // trigger event handlers
this.input.form.dispatchEvent(new Event('submit')); this.input.form.dispatchEvent(new Event('submit'));
} }
hideDropdown() { hideDropdown() {
const currentHook = this.getCurrentHook(); const currentHook = this.getCurrentHook();
if (currentHook) { if (currentHook) {
currentHook.list.hide(); currentHook.list.hide();
}
} }
}
resetFilters() { resetFilters() {
const hook = this.getCurrentHook(); const hook = this.getCurrentHook();
if (hook) { if (hook) {
const data = hook.list.data || []; const data = hook.list.data || [];
const results = data.map((o) => { const results = data.map((o) => {
const updated = o; const updated = o;
updated.droplab_hidden = false; updated.droplab_hidden = false;
return updated; return updated;
}); });
hook.list.render(results); hook.list.render(results);
}
} }
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.FilteredSearchDropdown = FilteredSearchDropdown; gl.FilteredSearchDropdown = FilteredSearchDropdown;
})();
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() {
if (this.droplab) {
this.droplab.destroy();
this.droplab = null;
} }
cleanup() { this.setupMapping();
if (this.droplab) {
this.droplab.destroy();
this.droplab = null;
}
this.setupMapping(); document.removeEventListener('beforeunload', this.cleanupWrapper);
}
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'),
},
};
}
setupMapping() { static addWordToInput(tokenName, tokenValue = '', clicked = false) {
this.mapping = { const input = FilteredSearchContainer.container.querySelector('.filtered-search');
author: {
reference: null, gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
gl: 'DropdownUser', input.value = '';
element: this.container.querySelector('#js-dropdown-author'),
}, if (clicked) {
assignee: { gl.FilteredSearchVisualTokens.moveInputToTheRight();
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) { updateCurrentDropdownOffset() {
const input = FilteredSearchContainer.container.querySelector('.filtered-search'); this.updateDropdownOffset(this.currentDropdown);
}
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue); updateDropdownOffset(key) {
input.value = ''; // Always align dropdown with the input field
let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
if (clicked) { const maxInputWidth = 240;
gl.FilteredSearchVisualTokens.moveInputToTheRight(); const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
}
}
updateCurrentDropdownOffset() { // Make sure offset never exceeds the input container
this.updateDropdownOffset(this.currentDropdown); const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
if (offsetMaxWidth < offset) {
offset = offsetMaxWidth;
} }
updateDropdownOffset(key) { this.mapping[key].reference.setOffset(offset);
// Always align dropdown with the input field }
let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
const maxInputWidth = 240; load(key, firstLoad = false) {
const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth; const mappingKey = this.mapping[key];
const glClass = mappingKey.gl;
const element = mappingKey.element;
let forceShowList = false;
// Make sure offset never exceeds the input container if (!mappingKey.reference) {
const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth; const dl = this.droplab;
if (offsetMaxWidth < offset) { const defaultArguments = [null, dl, element, this.filteredSearchInput, key];
offset = offsetMaxWidth; const glArguments = defaultArguments.concat(mappingKey.extraArguments || []);
}
this.mapping[key].reference.setOffset(offset); // Passing glArguments to `new gl[glClass](<arguments>)`
mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))();
} }
load(key, firstLoad = false) { if (firstLoad) {
const mappingKey = this.mapping[key]; mappingKey.reference.init();
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>)` if (this.currentDropdown === 'hint') {
mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))(); // Force the dropdown to show if it was clicked from the hint dropdown
} forceShowList = true;
}
if (firstLoad) { this.updateDropdownOffset(key);
mappingKey.reference.init(); mappingKey.reference.render(firstLoad, forceShowList);
}
if (this.currentDropdown === 'hint') { this.currentDropdown = key;
// Force the dropdown to show if it was clicked from the hint dropdown }
forceShowList = true;
}
this.updateDropdownOffset(key); loadDropdown(dropdownName = '') {
mappingKey.reference.render(firstLoad, forceShowList); let firstLoad = false;
this.currentDropdown = key; if (!this.droplab) {
firstLoad = true;
this.droplab = new DropLab();
} }
loadDropdown(dropdownName = '') { const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
let firstLoad = false; const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
&& this.mapping[match.key];
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
if (!this.droplab) { if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
firstLoad = true; const key = match && match.key ? match.key : 'hint';
this.droplab = new DropLab(); this.load(key, firstLoad);
} }
}
const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); setDropdown() {
const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key const query = gl.DropdownUtils.getSearchQuery(true);
&& this.mapping[match.key]; const { lastToken, searchToken } = this.tokenizer.processTokens(query);
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { if (this.currentDropdown) {
const key = match && match.key ? match.key : 'hint'; this.updateCurrentDropdownOffset();
this.load(key, firstLoad);
}
} }
setDropdown() { if (lastToken === searchToken && lastToken !== null) {
const query = gl.DropdownUtils.getSearchQuery(true); // Token is not fully initialized yet because it has no value
const { lastToken, searchToken } = this.tokenizer.processTokens(query); // Eg. token = 'label:'
if (this.currentDropdown) { const split = lastToken.split(':');
this.updateCurrentDropdownOffset(); const dropdownName = split[0].split(' ').last();
} this.loadDropdown(split.length > 1 ? dropdownName : '');
} else if (lastToken) {
if (lastToken === searchToken && lastToken !== null) { // Token has been initialized into an object because it has a value
// Token is not fully initialized yet because it has no value this.loadDropdown(lastToken.key);
// Eg. token = 'label:' } else {
this.loadDropdown('hint');
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() { resetDropdowns() {
if (!this.currentDropdown) { if (!this.currentDropdown) {
return; return;
} }
// Force current dropdown to hide // Force current dropdown to hide
this.mapping[this.currentDropdown].reference.hideDropdown(); this.mapping[this.currentDropdown].reference.hideDropdown();
// Re-Load dropdown // Re-Load dropdown
this.setDropdown(); this.setDropdown();
// Reset filters for current dropdown // Reset filters for current dropdown
this.mapping[this.currentDropdown].reference.resetFilters(); this.mapping[this.currentDropdown].reference.resetFilters();
// Reposition dropdown so that it is aligned with cursor // Reposition dropdown so that it is aligned with cursor
this.updateDropdownOffset(this.currentDropdown); this.updateDropdownOffset(this.currentDropdown);
} }
destroyDroplab() { destroyDroplab() {
this.droplab.destroy(); this.droplab.destroy();
}
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager; gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager;
})();
...@@ -6,489 +6,487 @@ import RecentSearchesStore from './stores/recent_searches_store'; ...@@ -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();
cleanup() { this.bindEvents();
this.unbindEvents(); this.loadSearchParamsFromURL();
document.removeEventListener('beforeunload', this.cleanupWrapper); this.dropdownManager.setDropdown();
if (this.recentSearchesRoot) { this.cleanupWrapper = this.cleanup.bind(this);
this.recentSearchesRoot.destroy(); document.addEventListener('beforeunload', this.cleanupWrapper);
}
} }
}
bindEvents() { cleanup() {
this.handleFormSubmit = this.handleFormSubmit.bind(this); this.unbindEvents();
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); document.removeEventListener('beforeunload', this.cleanupWrapper);
this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this);
this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.onClearSearchWrapper = this.onClearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.editTokenWrapper = this.editToken.bind(this);
this.tokenChange = this.tokenChange.bind(this);
this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper);
this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper);
this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.addEventListener('click', this.tokenChange);
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper);
document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenWrapper);
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
}
unbindEvents() { if (this.recentSearchesRoot) {
this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit); this.recentSearchesRoot.destroy();
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper);
this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper);
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
} }
}
checkForBackspace(e) { bindEvents() {
// 8 = Backspace Key this.handleFormSubmit = this.handleFormSubmit.bind(this);
// 46 = Delete Key this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
if (e.keyCode === 8 || e.keyCode === 46) { this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this);
this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.onClearSearchWrapper = this.onClearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.editTokenWrapper = this.editToken.bind(this);
this.tokenChange = this.tokenChange.bind(this);
this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper);
this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper);
this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.addEventListener('click', this.tokenChange);
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper);
document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenWrapper);
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
}
if (this.filteredSearchInput.value === '' && lastVisualToken) { unbindEvents() {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit);
gl.FilteredSearchVisualTokens.removeLastTokenPartial(); this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
} this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper);
this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper);
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
}
checkForBackspace(e) {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
// Reposition dropdown so that it is aligned with cursor if (this.filteredSearchInput.value === '' && lastVisualToken) {
this.dropdownManager.updateCurrentDropdownOffset(); this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
} }
}
checkForEnter(e) { // Reposition dropdown so that it is aligned with cursor
if (e.keyCode === 38 || e.keyCode === 40) { this.dropdownManager.updateCurrentDropdownOffset();
const selectionStart = this.filteredSearchInput.selectionStart; }
}
e.preventDefault(); checkForEnter(e) {
this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart); if (e.keyCode === 38 || e.keyCode === 40) {
} const selectionStart = this.filteredSearchInput.selectionStart;
if (e.keyCode === 13) { e.preventDefault();
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
const dropdownEl = dropdown.element; }
const activeElements = dropdownEl.querySelectorAll('.droplab-item-active');
e.preventDefault(); if (e.keyCode === 13) {
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
const dropdownEl = dropdown.element;
const activeElements = dropdownEl.querySelectorAll('.droplab-item-active');
if (!activeElements.length) { e.preventDefault();
if (this.isHandledAsync) {
e.stopImmediatePropagation();
this.filteredSearchInput.blur(); if (!activeElements.length) {
this.dropdownManager.resetDropdowns(); if (this.isHandledAsync) {
} else { e.stopImmediatePropagation();
// Prevent droplab from opening dropdown
this.dropdownManager.destroyDroplab();
}
this.search(); this.filteredSearchInput.blur();
this.dropdownManager.resetDropdowns();
} else {
// Prevent droplab from opening dropdown
this.dropdownManager.destroyDroplab();
} }
this.search();
} }
} }
}
addInputContainerFocus() { addInputContainerFocus() {
const inputContainer = this.filteredSearchInput.closest('.filtered-search-box'); const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
if (inputContainer) { if (inputContainer) {
inputContainer.classList.add('focus'); inputContainer.classList.add('focus');
}
} }
}
removeInputContainerFocus(e) { removeInputContainerFocus(e) {
const inputContainer = this.filteredSearchInput.closest('.filtered-search-box'); const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null; const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null; const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown && if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
!isElementInStaticFilterDropdown && inputContainer) { !isElementInStaticFilterDropdown && inputContainer) {
inputContainer.classList.remove('focus'); inputContainer.classList.remove('focus');
}
} }
}
static selectToken(e) { static selectToken(e) {
const button = e.target.closest('.selectable'); const button = e.target.closest('.selectable');
if (button) { if (button) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
gl.FilteredSearchVisualTokens.selectToken(button); gl.FilteredSearchVisualTokens.selectToken(button);
}
} }
}
unselectEditTokens(e) { unselectEditTokens(e) {
const inputContainer = this.container.querySelector('.filtered-search-box'); const inputContainer = this.container.querySelector('.filtered-search-box');
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null; const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
const isElementTokensContainer = e.target.classList.contains('tokens-container'); const isElementTokensContainer = e.target.classList.contains('tokens-container');
if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) { if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
gl.FilteredSearchVisualTokens.moveInputToTheRight(); gl.FilteredSearchVisualTokens.moveInputToTheRight();
this.dropdownManager.resetDropdowns(); this.dropdownManager.resetDropdowns();
}
} }
}
editToken(e) { editToken(e) {
const token = e.target.closest('.js-visual-token'); const token = e.target.closest('.js-visual-token');
if (token) { if (token) {
gl.FilteredSearchVisualTokens.editToken(token); gl.FilteredSearchVisualTokens.editToken(token);
this.tokenChange(); this.tokenChange();
}
} }
}
toggleClearSearchButton() { toggleClearSearchButton() {
const query = gl.DropdownUtils.getSearchQuery(); const query = gl.DropdownUtils.getSearchQuery();
const hidden = 'hidden'; const hidden = 'hidden';
const hasHidden = this.clearSearchButton.classList.contains(hidden); const hasHidden = this.clearSearchButton.classList.contains(hidden);
if (query.length === 0 && !hasHidden) { if (query.length === 0 && !hasHidden) {
this.clearSearchButton.classList.add(hidden); this.clearSearchButton.classList.add(hidden);
} else if (query.length && hasHidden) { } else if (query.length && hasHidden) {
this.clearSearchButton.classList.remove(hidden); this.clearSearchButton.classList.remove(hidden);
}
} }
}
handleInputPlaceholder() { handleInputPlaceholder() {
const query = gl.DropdownUtils.getSearchQuery(); const query = gl.DropdownUtils.getSearchQuery();
const placeholder = 'Search or filter results...'; const placeholder = 'Search or filter results...';
const currentPlaceholder = this.filteredSearchInput.placeholder; const currentPlaceholder = this.filteredSearchInput.placeholder;
if (query.length === 0 && currentPlaceholder !== placeholder) { if (query.length === 0 && currentPlaceholder !== placeholder) {
this.filteredSearchInput.placeholder = placeholder; this.filteredSearchInput.placeholder = placeholder;
} else if (query.length > 0 && currentPlaceholder !== '') { } else if (query.length > 0 && currentPlaceholder !== '') {
this.filteredSearchInput.placeholder = ''; this.filteredSearchInput.placeholder = '';
}
} }
}
removeSelectedToken(e) { removeSelectedToken(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) {
gl.FilteredSearchVisualTokens.removeSelectedToken(); gl.FilteredSearchVisualTokens.removeSelectedToken();
this.handleInputPlaceholder(); this.handleInputPlaceholder();
this.toggleClearSearchButton(); this.toggleClearSearchButton();
}
} }
}
onClearSearch(e) { onClearSearch(e) {
e.preventDefault(); e.preventDefault();
this.clearSearch(); this.clearSearch();
} }
clearSearch() { clearSearch() {
this.filteredSearchInput.value = ''; this.filteredSearchInput.value = '';
const removeElements = []; const removeElements = [];
[].forEach.call(this.tokensContainer.children, (t) => { [].forEach.call(this.tokensContainer.children, (t) => {
if (t.classList.contains('js-visual-token')) { if (t.classList.contains('js-visual-token')) {
removeElements.push(t); removeElements.push(t);
} }
}); });
removeElements.forEach((el) => { removeElements.forEach((el) => {
el.parentElement.removeChild(el); el.parentElement.removeChild(el);
}); });
this.clearSearchButton.classList.add('hidden'); this.clearSearchButton.classList.add('hidden');
this.handleInputPlaceholder(); this.handleInputPlaceholder();
this.dropdownManager.resetDropdowns(); this.dropdownManager.resetDropdowns();
if (this.isHandledAsync) { if (this.isHandledAsync) {
this.search(); this.search();
}
} }
}
handleInputVisualToken() { handleInputVisualToken() {
const input = this.filteredSearchInput; const input = this.filteredSearchInput;
const { tokens, searchToken } const { tokens, searchToken }
= gl.FilteredSearchTokenizer.processTokens(input.value); = gl.FilteredSearchTokenizer.processTokens(input.value);
const { isLastVisualTokenValid } const { isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (isLastVisualTokenValid) { if (isLastVisualTokenValid) {
tokens.forEach((t) => { tokens.forEach((t) => {
input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, ''); input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
gl.FilteredSearchVisualTokens.addFilterVisualToken(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); const fragments = searchToken.split(':');
input.value = input.value.replace(`${tokenKey}:`, ''); if (fragments.length > 1) {
} const inputValues = fragments[0].split(' ');
} else { const tokenKey = inputValues.last();
// 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] === ' ') { if (inputValues.length > 1) {
gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken); inputValues.pop();
const searchTerms = inputValues.join(' ');
// Trim the last space as seen in the if statement above input.value = input.value.replace(searchTerms, '');
input.value = input.value.replace(searchToken, '').trim(); 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;
handleFormSubmit(e) { if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
e.preventDefault(); gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
this.search();
}
saveCurrentSearchQuery() { // Trim the last space as seen in the if statement above
// Don't save before we have fetched the already saved searches input.value = input.value.replace(searchToken, '').trim();
this.fetchingRecentSearchesPromise.then(() => { }
const searchQuery = gl.DropdownUtils.getSearchQuery();
if (searchQuery.length > 0) {
const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
this.recentSearchesService.save(resultantSearches);
}
});
} }
}
loadSearchParamsFromURL() { handleFormSubmit(e) {
const params = gl.utils.getUrlParamsArray(); e.preventDefault();
const usernameParams = this.getUsernameParams(); this.search();
let hasFilteredSearch = false; }
params.forEach((p) => { saveCurrentSearchQuery() {
const split = p.split('='); // Don't save before we have fetched the already saved searches
const keyParam = decodeURIComponent(split[0]); this.fetchingRecentSearchesPromise.then(() => {
const value = split[1]; const searchQuery = gl.DropdownUtils.getSearchQuery();
if (searchQuery.length > 0) {
const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
this.recentSearchesService.save(resultantSearches);
}
});
}
// Check if it matches edge conditions listed in this.filteredSearchTokenKeys loadSearchParamsFromURL() {
const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p); const params = gl.utils.getUrlParamsArray();
const usernameParams = this.getUsernameParams();
let hasFilteredSearch = false;
if (condition) { params.forEach((p) => {
hasFilteredSearch = true; const split = p.split('=');
gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value); const keyParam = decodeURIComponent(split[0]);
} else { const value = split[1];
// Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded + // Check if it matches edge conditions listed in this.filteredSearchTokenKeys
const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
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 ? '"' : '\'';
}
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; hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
} else if (!match && keyParam === 'assignee_id') { }
const id = parseInt(value, 10); } else if (!match && keyParam === 'author_id') {
if (usernameParams[id]) { const id = parseInt(value, 10);
hasFilteredSearch = true; if (usernameParams[id]) {
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; hasFilteredSearch = true;
this.filteredSearchInput.value = sanitizedValue; gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
} }
} else if (!match && keyParam === 'search') {
hasFilteredSearch = true;
this.filteredSearchInput.value = sanitizedValue;
} }
}); }
});
this.saveCurrentSearchQuery(); this.saveCurrentSearchQuery();
if (hasFilteredSearch) { if (hasFilteredSearch) {
this.clearSearchButton.classList.remove('hidden'); this.clearSearchButton.classList.remove('hidden');
this.handleInputPlaceholder(); this.handleInputPlaceholder();
}
} }
}
search() { search() {
const paths = []; const paths = [];
const searchQuery = gl.DropdownUtils.getSearchQuery(); const searchQuery = gl.DropdownUtils.getSearchQuery();
this.saveCurrentSearchQuery();
const { tokens, searchToken } this.saveCurrentSearchQuery();
= this.tokenizer.processTokens(searchQuery);
const currentState = gl.utils.getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
tokens.forEach((token) => { const { tokens, searchToken }
const condition = this.filteredSearchTokenKeys = this.tokenizer.processTokens(searchQuery);
.searchByConditionKeyValue(token.key, token.value.toLowerCase()); const currentState = gl.utils.getParameterByName('state') || 'opened';
const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {}; paths.push(`state=${currentState}`);
const keyParam = param ? `${token.key}_${param}` : token.key;
let tokenPath = '';
if (condition) { tokens.forEach((token) => {
tokenPath = condition.url; const condition = this.filteredSearchTokenKeys
} else { .searchByConditionKeyValue(token.key, token.value.toLowerCase());
let tokenValue = token.value; const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
const keyParam = param ? `${token.key}_${param}` : token.key;
let tokenPath = '';
if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || if (condition) {
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { tokenPath = condition.url;
tokenValue = tokenValue.slice(1, tokenValue.length - 1); } else {
} let tokenValue = token.value;
tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`; if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
tokenValue = tokenValue.slice(1, tokenValue.length - 1);
} }
paths.push(tokenPath); tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
});
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('&')}`; paths.push(tokenPath);
});
if (this.updateObject) { if (searchToken) {
this.updateObject(parameterizedUrl); const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
} else { paths.push(`search=${sanitized}`);
gl.utils.visitUrl(parameterizedUrl);
}
} }
getUsernameParams() { const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`;
const usernamesById = {};
try { if (this.updateObject) {
const attribute = this.filteredSearchInput.getAttribute('data-username-params'); this.updateObject(parameterizedUrl);
JSON.parse(attribute).forEach((user) => { } else {
usernamesById[user.id] = user.username; gl.utils.visitUrl(parameterizedUrl);
});
} catch (e) {
// do nothing
}
return usernamesById;
} }
}
tokenChange() { getUsernameParams() {
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; 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;
}
if (dropdown) { tokenChange() {
const currentDropdownRef = dropdown.reference; const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
this.setDropdownWrapper(); if (dropdown) {
currentDropdownRef.dispatchInputEvent(); const currentDropdownRef = dropdown.reference;
}
}
onrecentSearchesItemSelected(text) { this.setDropdownWrapper();
this.clearSearch(); currentDropdownRef.dispatchInputEvent();
this.filteredSearchInput.value = text;
this.filteredSearchInput.dispatchEvent(new CustomEvent('input'));
this.search();
} }
} }
window.gl = window.gl || {}; onrecentSearchesItemSelected(text) {
gl.FilteredSearchManager = FilteredSearchManager; this.clearSearch();
})(); this.filteredSearchInput.value = text;
this.filteredSearchInput.dispatchEvent(new CustomEvent('input'));
this.search();
}
}
window.gl = window.gl || {};
gl.FilteredSearchManager = FilteredSearchManager;
(() => { const tokenKeys = [{
const tokenKeys = [{ key: 'author',
key: 'author', type: 'string',
type: 'string', param: 'username',
param: 'username', symbol: '@',
symbol: '@', }, {
}, { key: 'assignee',
key: 'assignee', type: 'string',
type: 'string', param: 'username',
param: 'username', symbol: '@',
symbol: '@', }, {
}, { key: 'milestone',
key: 'milestone', type: 'string',
type: 'string', param: 'title',
param: 'title', symbol: '%',
symbol: '%', }, {
}, { key: 'label',
key: 'label', type: 'array',
type: 'array', param: 'name[]',
param: 'name[]', symbol: '~',
symbol: '~', }];
}];
const alternativeTokenKeys = [{ const alternativeTokenKeys = [{
key: 'label', key: 'label',
type: 'string', type: 'string',
param: 'name', param: 'name',
symbol: '~', symbol: '~',
}]; }];
const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys); const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys);
const conditions = [{ const conditions = [{
url: 'assignee_id=0', url: 'assignee_id=0',
tokenKey: 'assignee', tokenKey: 'assignee',
value: 'none', value: 'none',
}, { }, {
url: 'milestone_title=No+Milestone', url: 'milestone_title=No+Milestone',
tokenKey: 'milestone', tokenKey: 'milestone',
value: 'none', value: 'none',
}, { }, {
url: 'milestone_title=%23upcoming', url: 'milestone_title=%23upcoming',
tokenKey: 'milestone', tokenKey: 'milestone',
value: 'upcoming', value: 'upcoming',
}, { }, {
url: 'milestone_title=%23started', url: 'milestone_title=%23started',
tokenKey: 'milestone', tokenKey: 'milestone',
value: 'started', value: 'started',
}, { }, {
url: 'label_name[]=No+Label', url: 'label_name[]=No+Label',
tokenKey: 'label', tokenKey: 'label',
value: 'none', value: 'none',
}]; }];
class FilteredSearchTokenKeys { class FilteredSearchTokenKeys {
static get() { static get() {
return tokenKeys; return tokenKeys;
} }
static getAlternatives() { static getAlternatives() {
return alternativeTokenKeys; return alternativeTokenKeys;
} }
static getConditions() { static getConditions() {
return conditions; return conditions;
} }
static searchByKey(key) { static searchByKey(key) {
return tokenKeys.find(tokenKey => tokenKey.key === key) || null; return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
} }
static searchBySymbol(symbol) { static searchBySymbol(symbol) {
return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
} }
static searchByKeyParam(keyParam) { static searchByKeyParam(keyParam) {
return tokenKeysWithAlternative.find((tokenKey) => { return tokenKeysWithAlternative.find((tokenKey) => {
let tokenKeyParam = tokenKey.key; let tokenKeyParam = tokenKey.key;
if (tokenKey.param) { if (tokenKey.param) {
tokenKeyParam += `_${tokenKey.param}`; tokenKeyParam += `_${tokenKey.param}`;
} }
return keyParam === tokenKeyParam; return keyParam === tokenKeyParam;
}) || null; }) || null;
} }
static searchByConditionUrl(url) { static searchByConditionUrl(url) {
return conditions.find(condition => condition.url === url) || null; return conditions.find(condition => condition.url === url) || null;
} }
static searchByConditionKeyValue(key, value) { static searchByConditionKeyValue(key, value) {
return conditions return conditions
.find(condition => condition.tokenKey === key && condition.value === value) || null; .find(condition => condition.tokenKey === key && condition.value === value) || null;
}
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys; gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
})();
require('./filtered_search_token_keys'); require('./filtered_search_token_keys');
(() => { class FilteredSearchTokenizer {
class FilteredSearchTokenizer { static processTokens(input) {
static processTokens(input) { const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key);
const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key); // Regex extracts `(token):(symbol)(value)`
// Regex extracts `(token):(symbol)(value)` // Values that start with a double quote must end in a double quote (same for single)
// Values that start with a double quote must end in a double quote (same for single) const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g'); const tokens = [];
const tokens = []; const tokenIndexes = []; // stores key+value for simple search
const tokenIndexes = []; // stores key+value for simple search let lastToken = null;
let lastToken = null; const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { let tokenValue = v1 || v2 || v3;
let tokenValue = v1 || v2 || v3; let tokenSymbol = symbol;
let tokenSymbol = symbol; let tokenIndex = '';
let tokenIndex = '';
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { tokenSymbol = tokenValue;
tokenSymbol = tokenValue; tokenValue = '';
tokenValue = '';
}
tokenIndex = `${key}:${tokenValue}`;
// Prevent adding duplicates
if (tokenIndexes.indexOf(tokenIndex) === -1) {
tokenIndexes.push(tokenIndex);
tokens.push({
key,
value: tokenValue || '',
symbol: tokenSymbol || '',
});
}
return '';
}).replace(/\s{2,}/g, ' ').trim() || '';
if (tokens.length > 0) {
const last = tokens[tokens.length - 1];
const lastString = `${last.key}:${last.symbol}${last.value}`;
lastToken = input.lastIndexOf(lastString) ===
input.length - lastString.length ? last : searchToken;
} else {
lastToken = searchToken;
} }
return { tokenIndex = `${key}:${tokenValue}`;
tokens,
lastToken, // Prevent adding duplicates
searchToken, if (tokenIndexes.indexOf(tokenIndex) === -1) {
}; tokenIndexes.push(tokenIndex);
tokens.push({
key,
value: tokenValue || '',
symbol: tokenSymbol || '',
});
}
return '';
}).replace(/\s{2,}/g, ' ').trim() || '';
if (tokens.length > 0) {
const last = tokens[tokens.length - 1];
const lastString = `${last.key}:${last.symbol}${last.value}`;
lastToken = input.lastIndexOf(lastString) ===
input.length - lastString.length ? last : searchToken;
} else {
lastToken = searchToken;
} }
return {
tokens,
lastToken,
searchToken,
};
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.FilteredSearchTokenizer = FilteredSearchTokenizer; gl.FilteredSearchTokenizer = FilteredSearchTokenizer;
})();
/* eslint-disable 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';
/** /**
......
...@@ -197,7 +197,7 @@ class User < ActiveRecord::Base ...@@ -197,7 +197,7 @@ class User < ActiveRecord::Base
scope :admins, -> { where(admin: true) } scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked) } scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) } scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active) } scope :active, -> { with_state(:active).non_internal }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) } scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
......
...@@ -17,4 +17,4 @@ ...@@ -17,4 +17,4 @@
"ci-lint-path" => ci_lint_path } } "ci-lint-path" => ci_lint_path } }
= page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('vue_pipelines') = page_specific_javascript_bundle_tag('pipelines')
---
title: refocus textarea after attaching a file
merge_request:
author:
---
title: Removed orphaned notification settings without a namespace
merge_request:
author:
...@@ -19,12 +19,11 @@ var WEBPACK_REPORT = process.env.WEBPACK_REPORT; ...@@ -19,12 +19,11 @@ var WEBPACK_REPORT = process.env.WEBPACK_REPORT;
var config = { var config = {
context: path.join(ROOT_PATH, 'app/assets/javascripts'), context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: { entry: {
blob: './blob_edit/blob_bundle.js',
boards: './boards/boards_bundle.js',
common: './commons/index.js', common: './commons/index.js',
common_vue: ['vue', './vue_shared/common_vue.js'], common_vue: ['vue', './vue_shared/common_vue.js'],
common_d3: ['d3'], common_d3: ['d3'],
main: './main.js',
blob: './blob_edit/blob_bundle.js',
boards: './boards/boards_bundle.js',
cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js', cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
commit_pipelines: './commit/pipelines/pipelines_bundle.js', commit_pipelines: './commit/pipelines/pipelines_bundle.js',
diff_notes: './diff_notes/diff_notes_bundle.js', diff_notes: './diff_notes/diff_notes_bundle.js',
...@@ -32,26 +31,27 @@ var config = { ...@@ -32,26 +31,27 @@ var config = {
environments_folder: './environments/folder/environments_folder_bundle.js', environments_folder: './environments/folder/environments_folder_bundle.js',
filtered_search: './filtered_search/filtered_search_bundle.js', filtered_search: './filtered_search/filtered_search_bundle.js',
graphs: './graphs/graphs_bundle.js', graphs: './graphs/graphs_bundle.js',
group: './group.js',
groups_list: './groups_list.js', groups_list: './groups_list.js',
issuable: './issuable/issuable_bundle.js', issuable: './issuable/issuable_bundle.js',
issue_show: './issue_show/index.js',
main: './main.js',
merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js', merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
merge_request_widget: './merge_request_widget/ci_bundle.js', merge_request_widget: './merge_request_widget/ci_bundle.js',
monitoring: './monitoring/monitoring_bundle.js', monitoring: './monitoring/monitoring_bundle.js',
network: './network/network_bundle.js', network: './network/network_bundle.js',
notebook_viewer: './blob/notebook_viewer.js', notebook_viewer: './blob/notebook_viewer.js',
sketch_viewer: './blob/sketch_viewer.js',
pdf_viewer: './blob/pdf_viewer.js', pdf_viewer: './blob/pdf_viewer.js',
pipelines: './pipelines/index.js',
profile: './profile/profile_bundle.js', profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js', protected_branches: './protected_branches/protected_branches_bundle.js',
protected_tags: './protected_tags', protected_tags: './protected_tags',
snippet: './snippet/snippet_bundle.js', snippet: './snippet/snippet_bundle.js',
sketch_viewer: './blob/sketch_viewer.js',
stl_viewer: './blob/stl_viewer.js', stl_viewer: './blob/stl_viewer.js',
terminal: './terminal/terminal_bundle.js', terminal: './terminal/terminal_bundle.js',
u2f: ['vendor/u2f'], u2f: ['vendor/u2f'],
users: './users/users_bundle.js', users: './users/users_bundle.js',
vue_pipelines: './vue_pipelines_index/index.js',
issue_show: './issue_show/index.js',
group: './group.js',
}, },
output: { output: {
...@@ -121,11 +121,11 @@ var config = { ...@@ -121,11 +121,11 @@ var config = {
'environments', 'environments',
'environments_folder', 'environments_folder',
'issuable', 'issuable',
'issue_show',
'merge_conflicts', 'merge_conflicts',
'notebook_viewer', 'notebook_viewer',
'pdf_viewer', 'pdf_viewer',
'vue_pipelines', 'pipelines',
'issue_show',
], ],
minChunks: function(module, count) { minChunks: function(module, count) {
return module.resource && (/vue_shared/).test(module.resource); return module.resource && (/vue_shared/).test(module.resource);
......
class DeleteOrphanNotificationSettings < ActiveRecord::Migration
DOWNTIME = false
def up
execute("DELETE FROM notification_settings WHERE EXISTS (SELECT true FROM (#{orphan_notification_settings}) AS ns WHERE ns.id = notification_settings.id)")
end
def down
# This is a no-op method to make the migration reversible.
# If someone is trying to rollback for other reasons, we should not throw an Exception.
# raise ActiveRecord::IrreversibleMigration
end
def orphan_notification_settings
<<-SQL
SELECT notification_settings.id
FROM notification_settings
LEFT OUTER JOIN namespaces
ON namespaces.id = notification_settings.source_id
WHERE notification_settings.source_type = 'Namespace'
AND namespaces.id IS NULL
SQL
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170408033905) do ActiveRecord::Schema.define(version: 20170418103908) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
......
...@@ -48,7 +48,7 @@ module ContainerRegistry ...@@ -48,7 +48,7 @@ module ContainerRegistry
end end
def root_repository? def root_repository?
@path == repository_project.full_path @path == project_path
end end
def repository_project def repository_project
...@@ -60,7 +60,13 @@ module ContainerRegistry ...@@ -60,7 +60,13 @@ module ContainerRegistry
def repository_name def repository_name
return unless has_project? return unless has_project?
@path.remove(%r(^#{Regexp.escape(repository_project.full_path)}/?)) @path.remove(%r(^#{Regexp.escape(project_path)}/?))
end
def project_path
return unless has_project?
repository_project.full_path.downcase
end end
def to_s def to_s
......
...@@ -5,129 +5,127 @@ require('~/diff_notes/models/discussion'); ...@@ -5,129 +5,127 @@ require('~/diff_notes/models/discussion');
require('~/diff_notes/models/note'); require('~/diff_notes/models/note');
require('~/diff_notes/stores/comments'); require('~/diff_notes/stores/comments');
(() => { function createDiscussion(noteId = 1, resolved = true) {
function createDiscussion(noteId = 1, resolved = true) { CommentsStore.create({
CommentsStore.create({ discussionId: 'a',
discussionId: 'a', noteId,
noteId, canResolve: true,
canResolve: true, resolved,
resolved, resolvedBy: 'test',
resolvedBy: 'test', authorName: 'test',
authorName: 'test', authorAvatar: 'test',
authorAvatar: 'test', noteTruncated: 'test...',
noteTruncated: 'test...',
});
}
beforeEach(() => {
CommentsStore.state = {};
}); });
}
describe('New discussion', () => { beforeEach(() => {
it('creates new discussion', () => { CommentsStore.state = {};
expect(Object.keys(CommentsStore.state).length).toBe(0); });
createDiscussion();
expect(Object.keys(CommentsStore.state).length).toBe(1);
});
it('creates new note in discussion', () => { describe('New discussion', () => {
createDiscussion(); it('creates new discussion', () => {
createDiscussion(2); expect(Object.keys(CommentsStore.state).length).toBe(0);
createDiscussion();
expect(Object.keys(CommentsStore.state).length).toBe(1);
});
const discussion = CommentsStore.state['a']; it('creates new note in discussion', () => {
expect(Object.keys(discussion.notes).length).toBe(2); createDiscussion();
}); createDiscussion(2);
const discussion = CommentsStore.state['a'];
expect(Object.keys(discussion.notes).length).toBe(2);
}); });
});
describe('Get note', () => { describe('Get note', () => {
beforeEach(() => { beforeEach(() => {
expect(Object.keys(CommentsStore.state).length).toBe(0); expect(Object.keys(CommentsStore.state).length).toBe(0);
createDiscussion(); createDiscussion();
}); });
it('gets note by ID', () => { it('gets note by ID', () => {
const note = CommentsStore.get('a', 1); const note = CommentsStore.get('a', 1);
expect(note).toBeDefined(); expect(note).toBeDefined();
expect(note.id).toBe(1); expect(note.id).toBe(1);
});
}); });
});
describe('Delete discussion', () => { describe('Delete discussion', () => {
beforeEach(() => { beforeEach(() => {
expect(Object.keys(CommentsStore.state).length).toBe(0); expect(Object.keys(CommentsStore.state).length).toBe(0);
createDiscussion(); createDiscussion();
}); });
it('deletes discussion by ID', () => { it('deletes discussion by ID', () => {
CommentsStore.delete('a', 1); CommentsStore.delete('a', 1);
expect(Object.keys(CommentsStore.state).length).toBe(0); expect(Object.keys(CommentsStore.state).length).toBe(0);
}); });
it('deletes discussion when no more notes', () => { it('deletes discussion when no more notes', () => {
createDiscussion(); createDiscussion();
createDiscussion(2); createDiscussion(2);
expect(Object.keys(CommentsStore.state).length).toBe(1); expect(Object.keys(CommentsStore.state).length).toBe(1);
expect(Object.keys(CommentsStore.state['a'].notes).length).toBe(2); expect(Object.keys(CommentsStore.state['a'].notes).length).toBe(2);
CommentsStore.delete('a', 1); CommentsStore.delete('a', 1);
CommentsStore.delete('a', 2); CommentsStore.delete('a', 2);
expect(Object.keys(CommentsStore.state).length).toBe(0); expect(Object.keys(CommentsStore.state).length).toBe(0);
});
}); });
});
describe('Update note', () => { describe('Update note', () => {
beforeEach(() => { beforeEach(() => {
expect(Object.keys(CommentsStore.state).length).toBe(0); expect(Object.keys(CommentsStore.state).length).toBe(0);
createDiscussion(); createDiscussion();
}); });
it('updates note to be unresolved', () => { it('updates note to be unresolved', () => {
CommentsStore.update('a', 1, false, 'test'); CommentsStore.update('a', 1, false, 'test');
const note = CommentsStore.get('a', 1); const note = CommentsStore.get('a', 1);
expect(note.resolved).toBe(false); expect(note.resolved).toBe(false);
});
}); });
});
describe('Discussion resolved', () => { describe('Discussion resolved', () => {
beforeEach(() => { beforeEach(() => {
expect(Object.keys(CommentsStore.state).length).toBe(0); expect(Object.keys(CommentsStore.state).length).toBe(0);
createDiscussion(); createDiscussion();
}); });
it('is resolved with single note', () => { it('is resolved with single note', () => {
const discussion = CommentsStore.state['a']; const discussion = CommentsStore.state['a'];
expect(discussion.isResolved()).toBe(true); expect(discussion.isResolved()).toBe(true);
}); });
it('is unresolved with 2 notes', () => { it('is unresolved with 2 notes', () => {
const discussion = CommentsStore.state['a']; const discussion = CommentsStore.state['a'];
createDiscussion(2, false); createDiscussion(2, false);
expect(discussion.isResolved()).toBe(false); expect(discussion.isResolved()).toBe(false);
}); });
it('is resolved with 2 notes', () => { it('is resolved with 2 notes', () => {
const discussion = CommentsStore.state['a']; const discussion = CommentsStore.state['a'];
createDiscussion(2); createDiscussion(2);
expect(discussion.isResolved()).toBe(true); expect(discussion.isResolved()).toBe(true);
}); });
it('resolve all notes', () => { it('resolve all notes', () => {
const discussion = CommentsStore.state['a']; const discussion = CommentsStore.state['a'];
createDiscussion(2, false); createDiscussion(2, false);
discussion.resolveAllNotes(); discussion.resolveAllNotes();
expect(discussion.isResolved()).toBe(true); expect(discussion.isResolved()).toBe(true);
}); });
it('unresolve all notes', () => { it('unresolve all notes', () => {
const discussion = CommentsStore.state['a']; const discussion = CommentsStore.state['a'];
createDiscussion(2); createDiscussion(2);
discussion.unResolveAllNotes(); discussion.unResolveAllNotes();
expect(discussion.isResolved()).toBe(false); expect(discussion.isResolved()).toBe(false);
});
}); });
})(); });
...@@ -3,69 +3,67 @@ require('~/filtered_search/filtered_search_tokenizer'); ...@@ -3,69 +3,67 @@ require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown'); require('~/filtered_search/filtered_search_dropdown');
require('~/filtered_search/dropdown_user'); require('~/filtered_search/dropdown_user');
(() => { describe('Dropdown User', () => {
describe('Dropdown User', () => { describe('getSearchInput', () => {
describe('getSearchInput', () => { let dropdownUser;
let dropdownUser;
beforeEach(() => { beforeEach(() => {
spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {});
dropdownUser = new gl.DropdownUser(); dropdownUser = new gl.DropdownUser();
}); });
it('should not return the double quote found in value', () => {
spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
lastToken: '"johnny appleseed',
});
expect(dropdownUser.getSearchInput()).toBe('johnny appleseed'); it('should not return the double quote found in value', () => {
spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
lastToken: '"johnny appleseed',
}); });
it('should not return the single quote found in value', () => { expect(dropdownUser.getSearchInput()).toBe('johnny appleseed');
spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ });
lastToken: '\'larry boy',
});
expect(dropdownUser.getSearchInput()).toBe('larry boy'); it('should not return the single quote found in value', () => {
spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
lastToken: '\'larry boy',
}); });
expect(dropdownUser.getSearchInput()).toBe('larry boy');
}); });
});
describe('config AjaxFilter\'s endpoint', () => { describe('config AjaxFilter\'s endpoint', () => {
beforeEach(() => { beforeEach(() => {
spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
}); });
it('should return endpoint', () => { it('should return endpoint', () => {
window.gon = { window.gon = {
relative_url_root: '', relative_url_root: '',
}; };
const dropdown = new gl.DropdownUser(); const dropdown = new gl.DropdownUser();
expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json');
}); });
it('should return endpoint when relative_url_root is undefined', () => { it('should return endpoint when relative_url_root is undefined', () => {
const dropdown = new gl.DropdownUser(); const dropdown = new gl.DropdownUser();
expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json');
}); });
it('should return endpoint with relative url when available', () => { it('should return endpoint with relative url when available', () => {
window.gon = { window.gon = {
relative_url_root: '/gitlab_directory', relative_url_root: '/gitlab_directory',
}; };
const dropdown = new gl.DropdownUser(); const dropdown = new gl.DropdownUser();
expect(dropdown.config.AjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json'); expect(dropdown.config.AjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json');
}); });
afterEach(() => { afterEach(() => {
window.gon = {}; window.gon = {};
});
}); });
}); });
})(); });
...@@ -3,308 +3,306 @@ require('~/filtered_search/dropdown_utils'); ...@@ -3,308 +3,306 @@ require('~/filtered_search/dropdown_utils');
require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown_manager'); require('~/filtered_search/filtered_search_dropdown_manager');
(() => { describe('Dropdown Utils', () => {
describe('Dropdown Utils', () => { describe('getEscapedText', () => {
describe('getEscapedText', () => { it('should return same word when it has no space', () => {
it('should return same word when it has no space', () => { const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace');
const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace'); expect(escaped).toBe('textWithoutSpace');
expect(escaped).toBe('textWithoutSpace'); });
});
it('should escape with double quotes', () => { it('should escape with double quotes', () => {
let escaped = gl.DropdownUtils.getEscapedText('text with space'); let escaped = gl.DropdownUtils.getEscapedText('text with space');
expect(escaped).toBe('"text with space"'); expect(escaped).toBe('"text with space"');
escaped = gl.DropdownUtils.getEscapedText('won\'t fix'); escaped = gl.DropdownUtils.getEscapedText('won\'t fix');
expect(escaped).toBe('"won\'t fix"'); expect(escaped).toBe('"won\'t fix"');
}); });
it('should escape with single quotes', () => { it('should escape with single quotes', () => {
const escaped = gl.DropdownUtils.getEscapedText('won"t fix'); const escaped = gl.DropdownUtils.getEscapedText('won"t fix');
expect(escaped).toBe('\'won"t fix\''); expect(escaped).toBe('\'won"t fix\'');
}); });
it('should escape with single quotes by default', () => { it('should escape with single quotes by default', () => {
const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix'); const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix');
expect(escaped).toBe('\'won"t\' fix\''); expect(escaped).toBe('\'won"t\' fix\'');
});
}); });
});
describe('filterWithSymbol', () => { describe('filterWithSymbol', () => {
let input; let input;
const item = { const item = {
title: '@root', title: '@root',
}; };
beforeEach(() => { beforeEach(() => {
setFixtures(` setFixtures(`
<input type="text" id="test" /> <input type="text" id="test" />
`); `);
input = document.getElementById('test'); input = document.getElementById('test');
}); });
it('should filter without symbol', () => { it('should filter without symbol', () => {
input.value = 'roo'; input.value = 'roo';
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with symbol', () => { it('should filter with symbol', () => {
input.value = '@roo'; input.value = '@roo';
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
describe('filters multiple word title', () => { describe('filters multiple word title', () => {
const multipleWordItem = { const multipleWordItem = {
title: 'Community Contributions', title: 'Community Contributions',
}; };
it('should filter with double quote', () => { it('should filter with double quote', () => {
input.value = '"'; input.value = '"';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with double quote and symbol', () => { it('should filter with double quote and symbol', () => {
input.value = '~"'; input.value = '~"';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with double quote and multiple words', () => { it('should filter with double quote and multiple words', () => {
input.value = '"community con'; input.value = '"community con';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with double quote, symbol and multiple words', () => { it('should filter with double quote, symbol and multiple words', () => {
input.value = '~"community con'; input.value = '~"community con';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with single quote', () => { it('should filter with single quote', () => {
input.value = '\''; input.value = '\'';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with single quote and symbol', () => { it('should filter with single quote and symbol', () => {
input.value = '~\''; input.value = '~\'';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with single quote and multiple words', () => { it('should filter with single quote and multiple words', () => {
input.value = '\'community con'; input.value = '\'community con';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with single quote, symbol and multiple words', () => { it('should filter with single quote, symbol and multiple words', () => {
input.value = '~\'community con'; input.value = '~\'community con';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
});
}); });
}); });
});
describe('filterHint', () => { describe('filterHint', () => {
let input; let input;
beforeEach(() => {
setFixtures(`
<ul class="tokens-container">
<li class="input-token">
<input class="filtered-search" type="text" id="test" />
</li>
</ul>
`);
input = document.getElementById('test');
});
it('should filter', () => { beforeEach(() => {
input.value = 'l'; setFixtures(`
let updatedItem = gl.DropdownUtils.filterHint(input, { <ul class="tokens-container">
hint: 'label', <li class="input-token">
}); <input class="filtered-search" type="text" id="test" />
expect(updatedItem.droplab_hidden).toBe(false); </li>
</ul>
`);
input.value = 'o'; input = document.getElementById('test');
updatedItem = gl.DropdownUtils.filterHint(input, { });
hint: 'label',
});
expect(updatedItem.droplab_hidden).toBe(true);
});
it('should return droplab_hidden false when item has no hint', () => { it('should filter', () => {
const updatedItem = gl.DropdownUtils.filterHint(input, {}, ''); input.value = 'l';
expect(updatedItem.droplab_hidden).toBe(false); let updatedItem = gl.DropdownUtils.filterHint(input, {
hint: 'label',
}); });
expect(updatedItem.droplab_hidden).toBe(false);
it('should allow multiple if item.type is array', () => { input.value = 'o';
input.value = 'label:~first la'; updatedItem = gl.DropdownUtils.filterHint(input, {
const updatedItem = gl.DropdownUtils.filterHint(input, { hint: 'label',
hint: 'label',
type: 'array',
});
expect(updatedItem.droplab_hidden).toBe(false);
}); });
expect(updatedItem.droplab_hidden).toBe(true);
});
it('should prevent multiple if item.type is not array', () => { it('should return droplab_hidden false when item has no hint', () => {
input.value = 'milestone:~first mile'; const updatedItem = gl.DropdownUtils.filterHint(input, {}, '');
let updatedItem = gl.DropdownUtils.filterHint(input, { expect(updatedItem.droplab_hidden).toBe(false);
hint: 'milestone', });
});
expect(updatedItem.droplab_hidden).toBe(true);
updatedItem = gl.DropdownUtils.filterHint(input, { it('should allow multiple if item.type is array', () => {
hint: 'milestone', input.value = 'label:~first la';
type: 'string', const updatedItem = gl.DropdownUtils.filterHint(input, {
}); hint: 'label',
expect(updatedItem.droplab_hidden).toBe(true); type: 'array',
}); });
expect(updatedItem.droplab_hidden).toBe(false);
}); });
describe('setDataValueIfSelected', () => { it('should prevent multiple if item.type is not array', () => {
beforeEach(() => { input.value = 'milestone:~first mile';
spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput') let updatedItem = gl.DropdownUtils.filterHint(input, {
.and.callFake(() => {}); hint: 'milestone',
}); });
expect(updatedItem.droplab_hidden).toBe(true);
it('calls addWordToInput when dataValue exists', () => { updatedItem = gl.DropdownUtils.filterHint(input, {
const selected = { hint: 'milestone',
getAttribute: () => 'value', type: 'string',
};
gl.DropdownUtils.setDataValueIfSelected(null, selected);
expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1);
}); });
expect(updatedItem.droplab_hidden).toBe(true);
});
});
it('returns true when dataValue exists', () => { describe('setDataValueIfSelected', () => {
const selected = { beforeEach(() => {
getAttribute: () => 'value', spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput')
}; .and.callFake(() => {});
});
const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); it('calls addWordToInput when dataValue exists', () => {
expect(result).toBe(true); const selected = {
}); getAttribute: () => 'value',
};
it('returns false when dataValue does not exist', () => { gl.DropdownUtils.setDataValueIfSelected(null, selected);
const selected = { expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1);
getAttribute: () => null, });
};
const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); it('returns true when dataValue exists', () => {
expect(result).toBe(false); const selected = {
}); getAttribute: () => 'value',
};
const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
expect(result).toBe(true);
}); });
describe('getInputSelectionPosition', () => { it('returns false when dataValue does not exist', () => {
describe('word with trailing spaces', () => { const selected = {
const value = 'label:none '; getAttribute: () => null,
};
const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
expect(result).toBe(false);
});
});
it('should return selectionStart when cursor is at the trailing space', () => { describe('getInputSelectionPosition', () => {
const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ describe('word with trailing spaces', () => {
selectionStart: 11, const value = 'label:none ';
value,
});
expect(left).toBe(11); it('should return selectionStart when cursor is at the trailing space', () => {
expect(right).toBe(11); const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
selectionStart: 11,
value,
}); });
it('should return input when cursor is at the start of input', () => { expect(left).toBe(11);
const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ expect(right).toBe(11);
selectionStart: 0, });
value,
});
expect(left).toBe(0); it('should return input when cursor is at the start of input', () => {
expect(right).toBe(10); const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
selectionStart: 0,
value,
}); });
it('should return input when cursor is at the middle of input', () => { expect(left).toBe(0);
const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ expect(right).toBe(10);
selectionStart: 7, });
value,
});
expect(left).toBe(0); it('should return input when cursor is at the middle of input', () => {
expect(right).toBe(10); const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
selectionStart: 7,
value,
}); });
it('should return input when cursor is at the end of input', () => { expect(left).toBe(0);
const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ expect(right).toBe(10);
selectionStart: 10, });
value,
});
expect(left).toBe(0); it('should return input when cursor is at the end of input', () => {
expect(right).toBe(10); const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
selectionStart: 10,
value,
}); });
});
describe('multiple words', () => { expect(left).toBe(0);
const value = 'label:~"Community Contribution"'; expect(right).toBe(10);
});
});
it('should return input when cursor is after the first word', () => { describe('multiple words', () => {
const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ const value = 'label:~"Community Contribution"';
selectionStart: 17,
value,
});
expect(left).toBe(0); it('should return input when cursor is after the first word', () => {
expect(right).toBe(31); const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
selectionStart: 17,
value,
}); });
it('should return input when cursor is before the second word', () => { expect(left).toBe(0);
const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ expect(right).toBe(31);
selectionStart: 18, });
value,
});
expect(left).toBe(0); it('should return input when cursor is before the second word', () => {
expect(right).toBe(31); const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
selectionStart: 18,
value,
}); });
});
describe('incomplete multiple words', () => { expect(left).toBe(0);
const value = 'label:~"Community Contribution'; expect(right).toBe(31);
});
});
it('should return entire input when cursor is at the start of input', () => { describe('incomplete multiple words', () => {
const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ const value = 'label:~"Community Contribution';
selectionStart: 0,
value,
});
expect(left).toBe(0); it('should return entire input when cursor is at the start of input', () => {
expect(right).toBe(30); const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
selectionStart: 0,
value,
}); });
it('should return entire input when cursor is at the end of input', () => { expect(left).toBe(0);
const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ expect(right).toBe(30);
selectionStart: 30, });
value,
});
expect(left).toBe(0); it('should return entire input when cursor is at the end of input', () => {
expect(right).toBe(30); const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
selectionStart: 30,
value,
}); });
expect(left).toBe(0);
expect(right).toBe(30);
}); });
}); });
}); });
})(); });
...@@ -3,99 +3,97 @@ require('~/filtered_search/filtered_search_visual_tokens'); ...@@ -3,99 +3,97 @@ require('~/filtered_search/filtered_search_visual_tokens');
require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown_manager'); require('~/filtered_search/filtered_search_dropdown_manager');
(() => { describe('Filtered Search Dropdown Manager', () => {
describe('Filtered Search Dropdown Manager', () => { describe('addWordToInput', () => {
describe('addWordToInput', () => { function getInputValue() {
function getInputValue() { return document.querySelector('.filtered-search').value;
return document.querySelector('.filtered-search').value; }
}
function setInputValue(value) {
function setInputValue(value) { document.querySelector('.filtered-search').value = value;
document.querySelector('.filtered-search').value = value; }
}
beforeEach(() => {
beforeEach(() => { setFixtures(`
setFixtures(` <ul class="tokens-container">
<ul class="tokens-container"> <li class="input-token">
<li class="input-token"> <input class="filtered-search">
<input class="filtered-search"> </li>
</li> </ul>
</ul> `);
`); });
});
describe('input has no existing value', () => { describe('input has no existing value', () => {
it('should add just tokenName', () => { it('should add just tokenName', () => {
gl.FilteredSearchDropdownManager.addWordToInput('milestone'); gl.FilteredSearchDropdownManager.addWordToInput('milestone');
const token = document.querySelector('.tokens-container .js-visual-token'); const token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('milestone'); expect(token.querySelector('.name').innerText).toBe('milestone');
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
}); });
it('should add tokenName and tokenValue', () => { it('should add tokenName and tokenValue', () => {
gl.FilteredSearchDropdownManager.addWordToInput('label'); gl.FilteredSearchDropdownManager.addWordToInput('label');
let token = document.querySelector('.tokens-container .js-visual-token'); let token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('label'); expect(token.querySelector('.name').innerText).toBe('label');
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
// We have to get that reference again // We have to get that reference again
// Because gl.FilteredSearchDropdownManager deletes the previous token // Because gl.FilteredSearchDropdownManager deletes the previous token
token = document.querySelector('.tokens-container .js-visual-token'); token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('label'); expect(token.querySelector('.name').innerText).toBe('label');
expect(token.querySelector('.value').innerText).toBe('none'); expect(token.querySelector('.value').innerText).toBe('none');
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
});
}); });
});
describe('input has existing value', () => { describe('input has existing value', () => {
it('should be able to just add tokenName', () => { it('should be able to just add tokenName', () => {
setInputValue('a'); setInputValue('a');
gl.FilteredSearchDropdownManager.addWordToInput('author'); gl.FilteredSearchDropdownManager.addWordToInput('author');
const token = document.querySelector('.tokens-container .js-visual-token'); const token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('author'); expect(token.querySelector('.name').innerText).toBe('author');
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
}); });
it('should replace tokenValue', () => { it('should replace tokenValue', () => {
gl.FilteredSearchDropdownManager.addWordToInput('author'); gl.FilteredSearchDropdownManager.addWordToInput('author');
setInputValue('roo'); setInputValue('roo');
gl.FilteredSearchDropdownManager.addWordToInput(null, '@root'); gl.FilteredSearchDropdownManager.addWordToInput(null, '@root');
const token = document.querySelector('.tokens-container .js-visual-token'); const token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('author'); expect(token.querySelector('.name').innerText).toBe('author');
expect(token.querySelector('.value').innerText).toBe('@root'); expect(token.querySelector('.value').innerText).toBe('@root');
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
}); });
it('should add tokenValues containing spaces', () => { it('should add tokenValues containing spaces', () => {
gl.FilteredSearchDropdownManager.addWordToInput('label'); gl.FilteredSearchDropdownManager.addWordToInput('label');
setInputValue('"test '); setInputValue('"test ');
gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\'');
const token = document.querySelector('.tokens-container .js-visual-token'); const token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('label'); expect(token.querySelector('.name').innerText).toBe('label');
expect(token.querySelector('.value').innerText).toBe('~\'"test me"\''); expect(token.querySelector('.value').innerText).toBe('~\'"test me"\'');
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
});
}); });
}); });
}); });
})(); });
...@@ -6,271 +6,269 @@ require('~/filtered_search/filtered_search_dropdown_manager'); ...@@ -6,271 +6,269 @@ require('~/filtered_search/filtered_search_dropdown_manager');
require('~/filtered_search/filtered_search_manager'); require('~/filtered_search/filtered_search_manager');
const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper'); const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper');
(() => { describe('Filtered Search Manager', () => {
describe('Filtered Search Manager', () => { let input;
let input; let manager;
let manager; let tokensContainer;
let tokensContainer; const placeholder = 'Search or filter results...';
const placeholder = 'Search or filter results...';
function dispatchBackspaceEvent(element, eventType) {
function dispatchBackspaceEvent(element, eventType) { const backspaceKey = 8;
const backspaceKey = 8; const event = new Event(eventType);
const event = new Event(eventType); event.keyCode = backspaceKey;
event.keyCode = backspaceKey; element.dispatchEvent(event);
element.dispatchEvent(event); }
}
function dispatchDeleteEvent(element, eventType) {
const deleteKey = 46;
const event = new Event(eventType);
event.keyCode = deleteKey;
element.dispatchEvent(event);
}
beforeEach(() => {
setFixtures(`
<div class="filtered-search-box">
<form>
<ul class="tokens-container list-unstyled">
${FilteredSearchSpecHelper.createInputHTML(placeholder)}
</ul>
<button class="clear-search" type="button">
<i class="fa fa-times"></i>
</button>
</form>
</div>
`);
spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {});
spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {});
spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough();
input = document.querySelector('.filtered-search');
tokensContainer = document.querySelector('.tokens-container');
manager = new gl.FilteredSearchManager();
});
function dispatchDeleteEvent(element, eventType) { afterEach(() => {
const deleteKey = 46; manager.cleanup();
const event = new Event(eventType); });
event.keyCode = deleteKey;
element.dispatchEvent(event);
}
beforeEach(() => { describe('search', () => {
setFixtures(` const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
<div class="filtered-search-box">
<form>
<ul class="tokens-container list-unstyled">
${FilteredSearchSpecHelper.createInputHTML(placeholder)}
</ul>
<button class="clear-search" type="button">
<i class="fa fa-times"></i>
</button>
</form>
</div>
`);
spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); it('should search with a single word', (done) => {
spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); input.value = 'searchTerm';
spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {});
spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough();
input = document.querySelector('.filtered-search'); spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
tokensContainer = document.querySelector('.tokens-container'); expect(url).toEqual(`${defaultParams}&search=searchTerm`);
manager = new gl.FilteredSearchManager(); done();
}); });
afterEach(() => { manager.search();
manager.cleanup();
}); });
describe('search', () => { it('should search with multiple words', (done) => {
const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened'; input.value = 'awesome search terms';
it('should search with a single word', (done) => {
input.value = 'searchTerm';
spyOn(gl.utils, 'visitUrl').and.callFake((url) => { spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=searchTerm`); expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
done(); done();
});
manager.search();
}); });
it('should search with multiple words', (done) => { manager.search();
input.value = 'awesome search terms'; });
spyOn(gl.utils, 'visitUrl').and.callFake((url) => { it('should search with special characters', (done) => {
expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`); input.value = '~!@#$%^&*()_+{}:<>,.?/';
done();
});
manager.search(); spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
done();
}); });
it('should search with special characters', (done) => { manager.search();
input.value = '~!@#$%^&*()_+{}:<>,.?/'; });
spyOn(gl.utils, 'visitUrl').and.callFake((url) => { it('removes duplicated tokens', (done) => {
expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`); tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
done(); ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
}); ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
`);
manager.search(); spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
done();
}); });
it('removes duplicated tokens', (done) => { manager.search();
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` });
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} });
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
`);
spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
done();
});
manager.search(); describe('handleInputPlaceholder', () => {
}); it('should render placeholder when there is no input', () => {
expect(input.placeholder).toEqual(placeholder);
}); });
describe('handleInputPlaceholder', () => { it('should not render placeholder when there is input', () => {
it('should render placeholder when there is no input', () => { input.value = 'test words';
expect(input.placeholder).toEqual(placeholder);
}); const event = new Event('input');
input.dispatchEvent(event);
it('should not render placeholder when there is input', () => { expect(input.placeholder).toEqual('');
input.value = 'test words'; });
const event = new Event('input'); it('should not render placeholder when there are tokens and no input', () => {
input.dispatchEvent(event); tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
);
expect(input.placeholder).toEqual(''); const event = new Event('input');
}); input.dispatchEvent(event);
it('should not render placeholder when there are tokens and no input', () => { expect(input.placeholder).toEqual('');
});
});
describe('checkForBackspace', () => {
describe('tokens and no input', () => {
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
); );
const event = new Event('input');
input.dispatchEvent(event);
expect(input.placeholder).toEqual('');
}); });
});
describe('checkForBackspace', () => {
describe('tokens and no input', () => {
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
);
});
it('removes last token', () => { it('removes last token', () => {
spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
dispatchBackspaceEvent(input, 'keyup'); dispatchBackspaceEvent(input, 'keyup');
expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
});
it('sets the input', () => {
spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
dispatchDeleteEvent(input, 'keyup');
expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled(); expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
expect(input.value).toEqual('~bug');
});
}); });
it('does not remove token or change input when there is existing input', () => { it('sets the input', () => {
spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
input.value = 'text';
dispatchDeleteEvent(input, 'keyup'); dispatchDeleteEvent(input, 'keyup');
expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled();
expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); expect(input.value).toEqual('~bug');
expect(input.value).toEqual('text');
}); });
}); });
describe('removeSelectedToken', () => { it('does not remove token or change input when there is existing input', () => {
function getVisualTokens() { spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
return tokensContainer.querySelectorAll('.js-visual-token'); spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
}
beforeEach(() => { input.value = 'text';
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( dispatchDeleteEvent(input, 'keyup');
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
);
});
it('removes selected token when the backspace key is pressed', () => { expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
expect(getVisualTokens().length).toEqual(1); expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
expect(input.value).toEqual('text');
});
});
dispatchBackspaceEvent(document, 'keydown'); describe('removeSelectedToken', () => {
function getVisualTokens() {
return tokensContainer.querySelectorAll('.js-visual-token');
}
expect(getVisualTokens().length).toEqual(0); beforeEach(() => {
}); tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
);
});
it('removes selected token when the delete key is pressed', () => { it('removes selected token when the backspace key is pressed', () => {
expect(getVisualTokens().length).toEqual(1); expect(getVisualTokens().length).toEqual(1);
dispatchDeleteEvent(document, 'keydown'); dispatchBackspaceEvent(document, 'keydown');
expect(getVisualTokens().length).toEqual(0); expect(getVisualTokens().length).toEqual(0);
}); });
it('updates the input placeholder after removal', () => { it('removes selected token when the delete key is pressed', () => {
manager.handleInputPlaceholder(); expect(getVisualTokens().length).toEqual(1);
expect(input.placeholder).toEqual(''); dispatchDeleteEvent(document, 'keydown');
expect(getVisualTokens().length).toEqual(1);
dispatchBackspaceEvent(document, 'keydown'); expect(getVisualTokens().length).toEqual(0);
});
expect(input.placeholder).not.toEqual(''); it('updates the input placeholder after removal', () => {
expect(getVisualTokens().length).toEqual(0); manager.handleInputPlaceholder();
});
it('updates the clear button after removal', () => { expect(input.placeholder).toEqual('');
manager.toggleClearSearchButton(); expect(getVisualTokens().length).toEqual(1);
const clearButton = document.querySelector('.clear-search'); dispatchBackspaceEvent(document, 'keydown');
expect(clearButton.classList.contains('hidden')).toEqual(false); expect(input.placeholder).not.toEqual('');
expect(getVisualTokens().length).toEqual(1); expect(getVisualTokens().length).toEqual(0);
});
dispatchBackspaceEvent(document, 'keydown'); it('updates the clear button after removal', () => {
manager.toggleClearSearchButton();
expect(clearButton.classList.contains('hidden')).toEqual(true); const clearButton = document.querySelector('.clear-search');
expect(getVisualTokens().length).toEqual(0);
}); expect(clearButton.classList.contains('hidden')).toEqual(false);
expect(getVisualTokens().length).toEqual(1);
dispatchBackspaceEvent(document, 'keydown');
expect(clearButton.classList.contains('hidden')).toEqual(true);
expect(getVisualTokens().length).toEqual(0);
}); });
});
describe('unselects token', () => { describe('unselects token', () => {
beforeEach(() => { beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)} ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')} ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
`); `);
}); });
it('unselects token when input is clicked', () => { it('unselects token when input is clicked', () => {
const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
expect(selectedToken.classList.contains('selected')).toEqual(true); expect(selectedToken.classList.contains('selected')).toEqual(true);
expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
// Click directly on input attached to document // Click directly on input attached to document
// so that the click event will propagate properly // so that the click event will propagate properly
document.querySelector('.filtered-search').click(); document.querySelector('.filtered-search').click();
expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
expect(selectedToken.classList.contains('selected')).toEqual(false); expect(selectedToken.classList.contains('selected')).toEqual(false);
}); });
it('unselects token when document.body is clicked', () => { it('unselects token when document.body is clicked', () => {
const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
expect(selectedToken.classList.contains('selected')).toEqual(true); expect(selectedToken.classList.contains('selected')).toEqual(true);
expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
document.body.click(); document.body.click();
expect(selectedToken.classList.contains('selected')).toEqual(false); expect(selectedToken.classList.contains('selected')).toEqual(false);
expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
});
}); });
});
describe('toggleInputContainerFocus', () => { describe('toggleInputContainerFocus', () => {
it('toggles on focus', () => { it('toggles on focus', () => {
input.focus(); input.focus();
expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(true); expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(true);
}); });
it('toggles on blur', () => { it('toggles on blur', () => {
input.blur(); input.blur();
expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(false); expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(false);
});
}); });
}); });
})(); });
require('~/extensions/array'); require('~/extensions/array');
require('~/filtered_search/filtered_search_token_keys'); require('~/filtered_search/filtered_search_token_keys');
(() => { describe('Filtered Search Token Keys', () => {
describe('Filtered Search Token Keys', () => { describe('get', () => {
describe('get', () => { let tokenKeys;
let tokenKeys;
beforeEach(() => {
beforeEach(() => { tokenKeys = gl.FilteredSearchTokenKeys.get();
tokenKeys = gl.FilteredSearchTokenKeys.get(); });
});
it('should return tokenKeys', () => {
it('should return tokenKeys', () => { expect(tokenKeys !== null).toBe(true);
expect(tokenKeys !== null).toBe(true); });
});
it('should return tokenKeys as an array', () => {
it('should return tokenKeys as an array', () => { expect(tokenKeys instanceof Array).toBe(true);
expect(tokenKeys instanceof Array).toBe(true); });
}); });
});
describe('getConditions', () => {
describe('getConditions', () => { let conditions;
let conditions;
beforeEach(() => {
beforeEach(() => { conditions = gl.FilteredSearchTokenKeys.getConditions();
conditions = gl.FilteredSearchTokenKeys.getConditions(); });
});
it('should return conditions', () => {
it('should return conditions', () => { expect(conditions !== null).toBe(true);
expect(conditions !== null).toBe(true); });
});
it('should return conditions as an array', () => {
it('should return conditions as an array', () => { expect(conditions instanceof Array).toBe(true);
expect(conditions instanceof Array).toBe(true); });
}); });
});
describe('searchByKey', () => {
describe('searchByKey', () => { it('should return null when key not found', () => {
it('should return null when key not found', () => { const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey');
const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey'); expect(tokenKey === null).toBe(true);
expect(tokenKey === null).toBe(true); });
});
it('should return tokenKey when found by key', () => {
it('should return tokenKey when found by key', () => { const tokenKeys = gl.FilteredSearchTokenKeys.get();
const tokenKeys = gl.FilteredSearchTokenKeys.get(); const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key);
const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key); expect(result).toEqual(tokenKeys[0]);
expect(result).toEqual(tokenKeys[0]); });
}); });
});
describe('searchBySymbol', () => {
describe('searchBySymbol', () => { it('should return null when symbol not found', () => {
it('should return null when symbol not found', () => { const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol');
const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol'); expect(tokenKey === null).toBe(true);
expect(tokenKey === null).toBe(true); });
});
it('should return tokenKey when found by symbol', () => {
it('should return tokenKey when found by symbol', () => { const tokenKeys = gl.FilteredSearchTokenKeys.get();
const tokenKeys = gl.FilteredSearchTokenKeys.get(); const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol);
const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol); expect(result).toEqual(tokenKeys[0]);
expect(result).toEqual(tokenKeys[0]); });
}); });
});
describe('searchByKeyParam', () => {
describe('searchByKeyParam', () => { it('should return null when key param not found', () => {
it('should return null when key param not found', () => { const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam');
const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam'); expect(tokenKey === null).toBe(true);
expect(tokenKey === null).toBe(true); });
});
it('should return tokenKey when found by key param', () => {
it('should return tokenKey when found by key param', () => { const tokenKeys = gl.FilteredSearchTokenKeys.get();
const tokenKeys = gl.FilteredSearchTokenKeys.get(); const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); expect(result).toEqual(tokenKeys[0]);
expect(result).toEqual(tokenKeys[0]); });
});
it('should return alternative tokenKey when found by key param', () => {
it('should return alternative tokenKey when found by key param', () => { const tokenKeys = gl.FilteredSearchTokenKeys.getAlternatives();
const tokenKeys = gl.FilteredSearchTokenKeys.getAlternatives(); const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); expect(result).toEqual(tokenKeys[0]);
expect(result).toEqual(tokenKeys[0]); });
}); });
});
describe('searchByConditionUrl', () => {
describe('searchByConditionUrl', () => { it('should return null when condition url not found', () => {
it('should return null when condition url not found', () => { const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null);
const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null); expect(condition === null).toBe(true);
expect(condition === null).toBe(true); });
});
it('should return condition when found by url', () => {
it('should return condition when found by url', () => { const conditions = gl.FilteredSearchTokenKeys.getConditions();
const conditions = gl.FilteredSearchTokenKeys.getConditions(); const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url);
const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url); expect(result).toBe(conditions[0]);
expect(result).toBe(conditions[0]); });
}); });
});
describe('searchByConditionKeyValue', () => {
describe('searchByConditionKeyValue', () => { it('should return null when condition tokenKey and value not found', () => {
it('should return null when condition tokenKey and value not found', () => { const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null);
const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null); expect(condition === null).toBe(true);
expect(condition === null).toBe(true); });
});
it('should return condition when found by tokenKey and value', () => {
it('should return condition when found by tokenKey and value', () => { const conditions = gl.FilteredSearchTokenKeys.getConditions();
const conditions = gl.FilteredSearchTokenKeys.getConditions(); const result = gl.FilteredSearchTokenKeys
const result = gl.FilteredSearchTokenKeys .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value);
.searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value); expect(result).toEqual(conditions[0]);
expect(result).toEqual(conditions[0]);
});
}); });
}); });
})(); });
...@@ -2,134 +2,132 @@ require('~/extensions/array'); ...@@ -2,134 +2,132 @@ require('~/extensions/array');
require('~/filtered_search/filtered_search_token_keys'); require('~/filtered_search/filtered_search_token_keys');
require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_tokenizer');
(() => { describe('Filtered Search Tokenizer', () => {
describe('Filtered Search Tokenizer', () => { describe('processTokens', () => {
describe('processTokens', () => { it('returns for input containing only search value', () => {
it('returns for input containing only search value', () => { const results = gl.FilteredSearchTokenizer.processTokens('searchTerm');
const results = gl.FilteredSearchTokenizer.processTokens('searchTerm'); expect(results.searchToken).toBe('searchTerm');
expect(results.searchToken).toBe('searchTerm'); expect(results.tokens.length).toBe(0);
expect(results.tokens.length).toBe(0); expect(results.lastToken).toBe(results.searchToken);
expect(results.lastToken).toBe(results.searchToken); });
});
it('returns for input containing only tokens', () => {
it('returns for input containing only tokens', () => { const results = gl.FilteredSearchTokenizer
const results = gl.FilteredSearchTokenizer .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none');
.processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none'); expect(results.searchToken).toBe('');
expect(results.searchToken).toBe(''); expect(results.tokens.length).toBe(4);
expect(results.tokens.length).toBe(4); expect(results.tokens[3]).toBe(results.lastToken);
expect(results.tokens[3]).toBe(results.lastToken);
expect(results.tokens[0].key).toBe('author');
expect(results.tokens[0].key).toBe('author'); expect(results.tokens[0].value).toBe('root');
expect(results.tokens[0].value).toBe('root'); expect(results.tokens[0].symbol).toBe('@');
expect(results.tokens[0].symbol).toBe('@');
expect(results.tokens[1].key).toBe('label');
expect(results.tokens[1].key).toBe('label'); expect(results.tokens[1].value).toBe('"Very Important"');
expect(results.tokens[1].value).toBe('"Very Important"'); expect(results.tokens[1].symbol).toBe('~');
expect(results.tokens[1].symbol).toBe('~');
expect(results.tokens[2].key).toBe('milestone');
expect(results.tokens[2].key).toBe('milestone'); expect(results.tokens[2].value).toBe('v1.0');
expect(results.tokens[2].value).toBe('v1.0'); expect(results.tokens[2].symbol).toBe('%');
expect(results.tokens[2].symbol).toBe('%');
expect(results.tokens[3].key).toBe('assignee');
expect(results.tokens[3].key).toBe('assignee'); expect(results.tokens[3].value).toBe('none');
expect(results.tokens[3].value).toBe('none'); expect(results.tokens[3].symbol).toBe('');
expect(results.tokens[3].symbol).toBe(''); });
});
it('returns for input starting with search value and ending with tokens', () => {
it('returns for input starting with search value and ending with tokens', () => { const results = gl.FilteredSearchTokenizer
const results = gl.FilteredSearchTokenizer .processTokens('searchTerm anotherSearchTerm milestone:none');
.processTokens('searchTerm anotherSearchTerm milestone:none'); expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); expect(results.tokens.length).toBe(1);
expect(results.tokens.length).toBe(1); expect(results.tokens[0]).toBe(results.lastToken);
expect(results.tokens[0]).toBe(results.lastToken); expect(results.tokens[0].key).toBe('milestone');
expect(results.tokens[0].key).toBe('milestone'); expect(results.tokens[0].value).toBe('none');
expect(results.tokens[0].value).toBe('none'); expect(results.tokens[0].symbol).toBe('');
expect(results.tokens[0].symbol).toBe(''); });
});
it('returns for input starting with tokens and ending with search value', () => {
it('returns for input starting with tokens and ending with search value', () => { const results = gl.FilteredSearchTokenizer
const results = gl.FilteredSearchTokenizer .processTokens('assignee:@user searchTerm');
.processTokens('assignee:@user searchTerm');
expect(results.searchToken).toBe('searchTerm');
expect(results.searchToken).toBe('searchTerm'); expect(results.tokens.length).toBe(1);
expect(results.tokens.length).toBe(1); expect(results.tokens[0].key).toBe('assignee');
expect(results.tokens[0].key).toBe('assignee'); expect(results.tokens[0].value).toBe('user');
expect(results.tokens[0].value).toBe('user'); expect(results.tokens[0].symbol).toBe('@');
expect(results.tokens[0].symbol).toBe('@'); expect(results.lastToken).toBe(results.searchToken);
expect(results.lastToken).toBe(results.searchToken); });
});
it('returns for input containing search value wrapped between tokens', () => {
it('returns for input containing search value wrapped between tokens', () => { const results = gl.FilteredSearchTokenizer
const results = gl.FilteredSearchTokenizer .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none');
.processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none');
expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); expect(results.tokens.length).toBe(3);
expect(results.tokens.length).toBe(3); expect(results.tokens[2]).toBe(results.lastToken);
expect(results.tokens[2]).toBe(results.lastToken);
expect(results.tokens[0].key).toBe('author');
expect(results.tokens[0].key).toBe('author'); expect(results.tokens[0].value).toBe('root');
expect(results.tokens[0].value).toBe('root'); expect(results.tokens[0].symbol).toBe('@');
expect(results.tokens[0].symbol).toBe('@');
expect(results.tokens[1].key).toBe('label');
expect(results.tokens[1].key).toBe('label'); expect(results.tokens[1].value).toBe('"Won\'t fix"');
expect(results.tokens[1].value).toBe('"Won\'t fix"'); expect(results.tokens[1].symbol).toBe('~');
expect(results.tokens[1].symbol).toBe('~');
expect(results.tokens[2].key).toBe('milestone');
expect(results.tokens[2].key).toBe('milestone'); expect(results.tokens[2].value).toBe('none');
expect(results.tokens[2].value).toBe('none'); expect(results.tokens[2].symbol).toBe('');
expect(results.tokens[2].symbol).toBe(''); });
});
it('returns for input containing search value in between tokens', () => {
it('returns for input containing search value in between tokens', () => { const results = gl.FilteredSearchTokenizer
const results = gl.FilteredSearchTokenizer .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing');
.processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing'); expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); expect(results.tokens.length).toBe(3);
expect(results.tokens.length).toBe(3); expect(results.tokens[2]).toBe(results.lastToken);
expect(results.tokens[2]).toBe(results.lastToken);
expect(results.tokens[0].key).toBe('author');
expect(results.tokens[0].key).toBe('author'); expect(results.tokens[0].value).toBe('root');
expect(results.tokens[0].value).toBe('root'); expect(results.tokens[0].symbol).toBe('@');
expect(results.tokens[0].symbol).toBe('@');
expect(results.tokens[1].key).toBe('assignee');
expect(results.tokens[1].key).toBe('assignee'); expect(results.tokens[1].value).toBe('none');
expect(results.tokens[1].value).toBe('none'); expect(results.tokens[1].symbol).toBe('');
expect(results.tokens[1].symbol).toBe('');
expect(results.tokens[2].key).toBe('label');
expect(results.tokens[2].key).toBe('label'); expect(results.tokens[2].value).toBe('Doing');
expect(results.tokens[2].value).toBe('Doing'); expect(results.tokens[2].symbol).toBe('~');
expect(results.tokens[2].symbol).toBe('~'); });
});
it('returns search value for invalid tokens', () => {
it('returns search value for invalid tokens', () => { const results = gl.FilteredSearchTokenizer.processTokens('fake:token');
const results = gl.FilteredSearchTokenizer.processTokens('fake:token'); expect(results.lastToken).toBe('fake:token');
expect(results.lastToken).toBe('fake:token'); expect(results.searchToken).toBe('fake:token');
expect(results.searchToken).toBe('fake:token'); expect(results.tokens.length).toEqual(0);
expect(results.tokens.length).toEqual(0); });
});
it('returns search value and token for mix of valid and invalid tokens', () => {
it('returns search value and token for mix of valid and invalid tokens', () => { const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token');
const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token'); expect(results.tokens.length).toEqual(1);
expect(results.tokens.length).toEqual(1); expect(results.tokens[0].key).toBe('label');
expect(results.tokens[0].key).toBe('label'); expect(results.tokens[0].value).toBe('real');
expect(results.tokens[0].value).toBe('real'); expect(results.tokens[0].symbol).toBe('');
expect(results.tokens[0].symbol).toBe(''); expect(results.lastToken).toBe('fake:token');
expect(results.lastToken).toBe('fake:token'); expect(results.searchToken).toBe('fake:token');
expect(results.searchToken).toBe('fake:token'); });
});
it('returns search value for invalid symbols', () => {
it('returns search value for invalid symbols', () => { const results = gl.FilteredSearchTokenizer.processTokens('std::includes');
const results = gl.FilteredSearchTokenizer.processTokens('std::includes'); expect(results.lastToken).toBe('std::includes');
expect(results.lastToken).toBe('std::includes'); expect(results.searchToken).toBe('std::includes');
expect(results.searchToken).toBe('std::includes'); });
});
it('removes duplicated values', () => {
it('removes duplicated values', () => { const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo');
const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo'); expect(results.tokens.length).toBe(1);
expect(results.tokens.length).toBe(1); expect(results.tokens[0].key).toBe('label');
expect(results.tokens[0].key).toBe('label'); expect(results.tokens[0].value).toBe('foo');
expect(results.tokens[0].value).toBe('foo'); expect(results.tokens[0].symbol).toBe('~');
expect(results.tokens[0].symbol).toBe('~');
});
}); });
}); });
})(); });
import Vue from 'vue'; import Vue from 'vue';
import asyncButtonComp from '~/vue_pipelines_index/components/async_button.vue'; import asyncButtonComp from '~/pipelines/components/async_button.vue';
describe('Pipelines Async Button', () => { describe('Pipelines Async Button', () => {
let component; let component;
......
import Vue from 'vue'; import Vue from 'vue';
import emptyStateComp from '~/vue_pipelines_index/components/empty_state.vue'; import emptyStateComp from '~/pipelines/components/empty_state.vue';
describe('Pipelines Empty State', () => { describe('Pipelines Empty State', () => {
let component; let component;
......
import Vue from 'vue'; import Vue from 'vue';
import errorStateComp from '~/vue_pipelines_index/components/error_state.vue'; import errorStateComp from '~/pipelines/components/error_state.vue';
describe('Pipelines Error State', () => { describe('Pipelines Error State', () => {
let component; let component;
......
import Vue from 'vue'; import Vue from 'vue';
import navControlsComp from '~/vue_pipelines_index/components/nav_controls'; import navControlsComp from '~/pipelines/components/nav_controls';
describe('Pipelines Nav Controls', () => { describe('Pipelines Nav Controls', () => {
let NavControlsComponent; let NavControlsComponent;
......
import Vue from 'vue'; import Vue from 'vue';
import pipelineUrlComp from '~/vue_pipelines_index/components/pipeline_url'; import pipelineUrlComp from '~/pipelines/components/pipeline_url';
describe('Pipeline Url Component', () => { describe('Pipeline Url Component', () => {
let PipelineUrlComponent; let PipelineUrlComponent;
......
import Vue from 'vue'; import Vue from 'vue';
import pipelinesActionsComp from '~/vue_pipelines_index/components/pipelines_actions'; import pipelinesActionsComp from '~/pipelines/components/pipelines_actions';
describe('Pipelines Actions dropdown', () => { describe('Pipelines Actions dropdown', () => {
let component; let component;
......
import Vue from 'vue'; import Vue from 'vue';
import artifactsComp from '~/vue_pipelines_index/components/pipelines_artifacts'; import artifactsComp from '~/pipelines/components/pipelines_artifacts';
describe('Pipelines Artifacts dropdown', () => { describe('Pipelines Artifacts dropdown', () => {
let component; let component;
......
import Vue from 'vue'; import Vue from 'vue';
import pipelinesComp from '~/vue_pipelines_index/pipelines'; import pipelinesComp from '~/pipelines/pipelines';
import Store from '~/vue_pipelines_index/stores/pipelines_store'; import Store from '~/pipelines/stores/pipelines_store';
import pipelinesData from './mock_data'; import pipelinesData from './mock_data';
describe('Pipelines', () => { describe('Pipelines', () => {
......
import PipelineStore from '~/vue_pipelines_index/stores/pipelines_store'; import PipelineStore from '~/pipelines/stores/pipelines_store';
describe('Pipelines Store', () => { describe('Pipelines Store', () => {
let store; let store;
......
import Vue from 'vue'; import Vue from 'vue';
import { SUCCESS_SVG } from '~/ci_status_icons'; import { SUCCESS_SVG } from '~/ci_status_icons';
import Stage from '~/vue_pipelines_index/components/stage'; import Stage from '~/pipelines/components/stage';
function minify(string) { function minify(string) {
return string.replace(/\s/g, ''); return string.replace(/\s/g, '');
......
...@@ -189,15 +189,10 @@ describe ContainerRegistry::Path do ...@@ -189,15 +189,10 @@ describe ContainerRegistry::Path do
end end
context 'when project exists' do context 'when project exists' do
let(:group) { create(:group, path: 'some_group') } let(:group) { create(:group, path: 'Some_Group') }
let(:project) do
create(:empty_project, group: group, name: 'some_project')
end
before do before do
allow(path).to receive(:repository_project) create(:empty_project, group: group, name: 'some_project')
.and_return(project)
end end
context 'when project path equal repository path' do context 'when project path equal repository path' do
...@@ -225,4 +220,27 @@ describe ContainerRegistry::Path do ...@@ -225,4 +220,27 @@ describe ContainerRegistry::Path do
end end
end end
end end
describe '#project_path' do
context 'when project does not exist' do
let(:path) { 'some/name' }
it 'returns nil' do
expect(subject.project_path).to be_nil
end
end
context 'when project with uppercase characters in path exists' do
let(:path) { 'somegroup/myproject/my/image' }
let(:group) { create(:group, path: 'SomeGroup') }
before do
create(:empty_project, group: group, name: 'MyProject')
end
it 'returns downcased project path' do
expect(subject.project_path).to eq 'somegroup/myproject'
end
end
end
end end
...@@ -1631,4 +1631,16 @@ describe User, models: true do ...@@ -1631,4 +1631,16 @@ describe User, models: true do
end end
end end
end end
context '.active' do
before do
User.ghost
create(:user, name: 'user', state: 'active')
create(:user, name: 'user', state: 'blocked')
end
it 'only counts active and non internal users' do
expect(User.active.count).to eq(1)
end
end
end end
...@@ -7,6 +7,7 @@ describe Groups::DestroyService, services: true do ...@@ -7,6 +7,7 @@ describe Groups::DestroyService, services: true do
let!(:group) { create(:group) } let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) } let!(:nested_group) { create(:group, parent: group) }
let!(:project) { create(:empty_project, namespace: group) } let!(:project) { create(:empty_project, namespace: group) }
let!(:notification_setting) { create(:notification_setting, source: group)}
let!(:gitlab_shell) { Gitlab::Shell.new } let!(:gitlab_shell) { Gitlab::Shell.new }
let!(:remove_path) { group.path + "+#{group.id}+deleted" } let!(:remove_path) { group.path + "+#{group.id}+deleted" }
...@@ -23,6 +24,7 @@ describe Groups::DestroyService, services: true do ...@@ -23,6 +24,7 @@ describe Groups::DestroyService, services: true do
it { expect(Group.unscoped.all).not_to include(group) } it { expect(Group.unscoped.all).not_to include(group) }
it { expect(Group.unscoped.all).not_to include(nested_group) } it { expect(Group.unscoped.all).not_to include(nested_group) }
it { expect(Project.unscoped.all).not_to include(project) } it { expect(Project.unscoped.all).not_to include(project) }
it { expect(NotificationSetting.unscoped.all).not_to include(notification_setting) }
end end
context 'file system' do context 'file system' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment