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);
})();
...@@ -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;
})();
(() => { const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
class FilteredSearchDropdown {
class FilteredSearchDropdown { constructor(droplab, dropdown, input, filter) {
constructor(droplab, dropdown, input, filter) { this.droplab = droplab;
this.droplab = droplab; this.hookId = input && input.id;
this.hookId = input && input.id; this.input = input;
this.input = input; this.filter = filter;
this.filter = filter; this.dropdown = dropdown;
this.dropdown = dropdown; this.loadingTemplate = `<div class="filter-dropdown-loading">
this.loadingTemplate = `<div class="filter-dropdown-loading"> <i class="fa fa-spinner fa-spin"></i>
<i class="fa fa-spinner fa-spin"></i> </div>`;
</div>`; this.bindEvents();
this.bindEvents(); }
}
bindEvents() {
this.itemClickedWrapper = this.itemClicked.bind(this);
this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
}
unbindEvents() { bindEvents() {
this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); this.itemClickedWrapper = this.itemClicked.bind(this);
} this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
}
getCurrentHook() { unbindEvents() {
return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null; this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper);
} }
itemClicked(e, getValueFunction) { getCurrentHook() {
const { selected } = e.detail; return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null;
}
if (selected.tagName === 'LI' && selected.innerHTML) { itemClicked(e, getValueFunction) {
const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected); const { selected } = e.detail;
if (!dataValueSet) { if (selected.tagName === 'LI' && selected.innerHTML) {
const value = getValueFunction(selected); const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
}
this.resetFilters(); if (!dataValueSet) {
this.dismissDropdown(); const value = getValueFunction(selected);
this.dispatchInputEvent(); gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
} }
}
setAsDropdown() { this.resetFilters();
this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`); this.dismissDropdown();
this.dispatchInputEvent();
} }
}
setOffset(offset = 0) { setAsDropdown() {
if (window.innerWidth > 480) { this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`);
this.dropdown.style.left = `${offset}px`; }
} else {
this.dropdown.style.left = '0px'; setOffset(offset = 0) {
} if (window.innerWidth > 480) {
this.dropdown.style.left = `${offset}px`;
} else {
this.dropdown.style.left = '0px';
} }
}
renderContent(forceShowList = false) { renderContent(forceShowList = false) {
const currentHook = this.getCurrentHook(); const currentHook = this.getCurrentHook();
if (forceShowList && currentHook && currentHook.list.hidden) { if (forceShowList && currentHook && currentHook.list.hidden) {
currentHook.list.show(); currentHook.list.show();
}
} }
}
render(forceRenderContent = false, forceShowList = false) { render(forceRenderContent = false, forceShowList = false) {
this.setAsDropdown(); this.setAsDropdown();
const currentHook = this.getCurrentHook(); const currentHook = this.getCurrentHook();
const firstTimeInitialized = currentHook === null; const firstTimeInitialized = currentHook === null;
if (firstTimeInitialized || forceRenderContent) { if (firstTimeInitialized || forceRenderContent) {
this.renderContent(forceShowList); this.renderContent(forceShowList);
} else if (currentHook.list.list.id !== this.dropdown.id) { } else if (currentHook.list.list.id !== this.dropdown.id) {
this.renderContent(forceShowList); this.renderContent(forceShowList);
}
} }
}
dismissDropdown() { dismissDropdown() {
// Focusing on the input will dismiss dropdown // Focusing on the input will dismiss dropdown
// (default droplab functionality) // (default droplab functionality)
this.input.focus(); this.input.focus();
} }
dispatchInputEvent() { dispatchInputEvent() {
// Propogate input change to FilteredSearchDropdownManager // Propogate input change to FilteredSearchDropdownManager
// so that it can determine which dropdowns to open // so that it can determine which dropdowns to open
this.input.dispatchEvent(new CustomEvent('input', { this.input.dispatchEvent(new CustomEvent('input', {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
})); }));
} }
dispatchFormSubmitEvent() { dispatchFormSubmitEvent() {
// dispatchEvent() is necessary as form.submit() does not // dispatchEvent() is necessary as form.submit() does not
// trigger event handlers // trigger event handlers
this.input.form.dispatchEvent(new Event('submit')); this.input.form.dispatchEvent(new Event('submit'));
} }
hideDropdown() { hideDropdown() {
const currentHook = this.getCurrentHook(); const currentHook = this.getCurrentHook();
if (currentHook) { if (currentHook) {
currentHook.list.hide(); currentHook.list.hide();
}
} }
}
resetFilters() { resetFilters() {
const hook = this.getCurrentHook(); const hook = this.getCurrentHook();
if (hook) { if (hook) {
const data = hook.list.data || []; const data = hook.list.data || [];
const results = data.map((o) => { const results = data.map((o) => {
const updated = o; const updated = o;
updated.droplab_hidden = false; updated.droplab_hidden = false;
return updated; return updated;
}); });
hook.list.render(results); hook.list.render(results);
}
} }
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.FilteredSearchDropdown = FilteredSearchDropdown; gl.FilteredSearchDropdown = FilteredSearchDropdown;
})();
(() => { const tokenKeys = [{
const tokenKeys = [{ key: 'author',
key: 'author', type: 'string',
type: 'string', param: 'username',
param: 'username', symbol: '@',
symbol: '@', }, {
}, { key: 'assignee',
key: 'assignee', type: 'string',
type: 'string', param: 'username',
param: 'username', symbol: '@',
symbol: '@', }, {
}, { key: 'milestone',
key: 'milestone', type: 'string',
type: 'string', param: 'title',
param: 'title', symbol: '%',
symbol: '%', }, {
}, { key: 'label',
key: 'label', type: 'array',
type: 'array', param: 'name[]',
param: 'name[]', symbol: '~',
symbol: '~', }];
}];
const alternativeTokenKeys = [{ const alternativeTokenKeys = [{
key: 'label', key: 'label',
type: 'string', type: 'string',
param: 'name', param: 'name',
symbol: '~', symbol: '~',
}]; }];
const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys); const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys);
const conditions = [{ const conditions = [{
url: 'assignee_id=0', url: 'assignee_id=0',
tokenKey: 'assignee', tokenKey: 'assignee',
value: 'none', value: 'none',
}, { }, {
url: 'milestone_title=No+Milestone', url: 'milestone_title=No+Milestone',
tokenKey: 'milestone', tokenKey: 'milestone',
value: 'none', value: 'none',
}, { }, {
url: 'milestone_title=%23upcoming', url: 'milestone_title=%23upcoming',
tokenKey: 'milestone', tokenKey: 'milestone',
value: 'upcoming', value: 'upcoming',
}, { }, {
url: 'milestone_title=%23started', url: 'milestone_title=%23started',
tokenKey: 'milestone', tokenKey: 'milestone',
value: 'started', value: 'started',
}, { }, {
url: 'label_name[]=No+Label', url: 'label_name[]=No+Label',
tokenKey: 'label', tokenKey: 'label',
value: 'none', value: 'none',
}]; }];
class FilteredSearchTokenKeys { class FilteredSearchTokenKeys {
static get() { static get() {
return tokenKeys; return tokenKeys;
} }
static getAlternatives() { static getAlternatives() {
return alternativeTokenKeys; return alternativeTokenKeys;
} }
static getConditions() { static getConditions() {
return conditions; return conditions;
} }
static searchByKey(key) { static searchByKey(key) {
return tokenKeys.find(tokenKey => tokenKey.key === key) || null; return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
} }
static searchBySymbol(symbol) { static searchBySymbol(symbol) {
return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
} }
static searchByKeyParam(keyParam) { static searchByKeyParam(keyParam) {
return tokenKeysWithAlternative.find((tokenKey) => { return tokenKeysWithAlternative.find((tokenKey) => {
let tokenKeyParam = tokenKey.key; let tokenKeyParam = tokenKey.key;
if (tokenKey.param) { if (tokenKey.param) {
tokenKeyParam += `_${tokenKey.param}`; tokenKeyParam += `_${tokenKey.param}`;
} }
return keyParam === tokenKeyParam; return keyParam === tokenKeyParam;
}) || null; }) || null;
} }
static searchByConditionUrl(url) { static searchByConditionUrl(url) {
return conditions.find(condition => condition.url === url) || null; return conditions.find(condition => condition.url === url) || null;
} }
static searchByConditionKeyValue(key, value) { static searchByConditionKeyValue(key, value) {
return conditions return conditions
.find(condition => condition.tokenKey === key && condition.value === value) || null; .find(condition => condition.tokenKey === key && condition.value === value) || null;
}
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys; gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
})();
require('./filtered_search_token_keys'); require('./filtered_search_token_keys');
(() => { class FilteredSearchTokenizer {
class FilteredSearchTokenizer { static processTokens(input) {
static processTokens(input) { const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key);
const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key); // Regex extracts `(token):(symbol)(value)`
// Regex extracts `(token):(symbol)(value)` // Values that start with a double quote must end in a double quote (same for single)
// Values that start with a double quote must end in a double quote (same for single) const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g'); const tokens = [];
const tokens = []; const tokenIndexes = []; // stores key+value for simple search
const tokenIndexes = []; // stores key+value for simple search let lastToken = null;
let lastToken = null; const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { let tokenValue = v1 || v2 || v3;
let tokenValue = v1 || v2 || v3; let tokenSymbol = symbol;
let tokenSymbol = symbol; let tokenIndex = '';
let tokenIndex = '';
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { tokenSymbol = tokenValue;
tokenSymbol = tokenValue; tokenValue = '';
tokenValue = '';
}
tokenIndex = `${key}:${tokenValue}`;
// Prevent adding duplicates
if (tokenIndexes.indexOf(tokenIndex) === -1) {
tokenIndexes.push(tokenIndex);
tokens.push({
key,
value: tokenValue || '',
symbol: tokenSymbol || '',
});
}
return '';
}).replace(/\s{2,}/g, ' ').trim() || '';
if (tokens.length > 0) {
const last = tokens[tokens.length - 1];
const lastString = `${last.key}:${last.symbol}${last.value}`;
lastToken = input.lastIndexOf(lastString) ===
input.length - lastString.length ? last : searchToken;
} else {
lastToken = searchToken;
} }
return { tokenIndex = `${key}:${tokenValue}`;
tokens,
lastToken, // Prevent adding duplicates
searchToken, if (tokenIndexes.indexOf(tokenIndex) === -1) {
}; tokenIndexes.push(tokenIndex);
tokens.push({
key,
value: tokenValue || '',
symbol: tokenSymbol || '',
});
}
return '';
}).replace(/\s{2,}/g, ' ').trim() || '';
if (tokens.length > 0) {
const last = tokens[tokens.length - 1];
const lastString = `${last.key}:${last.symbol}${last.value}`;
lastToken = input.lastIndexOf(lastString) ===
input.length - lastString.length ? last : searchToken;
} else {
lastToken = searchToken;
} }
return {
tokens,
lastToken,
searchToken,
};
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.FilteredSearchTokenizer = FilteredSearchTokenizer; gl.FilteredSearchTokenizer = FilteredSearchTokenizer;
})();
/* eslint-disable 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 = {};
});
}); });
}); });
})(); });
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, '');
......
This diff is collapsed.
...@@ -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
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