summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.babelrc1
-rw-r--r--app/assets/javascripts/comment_type_toggle.js41
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js12
-rw-r--r--app/assets/javascripts/droplab/constants.js11
-rw-r--r--app/assets/javascripts/droplab/drop_down.js137
-rw-r--r--app/assets/javascripts/droplab/drop_lab.js152
-rw-r--r--app/assets/javascripts/droplab/droplab.js741
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax.js103
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax_filter.js164
-rw-r--r--app/assets/javascripts/droplab/droplab_filter.js76
-rw-r--r--app/assets/javascripts/droplab/hook.js22
-rw-r--r--app/assets/javascripts/droplab/hook_button.js65
-rw-r--r--app/assets/javascripts/droplab/hook_input.js119
-rw-r--r--app/assets/javascripts/droplab/keyboard.js113
-rw-r--r--app/assets/javascripts/droplab/plugins/ajax.js65
-rw-r--r--app/assets/javascripts/droplab/plugins/ajax_filter.js133
-rw-r--r--app/assets/javascripts/droplab/plugins/filter.js95
-rw-r--r--app/assets/javascripts/droplab/plugins/input_setter.js51
-rw-r--r--app/assets/javascripts/droplab/utils.js38
-rw-r--r--app/assets/javascripts/files_comment_button.js26
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js11
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js22
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js17
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js2
-rw-r--r--app/assets/javascripts/gl_form.js3
-rw-r--r--app/assets/javascripts/main.js8
-rw-r--r--app/assets/javascripts/notes.js139
-rw-r--r--app/assets/stylesheets/framework/filters.scss2
-rw-r--r--app/assets/stylesheets/pages/note_form.scss55
-rw-r--r--app/assets/stylesheets/pages/notes.scss12
-rw-r--r--app/controllers/concerns/renders_notes.rb20
-rw-r--r--app/controllers/projects/commit_controller.rb20
-rw-r--r--app/controllers/projects/discussions_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb12
-rwxr-xr-xapp/controllers/projects/merge_requests_controller.rb32
-rw-r--r--app/controllers/projects/notes_controller.rb94
-rw-r--r--app/controllers/projects/snippets_controller.rb5
-rw-r--r--app/finders/notes_finder.rb65
-rw-r--r--app/helpers/diff_helper.rb8
-rw-r--r--app/helpers/notes_helper.rb56
-rw-r--r--app/mailers/emails/notes.rb17
-rw-r--r--app/mailers/notify.rb4
-rw-r--r--app/models/commit.rb5
-rw-r--r--app/models/concerns/discussion_on_diff.rb57
-rw-r--r--app/models/concerns/ignorable_column.rb28
-rw-r--r--app/models/concerns/issuable.rb11
-rw-r--r--app/models/concerns/note_on_diff.rb9
-rw-r--r--app/models/concerns/noteable.rb68
-rw-r--r--app/models/concerns/resolvable_discussion.rb103
-rw-r--r--app/models/concerns/resolvable_note.rb72
-rw-r--r--app/models/diff_discussion.rb26
-rw-r--r--app/models/diff_note.rb120
-rw-r--r--app/models/discussion.rb200
-rw-r--r--app/models/discussion_note.rb13
-rw-r--r--app/models/individual_note_discussion.rb13
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/legacy_diff_discussion.rb25
-rw-r--r--app/models/legacy_diff_note.rb24
-rw-r--r--app/models/merge_request.rb43
-rw-r--r--app/models/note.rb133
-rw-r--r--app/models/out_of_context_discussion.rb22
-rw-r--r--app/models/sent_notification.rb84
-rw-r--r--app/models/snippet.rb1
-rw-r--r--app/services/concerns/issues/resolve_discussions.rb4
-rw-r--r--app/services/issues/build_service.rb13
-rw-r--r--app/services/notes/build_service.rb25
-rw-r--r--app/services/notes/create_service.rb8
-rw-r--r--app/services/system_note_service.rb8
-rw-r--r--app/views/discussions/_diff_discussion.html.haml2
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml2
-rw-r--r--app/views/discussions/_discussion.html.haml16
-rw-r--r--app/views/discussions/_notes.html.haml28
-rw-r--r--app/views/discussions/_parallel_diff_discussion.html.haml14
-rw-r--r--app/views/discussions/_resolve_all.html.haml17
-rw-r--r--app/views/layouts/mailer.text.erb4
-rw-r--r--app/views/layouts/mailer.text.haml5
-rw-r--r--app/views/layouts/notify.html.haml4
-rw-r--r--app/views/layouts/notify.text.erb12
-rw-r--r--app/views/notify/_note_email.html.haml37
-rw-r--r--app/views/notify/_note_email.text.erb26
-rw-r--r--app/views/notify/_note_message.html.haml5
-rw-r--r--app/views/notify/_note_message.text.erb5
-rw-r--r--app/views/notify/_note_mr_or_commit_email.html.haml18
-rw-r--r--app/views/notify/_note_mr_or_commit_email.text.erb8
-rw-r--r--app/views/notify/_simple_diff.text.erb3
-rw-r--r--app/views/notify/new_issue_email.html.haml10
-rw-r--r--app/views/notify/new_mention_in_issue_email.html.haml10
-rw-r--r--app/views/notify/new_mention_in_merge_request_email.html.haml13
-rw-r--r--app/views/notify/new_merge_request_email.html.haml10
-rw-r--r--app/views/notify/note_commit_email.html.haml3
-rw-r--r--app/views/notify/note_commit_email.text.erb3
-rw-r--r--app/views/notify/note_issue_email.html.haml2
-rw-r--r--app/views/notify/note_issue_email.text.erb10
-rw-r--r--app/views/notify/note_merge_request_email.html.haml3
-rw-r--r--app/views/notify/note_merge_request_email.text.erb3
-rw-r--r--app/views/notify/note_personal_snippet_email.html.haml2
-rw-r--r--app/views/notify/note_personal_snippet_email.text.erb9
-rw-r--r--app/views/notify/note_snippet_email.html.haml2
-rw-r--r--app/views/notify/note_snippet_email.text.erb9
-rw-r--r--app/views/projects/commit/show.html.haml1
-rw-r--r--app/views/projects/diffs/_line.html.haml9
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml8
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml4
-rw-r--r--app/views/projects/notes/_comment_button.html.haml28
-rw-r--r--app/views/projects/notes/_form.html.haml16
-rw-r--r--app/views/projects/notes/_note.html.haml2
-rw-r--r--app/views/projects/notes/_notes.html.haml6
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml2
-rw-r--r--changelogs/unreleased/new-resolvable-discussion.yml4
-rw-r--r--db/migrate/20161128095517_add_in_reply_to_discussion_id_to_sent_notifications.rb29
-rw-r--r--db/post_migrate/20170404170532_remove_notes_original_discussion_id.rb23
-rw-r--r--db/schema.rb5
-rw-r--r--features/steps/project/merge_requests.rb3
-rw-r--r--lib/gitlab/diff/line.rb4
-rw-r--r--lib/gitlab/email/handler/create_note_handler.rb13
-rw-r--r--package.json1
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb4
-rw-r--r--spec/controllers/projects/discussions_controller_spec.rb2
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb4
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb133
-rw-r--r--spec/factories/notes.rb28
-rw-r--r--spec/factories/sent_notifications.rb4
-rw-r--r--spec/features/discussion_comments_spec.rb254
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb14
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb31
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb14
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb18
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb4
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb2
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb2
-rw-r--r--spec/finders/notes_finder_spec.rb41
-rw-r--r--spec/helpers/issues_helper_spec.rb2
-rw-r--r--spec/helpers/notes_helper_spec.rb17
-rw-r--r--spec/javascripts/comment_type_toggle_spec.js124
-rw-r--r--spec/javascripts/droplab/constants.js29
-rw-r--r--spec/javascripts/droplab/drop_down.js578
-rw-r--r--spec/javascripts/droplab/hook.js82
-rw-r--r--spec/javascripts/droplab/plugins/input_setter.js177
-rw-r--r--spec/javascripts/filtered_search/dropdown_user_spec.js8
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb1
-rw-r--r--spec/mailers/notify_spec.rb142
-rw-r--r--spec/mailers/previews/notify_preview.rb96
-rw-r--r--spec/models/concerns/discussion_on_diff_spec.rb24
-rw-r--r--spec/models/concerns/noteable_spec.rb261
-rw-r--r--spec/models/concerns/resolvable_discussion_spec.rb548
-rw-r--r--spec/models/concerns/resolvable_note_spec.rb329
-rw-r--r--spec/models/diff_discussion_spec.rb19
-rw-r--r--spec/models/diff_note_spec.rb334
-rw-r--r--spec/models/discussion_spec.rb623
-rw-r--r--spec/models/legacy_diff_discussion_spec.rb11
-rw-r--r--spec/models/legacy_diff_note_spec.rb101
-rw-r--r--spec/models/merge_request_spec.rb176
-rw-r--r--spec/models/note_spec.rb293
-rw-r--r--spec/models/sent_notification_spec.rb166
-rw-r--r--spec/requests/api/issues_spec.rb2
-rw-r--r--spec/requests/api/v3/issues_spec.rb2
-rw-r--r--spec/services/discussions/resolve_service_spec.rb4
-rw-r--r--spec/services/issues/build_service_spec.rb16
-rw-r--r--spec/services/issues/create_service_spec.rb2
-rw-r--r--spec/services/issues/resolve_discussions_spec.rb2
-rw-r--r--spec/services/notes/build_service_spec.rb40
-rw-r--r--spec/services/notification_service_spec.rb2
-rw-r--r--spec/services/system_note_service_spec.rb2
-rw-r--r--yarn.lock1047
168 files changed, 6383 insertions, 3780 deletions
diff --git a/.babelrc b/.babelrc
index ee4c391da30..2bae7ca9fbf 100644
--- a/.babelrc
+++ b/.babelrc
@@ -8,7 +8,6 @@
"plugins": [
["istanbul", {
"exclude": [
- "app/assets/javascripts/droplab/**/*",
"spec/javascripts/**/*"
]
}],
diff --git a/app/assets/javascripts/comment_type_toggle.js b/app/assets/javascripts/comment_type_toggle.js
new file mode 100644
index 00000000000..34878f90050
--- /dev/null
+++ b/app/assets/javascripts/comment_type_toggle.js
@@ -0,0 +1,41 @@
+import DropLab from '~/droplab/drop_lab';
+import InputSetter from '~/droplab/plugins/input_setter';
+
+class CommentTypeToggle {
+ constructor(dropdownTrigger, dropdownList, noteTypeInput, submitButton, closeButton) {
+ this.dropdownTrigger = dropdownTrigger;
+ this.dropdownList = dropdownList;
+ this.noteTypeInput = noteTypeInput;
+ this.submitButton = submitButton;
+ this.closeButton = closeButton;
+ }
+
+ initDroplab() {
+ this.droplab = new DropLab();
+
+ const inputSetterConfig = [{
+ input: this.noteTypeInput,
+ valueAttribute: 'data-value',
+ },
+ {
+ input: this.submitButton,
+ valueAttribute: 'data-button-text',
+ }];
+ if (this.closeButton) {
+ inputSetterConfig.push({
+ input: this.closeButton,
+ valueAttribute: 'data-secondary-button-text',
+ }, {
+ input: this.closeButton,
+ valueAttribute: 'data-secondary-button-text',
+ inputAttribute: 'data-alternative-text',
+ });
+ }
+
+ this.droplab.init(this.dropdownTrigger, this.dropdownList, [InputSetter], {
+ InputSetter: inputSetterConfig,
+ });
+ }
+}
+
+export default CommentTypeToggle;
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
index fc2f20e3bcb..eb76b7d15fd 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
@@ -42,10 +42,14 @@ import Vue from 'vue';
}
},
created() {
- this.discussion = CommentsStore.state[this.discussionId];
+ if (this.discussionId) {
+ this.discussion = CommentsStore.state[this.discussionId];
+ }
},
mounted: function () {
- const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`);
+ if (!this.discussionId) return;
+
+ const $textarea = $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`);
this.textareaIsEmpty = $textarea.val() === '';
$textarea.on('input.comment-and-resolve-btn', () => {
@@ -53,7 +57,9 @@ import Vue from 'vue';
});
},
destroyed: function () {
- $(`#new-discussion-note-form-${this.discussionId} .note-textarea`).off('input.comment-and-resolve-btn');
+ if (!this.discussionId) return;
+
+ $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off('input.comment-and-resolve-btn');
}
});
diff --git a/app/assets/javascripts/droplab/constants.js b/app/assets/javascripts/droplab/constants.js
new file mode 100644
index 00000000000..a23d914772a
--- /dev/null
+++ b/app/assets/javascripts/droplab/constants.js
@@ -0,0 +1,11 @@
+const DATA_TRIGGER = 'data-dropdown-trigger';
+const DATA_DROPDOWN = 'data-dropdown';
+const SELECTED_CLASS = 'droplab-item-selected';
+const ACTIVE_CLASS = 'droplab-item-active';
+
+export {
+ DATA_TRIGGER,
+ DATA_DROPDOWN,
+ SELECTED_CLASS,
+ ACTIVE_CLASS,
+};
diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js
new file mode 100644
index 00000000000..f686ad33f6f
--- /dev/null
+++ b/app/assets/javascripts/droplab/drop_down.js
@@ -0,0 +1,137 @@
+/* eslint-disable */
+
+import utils from './utils';
+import { SELECTED_CLASS } from './constants';
+
+var DropDown = function(list) {
+ this.currentIndex = 0;
+ this.hidden = true;
+ this.list = typeof list === 'string' ? document.querySelector(list) : list;
+ this.items = [];
+
+ this.eventWrapper = {};
+
+ this.getItems();
+ this.initTemplateString();
+ this.addEvents();
+
+ this.initialState = list.innerHTML;
+};
+
+Object.assign(DropDown.prototype, {
+ getItems: function() {
+ this.items = [].slice.call(this.list.querySelectorAll('li'));
+ return this.items;
+ },
+
+ initTemplateString: function() {
+ var items = this.items || this.getItems();
+
+ var templateString = '';
+ if (items.length > 0) templateString = items[items.length - 1].outerHTML;
+ this.templateString = templateString;
+
+ return this.templateString;
+ },
+
+ clickEvent: function(e) {
+ var selected = utils.closest(e.target, 'LI');
+ if (!selected) return;
+
+ this.addSelectedClass(selected);
+
+ e.preventDefault();
+ this.hide();
+
+ var listEvent = new CustomEvent('click.dl', {
+ detail: {
+ list: this,
+ selected: selected,
+ data: e.target.dataset,
+ },
+ });
+ this.list.dispatchEvent(listEvent);
+ },
+
+ addSelectedClass: function (selected) {
+ this.removeSelectedClasses();
+ selected.classList.add(SELECTED_CLASS);
+ },
+
+ removeSelectedClasses: function () {
+ const items = this.items || this.getItems();
+
+ items.forEach(item => item.classList.remove(SELECTED_CLASS));
+ },
+
+ addEvents: function() {
+ this.eventWrapper.clickEvent = this.clickEvent.bind(this)
+ this.list.addEventListener('click', this.eventWrapper.clickEvent);
+ },
+
+ toggle: function() {
+ this.hidden ? this.show() : this.hide();
+ },
+
+ setData: function(data) {
+ this.data = data;
+ this.render(data);
+ },
+
+ addData: function(data) {
+ this.data = (this.data || []).concat(data);
+ this.render(this.data);
+ },
+
+ render: function(data) {
+ const children = data ? data.map(this.renderChildren.bind(this)) : [];
+ const renderableList = this.list.querySelector('ul[data-dynamic]') || this.list;
+
+ renderableList.innerHTML = children.join('');
+ },
+
+ renderChildren: function(data) {
+ var html = utils.t(this.templateString, data);
+ var template = document.createElement('div');
+
+ template.innerHTML = html;
+ this.setImagesSrc(template);
+ template.firstChild.style.display = data.droplab_hidden ? 'none' : 'block';
+
+ return template.firstChild.outerHTML;
+ },
+
+ setImagesSrc: function(template) {
+ const images = [].slice.call(template.querySelectorAll('img[data-src]'));
+
+ images.forEach((image) => {
+ image.src = image.getAttribute('data-src');
+ image.removeAttribute('data-src');
+ });
+ },
+
+ show: function() {
+ if (!this.hidden) return;
+ this.list.style.display = 'block';
+ this.currentIndex = 0;
+ this.hidden = false;
+ },
+
+ hide: function() {
+ if (this.hidden) return;
+ this.list.style.display = 'none';
+ this.currentIndex = 0;
+ this.hidden = true;
+ },
+
+ toggle: function () {
+ this.hidden ? this.show() : this.hide();
+ },
+
+ destroy: function() {
+ this.hide();
+ this.list.removeEventListener('click', this.eventWrapper.clickEvent);
+ }
+});
+
+export default DropDown;
diff --git a/app/assets/javascripts/droplab/drop_lab.js b/app/assets/javascripts/droplab/drop_lab.js
new file mode 100644
index 00000000000..6eb9f314af7
--- /dev/null
+++ b/app/assets/javascripts/droplab/drop_lab.js
@@ -0,0 +1,152 @@
+/* eslint-disable */
+
+import HookButton from './hook_button';
+import HookInput from './hook_input';
+import utils from './utils';
+import Keyboard from './keyboard';
+import { DATA_TRIGGER } from './constants';
+
+var DropLab = function() {
+ this.ready = false;
+ this.hooks = [];
+ this.queuedData = [];
+ this.config = {};
+
+ this.eventWrapper = {};
+};
+
+Object.assign(DropLab.prototype, {
+ loadStatic: function(){
+ var dropdownTriggers = [].slice.apply(document.querySelectorAll(`[${DATA_TRIGGER}]`));
+ this.addHooks(dropdownTriggers);
+ },
+
+ addData: function () {
+ var args = [].slice.apply(arguments);
+ this.applyArgs(args, '_addData');
+ },
+
+ setData: function() {
+ var args = [].slice.apply(arguments);
+ this.applyArgs(args, '_setData');
+ },
+
+ destroy: function() {
+ this.hooks.forEach(hook => hook.destroy());
+ this.hooks = [];
+ this.removeEvents();
+ },
+
+ applyArgs: function(args, methodName) {
+ if (this.ready) return this[methodName].apply(this, args);
+
+ this.queuedData = this.queuedData || [];
+ this.queuedData.push(args);
+ },
+
+ _addData: function(trigger, data) {
+ this._processData(trigger, data, 'addData');
+ },
+
+ _setData: function(trigger, data) {
+ this._processData(trigger, data, 'setData');
+ },
+
+ _processData: function(trigger, data, methodName) {
+ this.hooks.forEach((hook) => {
+ if (Array.isArray(trigger)) hook.list[methodName](trigger);
+
+ if (hook.trigger.id === trigger) hook.list[methodName](data);
+ });
+ },
+
+ addEvents: function() {
+ this.eventWrapper.documentClicked = this.documentClicked.bind(this)
+ document.addEventListener('click', this.eventWrapper.documentClicked);
+ },
+
+ documentClicked: function(e) {
+ let thisTag = e.target;
+
+ if (thisTag.tagName !== 'UL') thisTag = utils.closest(thisTag, 'UL');
+ if (utils.isDropDownParts(thisTag, this.hooks) || utils.isDropDownParts(e.target, this.hooks)) return;
+
+ this.hooks.forEach(hook => hook.list.hide());
+ },
+
+ removeEvents: function(){
+ document.removeEventListener('click', this.eventWrapper.documentClicked);
+ },
+
+ changeHookList: function(trigger, list, plugins, config) {
+ const availableTrigger = typeof trigger === 'string' ? document.getElementById(trigger) : trigger;
+
+
+ this.hooks.forEach((hook, i) => {
+ hook.list.list.dataset.dropdownActive = false;
+
+ if (hook.trigger !== availableTrigger) return;
+
+ hook.destroy();
+ this.hooks.splice(i, 1);
+ this.addHook(availableTrigger, list, plugins, config);
+ });
+ },
+
+ addHook: function(hook, list, plugins, config) {
+ const availableHook = typeof hook === 'string' ? document.querySelector(hook) : hook;
+ let availableList;
+
+ if (typeof list === 'string') {
+ availableList = document.querySelector(list);
+ } else if (list instanceof Element) {
+ availableList = list;
+ } else {
+ availableList = document.querySelector(hook.dataset[utils.toCamelCase(DATA_TRIGGER)]);
+ }
+
+ availableList.dataset.dropdownActive = true;
+
+ const HookObject = availableHook.tagName === 'INPUT' ? HookInput : HookButton;
+ this.hooks.push(new HookObject(availableHook, availableList, plugins, config));
+
+ return this;
+ },
+
+ addHooks: function(hooks, plugins, config) {
+ hooks.forEach(hook => this.addHook(hook, null, plugins, config));
+ return this;
+ },
+
+ setConfig: function(obj){
+ this.config = obj;
+ },
+
+ fireReady: function() {
+ const readyEvent = new CustomEvent('ready.dl', {
+ detail: {
+ dropdown: this,
+ },
+ });
+ document.dispatchEvent(readyEvent);
+
+ this.ready = true;
+ },
+
+ init: function (hook, list, plugins, config) {
+ hook ? this.addHook(hook, list, plugins, config) : this.loadStatic();
+
+ this.addEvents();
+
+ Keyboard();
+
+ this.fireReady();
+
+ this.queuedData.forEach(data => this.addData(data));
+ this.queuedData = [];
+
+ return this;
+ },
+});
+
+export default DropLab;
diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js
deleted file mode 100644
index 8b14191395b..00000000000
--- a/app/assets/javascripts/droplab/droplab.js
+++ /dev/null
@@ -1,741 +0,0 @@
-/* eslint-disable */
-// Determine where to place this
-if (typeof Object.assign != 'function') {
- Object.assign = function (target, varArgs) { // .length of function is 2
- 'use strict';
- if (target == null) { // TypeError if undefined or null
- throw new TypeError('Cannot convert undefined or null to object');
- }
-
- var to = Object(target);
-
- for (var index = 1; index < arguments.length; index++) {
- var nextSource = arguments[index];
-
- if (nextSource != null) { // Skip over if undefined or null
- for (var nextKey in nextSource) {
- // Avoid bugs when hasOwnProperty is shadowed
- if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
- to[nextKey] = nextSource[nextKey];
- }
- }
- }
- }
- return to;
- };
-}
-
-(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.droplab = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
-var DATA_TRIGGER = 'data-dropdown-trigger';
-var DATA_DROPDOWN = 'data-dropdown';
-
-module.exports = {
- DATA_TRIGGER: DATA_TRIGGER,
- DATA_DROPDOWN: DATA_DROPDOWN,
-}
-
-},{}],2:[function(require,module,exports){
-// Custom event support for IE
-if ( typeof CustomEvent === "function" ) {
- module.exports = CustomEvent;
-} else {
- require('./window')(function(w){
- var CustomEvent = function ( event, params ) {
- params = params || { bubbles: false, cancelable: false, detail: undefined };
- var evt = document.createEvent( 'CustomEvent' );
- evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
- return evt;
- }
- CustomEvent.prototype = w.Event.prototype;
-
- w.CustomEvent = CustomEvent;
- });
- module.exports = CustomEvent;
-}
-
-},{"./window":11}],3:[function(require,module,exports){
-var CustomEvent = require('./custom_event_polyfill');
-var utils = require('./utils');
-
-var DropDown = function(list) {
- this.currentIndex = 0;
- this.hidden = true;
- this.list = list;
- this.items = [];
- this.getItems();
- this.initTemplateString();
- this.addEvents();
- this.initialState = list.innerHTML;
-};
-
-Object.assign(DropDown.prototype, {
- getItems: function() {
- this.items = [].slice.call(this.list.querySelectorAll('li'));
- return this.items;
- },
-
- initTemplateString: function() {
- var items = this.items || this.getItems();
-
- var templateString = '';
- if(items.length > 0) {
- templateString = items[items.length - 1].outerHTML;
- }
- this.templateString = templateString;
- return this.templateString;
- },
-
- clickEvent: function(e) {
- // climb up the tree to find the LI
- var selected = utils.closest(e.target, 'LI');
-
- if(selected) {
- e.preventDefault();
- this.hide();
- var listEvent = new CustomEvent('click.dl', {
- detail: {
- list: this,
- selected: selected,
- data: e.target.dataset,
- },
- });
- this.list.dispatchEvent(listEvent);
- }
- },
-
- addEvents: function() {
- this.clickWrapper = this.clickEvent.bind(this);
- // event delegation.
- this.list.addEventListener('click', this.clickWrapper);
- },
-
- toggle: function() {
- if(this.hidden) {
- this.show();
- } else {
- this.hide();
- }
- },
-
- setData: function(data) {
- this.data = data;
- this.render(data);
- },
-
- addData: function(data) {
- this.data = (this.data || []).concat(data);
- this.render(this.data);
- },
-
- // call render manually on data;
- render: function(data){
- // debugger
- // empty the list first
- var templateString = this.templateString;
- var newChildren = [];
- var toAppend;
-
- newChildren = (data ||[]).map(function(dat){
- var html = utils.t(templateString, dat);
- var template = document.createElement('div');
- template.innerHTML = html;
-
- // Help set the image src template
- var imageTags = template.querySelectorAll('img[data-src]');
- // debugger
- for(var i = 0; i < imageTags.length; i++) {
- var imageTag = imageTags[i];
- imageTag.src = imageTag.getAttribute('data-src');
- imageTag.removeAttribute('data-src');
- }
-
- if(dat.hasOwnProperty('droplab_hidden') && dat.droplab_hidden){
- template.firstChild.style.display = 'none'
- }else{
- template.firstChild.style.display = 'block';
- }
- return template.firstChild.outerHTML;
- });
- toAppend = this.list.querySelector('ul[data-dynamic]');
- if(toAppend) {
- toAppend.innerHTML = newChildren.join('');
- } else {
- this.list.innerHTML = newChildren.join('');
- }
- },
-
- show: function() {
- if (this.hidden) {
- // debugger
- this.list.style.display = 'block';
- this.currentIndex = 0;
- this.hidden = false;
- }
- },
-
- hide: function() {
- if (!this.hidden) {
- // debugger
- this.list.style.display = 'none';
- this.currentIndex = 0;
- this.hidden = true;
- }
- },
-
- destroy: function() {
- this.hide();
- this.list.removeEventListener('click', this.clickWrapper);
- }
-});
-
-module.exports = DropDown;
-
-},{"./custom_event_polyfill":2,"./utils":10}],4:[function(require,module,exports){
-require('./window')(function(w){
- module.exports = function(deps) {
- deps = deps || {};
- var window = deps.window || w;
- var document = deps.document || window.document;
- var CustomEvent = deps.CustomEvent || require('./custom_event_polyfill');
- var HookButton = deps.HookButton || require('./hook_button');
- var HookInput = deps.HookInput || require('./hook_input');
- var utils = deps.utils || require('./utils');
- var DATA_TRIGGER = require('./constants').DATA_TRIGGER;
-
- var DropLab = function(hook){
- if (!(this instanceof DropLab)) return new DropLab(hook);
- this.ready = false;
- this.hooks = [];
- this.queuedData = [];
- this.config = {};
- this.loadWrapper;
- if(typeof hook !== 'undefined'){
- this.addHook(hook);
- }
- };
-
-
- Object.assign(DropLab.prototype, {
- load: function() {
- this.loadWrapper();
- },
-
- loadWrapper: function(){
- var dropdownTriggers = [].slice.apply(document.querySelectorAll('['+DATA_TRIGGER+']'));
- this.addHooks(dropdownTriggers).init();
- },
-
- addData: function () {
- var args = [].slice.apply(arguments);
- this.applyArgs(args, '_addData');
- },
-
- setData: function() {
- var args = [].slice.apply(arguments);
- this.applyArgs(args, '_setData');
- },
-
- destroy: function() {
- for(var i = 0; i < this.hooks.length; i++) {
- this.hooks[i].destroy();
- }
- this.hooks = [];
- this.removeEvents();
- },
-
- applyArgs: function(args, methodName) {
- if(this.ready) {
- this[methodName].apply(this, args);
- } else {
- this.queuedData = this.queuedData || [];
- this.queuedData.push(args);
- }
- },
-
- _addData: function(trigger, data) {
- this._processData(trigger, data, 'addData');
- },
-
- _setData: function(trigger, data) {
- this._processData(trigger, data, 'setData');
- },
-
- _processData: function(trigger, data, methodName) {
- for(var i = 0; i < this.hooks.length; i++) {
- var hook = this.hooks[i];
- if(hook.trigger.dataset.hasOwnProperty('id')) {
- if(hook.trigger.dataset.id === trigger) {
- hook.list[methodName](data);
- }
- }
- }
- },
-
- addEvents: function() {
- var self = this;
- this.windowClickedWrapper = function(e){
- var thisTag = e.target;
- if(thisTag.tagName !== 'UL'){
- // climb up the tree to find the UL
- thisTag = utils.closest(thisTag, 'UL');
- }
- if(utils.isDropDownParts(thisTag)){ return }
- if(utils.isDropDownParts(e.target)){ return }
- for(var i = 0; i < self.hooks.length; i++) {
- self.hooks[i].list.hide();
- }
- }.bind(this);
- document.addEventListener('click', this.windowClickedWrapper);
- },
-
- removeEvents: function(){
- w.removeEventListener('click', this.windowClickedWrapper);
- w.removeEventListener('load', this.loadWrapper);
- },
-
- changeHookList: function(trigger, list, plugins, config) {
- trigger = document.querySelector('[data-id="'+trigger+'"]');
- // list = document.querySelector(list);
- this.hooks.every(function(hook, i) {
- if(hook.trigger === trigger) {
- hook.destroy();
- this.hooks.splice(i, 1);
- this.addHook(trigger, list, plugins, config);
- return false;
- }
- return true
- }.bind(this));
- },
-
- addHook: function(hook, list, plugins, config) {
- if(!(hook instanceof HTMLElement) && typeof hook === 'string'){
- hook = document.querySelector(hook);
- }
- if(!list){
- list = document.querySelector(hook.dataset[utils.toDataCamelCase(DATA_TRIGGER)]);
- }
-
- if(hook) {
- if(hook.tagName === 'A' || hook.tagName === 'BUTTON') {
- this.hooks.push(new HookButton(hook, list, plugins, config));
- } else if(hook.tagName === 'INPUT') {
- this.hooks.push(new HookInput(hook, list, plugins, config));
- }
- }
- return this;
- },
-
- addHooks: function(hooks, plugins, config) {
- for(var i = 0; i < hooks.length; i++) {
- var hook = hooks[i];
- this.addHook(hook, null, plugins, config);
- }
- return this;
- },
-
- setConfig: function(obj){
- this.config = obj;
- },
-
- init: function () {
- this.addEvents();
- var readyEvent = new CustomEvent('ready.dl', {
- detail: {
- dropdown: this,
- },
- });
- window.dispatchEvent(readyEvent);
- this.ready = true;
- for(var i = 0; i < this.queuedData.length; i++) {
- this.addData.apply(this, this.queuedData[i]);
- }
- this.queuedData = [];
- return this;
- },
- });
-
- return DropLab;
- };
-});
-
-},{"./constants":1,"./custom_event_polyfill":2,"./hook_button":6,"./hook_input":7,"./utils":10,"./window":11}],5:[function(require,module,exports){
-var DropDown = require('./dropdown');
-
-var Hook = function(trigger, list, plugins, config){
- this.trigger = trigger;
- this.list = new DropDown(list);
- this.type = 'Hook';
- this.event = 'click';
- this.plugins = plugins || [];
- this.config = config || {};
- this.id = trigger.dataset.id;
-};
-
-Object.assign(Hook.prototype, {
-
- addEvents: function(){},
-
- constructor: Hook,
-});
-
-module.exports = Hook;
-
-},{"./dropdown":3}],6:[function(require,module,exports){
-var CustomEvent = require('./custom_event_polyfill');
-var Hook = require('./hook');
-
-var HookButton = function(trigger, list, plugins, config) {
- Hook.call(this, trigger, list, plugins, config);
- this.type = 'button';
- this.event = 'click';
- this.addEvents();
- this.addPlugins();
-};
-
-HookButton.prototype = Object.create(Hook.prototype);
-
-Object.assign(HookButton.prototype, {
- addPlugins: function() {
- for(var i = 0; i < this.plugins.length; i++) {
- this.plugins[i].init(this);
- }
- },
-
- clicked: function(e){
- var buttonEvent = new CustomEvent('click.dl', {
- detail: {
- hook: this,
- },
- bubbles: true,
- cancelable: true
- });
- this.list.show();
- e.target.dispatchEvent(buttonEvent);
- },
-
- addEvents: function(){
- this.clickedWrapper = this.clicked.bind(this);
- this.trigger.addEventListener('click', this.clickedWrapper);
- },
-
- removeEvents: function(){
- this.trigger.removeEventListener('click', this.clickedWrapper);
- },
-
- restoreInitialState: function() {
- this.list.list.innerHTML = this.list.initialState;
- },
-
- removePlugins: function() {
- for(var i = 0; i < this.plugins.length; i++) {
- this.plugins[i].destroy();
- }
- },
-
- destroy: function() {
- this.restoreInitialState();
- this.removeEvents();
- this.removePlugins();
- },
-
-
- constructor: HookButton,
-});
-
-
-module.exports = HookButton;
-
-},{"./custom_event_polyfill":2,"./hook":5}],7:[function(require,module,exports){
-var CustomEvent = require('./custom_event_polyfill');
-var Hook = require('./hook');
-
-var HookInput = function(trigger, list, plugins, config) {
- Hook.call(this, trigger, list, plugins, config);
- this.type = 'input';
- this.event = 'input';
- this.addPlugins();
- this.addEvents();
-};
-
-Object.assign(HookInput.prototype, {
- addPlugins: function() {
- var self = this;
- for(var i = 0; i < this.plugins.length; i++) {
- this.plugins[i].init(self);
- }
- },
-
- addEvents: function(){
- var self = this;
-
- this.mousedown = function mousedown(e) {
- if(self.hasRemovedEvents) return;
-
- var mouseEvent = new CustomEvent('mousedown.dl', {
- detail: {
- hook: self,
- text: e.target.value,
- },
- bubbles: true,
- cancelable: true
- });
- e.target.dispatchEvent(mouseEvent);
- }
-
- this.input = function input(e) {
- if(self.hasRemovedEvents) return;
-
- self.list.show();
-
- var inputEvent = new CustomEvent('input.dl', {
- detail: {
- hook: self,
- text: e.target.value,
- },
- bubbles: true,
- cancelable: true
- });
- e.target.dispatchEvent(inputEvent);
- }
-
- this.keyup = function keyup(e) {
- if(self.hasRemovedEvents) return;
-
- keyEvent(e, 'keyup.dl');
- }
-
- this.keydown = function keydown(e) {
- if(self.hasRemovedEvents) return;
-
- keyEvent(e, 'keydown.dl');
- }
-
- function keyEvent(e, keyEventName){
- self.list.show();
-
- var keyEvent = new CustomEvent(keyEventName, {
- detail: {
- hook: self,
- text: e.target.value,
- which: e.which,
- key: e.key,
- },
- bubbles: true,
- cancelable: true
- });
- e.target.dispatchEvent(keyEvent);
- }
-
- this.events = this.events || {};
- this.events.mousedown = this.mousedown;
- this.events.input = this.input;
- this.events.keyup = this.keyup;
- this.events.keydown = this.keydown;
- this.trigger.addEventListener('mousedown', this.mousedown);
- this.trigger.addEventListener('input', this.input);
- this.trigger.addEventListener('keyup', this.keyup);
- this.trigger.addEventListener('keydown', this.keydown);
- },
-
- removeEvents: function() {
- this.hasRemovedEvents = true;
- this.trigger.removeEventListener('mousedown', this.mousedown);
- this.trigger.removeEventListener('input', this.input);
- this.trigger.removeEventListener('keyup', this.keyup);
- this.trigger.removeEventListener('keydown', this.keydown);
- },
-
- restoreInitialState: function() {
- this.list.list.innerHTML = this.list.initialState;
- },
-
- removePlugins: function() {
- for(var i = 0; i < this.plugins.length; i++) {
- this.plugins[i].destroy();
- }
- },
-
- destroy: function() {
- this.restoreInitialState();
- this.removeEvents();
- this.removePlugins();
- this.list.destroy();
- }
-});
-
-module.exports = HookInput;
-
-},{"./custom_event_polyfill":2,"./hook":5}],8:[function(require,module,exports){
-var DropLab = require('./droplab')();
-var DATA_TRIGGER = require('./constants').DATA_TRIGGER;
-var keyboard = require('./keyboard')();
-var setup = function() {
- window.DropLab = DropLab;
-};
-
-
-module.exports = setup();
-
-},{"./constants":1,"./droplab":4,"./keyboard":9}],9:[function(require,module,exports){
-require('./window')(function(w){
- module.exports = function(){
- var currentKey;
- var currentFocus;
- var isUpArrow = false;
- var isDownArrow = false;
- var removeHighlight = function removeHighlight(list) {
- var listItems = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider)'), 0);
- var listItemsTmp = [];
- for(var i = 0; i < listItems.length; i++) {
- var listItem = listItems[i];
- listItem.classList.remove('dropdown-active');
-
- if (listItem.style.display !== 'none') {
- listItemsTmp.push(listItem);
- }
- }
- return listItemsTmp;
- };
-
- var setMenuForArrows = function setMenuForArrows(list) {
- var listItems = removeHighlight(list);
- if(list.currentIndex>0){
- if(!listItems[list.currentIndex-1]){
- list.currentIndex = list.currentIndex-1;
- }
-
- if (listItems[list.currentIndex-1]) {
- var el = listItems[list.currentIndex-1];
- var filterDropdownEl = el.closest('.filter-dropdown');
- el.classList.add('dropdown-active');
-
- if (filterDropdownEl) {
- var filterDropdownBottom = filterDropdownEl.offsetHeight;
- var elOffsetTop = el.offsetTop - 30;
-
- if (elOffsetTop > filterDropdownBottom) {
- filterDropdownEl.scrollTop = elOffsetTop - filterDropdownBottom;
- }
- }
- }
- }
- };
-
- var mousedown = function mousedown(e) {
- var list = e.detail.hook.list;
- removeHighlight(list);
- list.show();
- list.currentIndex = 0;
- isUpArrow = false;
- isDownArrow = false;
- };
- var selectItem = function selectItem(list) {
- var listItems = removeHighlight(list);
- var currentItem = listItems[list.currentIndex-1];
- var listEvent = new CustomEvent('click.dl', {
- detail: {
- list: list,
- selected: currentItem,
- data: currentItem.dataset,
- },
- });
- list.list.dispatchEvent(listEvent);
- list.hide();
- }
-
- var keydown = function keydown(e){
- var typedOn = e.target;
- var list = e.detail.hook.list;
- var currentIndex = list.currentIndex;
- isUpArrow = false;
- isDownArrow = false;
-
- if(e.detail.which){
- currentKey = e.detail.which;
- if(currentKey === 13){
- selectItem(e.detail.hook.list);
- return;
- }
- if(currentKey === 38) {
- isUpArrow = true;
- }
- if(currentKey === 40) {
- isDownArrow = true;
- }
- } else if(e.detail.key) {
- currentKey = e.detail.key;
- if(currentKey === 'Enter'){
- selectItem(e.detail.hook.list);
- return;
- }
- if(currentKey === 'ArrowUp') {
- isUpArrow = true;
- }
- if(currentKey === 'ArrowDown') {
- isDownArrow = true;
- }
- }
- if(isUpArrow){ currentIndex--; }
- if(isDownArrow){ currentIndex++; }
- if(currentIndex < 0){ currentIndex = 0; }
- list.currentIndex = currentIndex;
- setMenuForArrows(e.detail.hook.list);
- };
-
- w.addEventListener('mousedown.dl', mousedown);
- w.addEventListener('keydown.dl', keydown);
- };
-});
-},{"./window":11}],10:[function(require,module,exports){
-var DATA_TRIGGER = require('./constants').DATA_TRIGGER;
-var DATA_DROPDOWN = require('./constants').DATA_DROPDOWN;
-
-var toDataCamelCase = function(attr){
- return this.camelize(attr.split('-').slice(1).join(' '));
-};
-
-// the tiniest damn templating I can do
-var t = function(s,d){
- for(var p in d)
- s=s.replace(new RegExp('{{'+p+'}}','g'), d[p]);
- return s;
-};
-
-var camelize = function(str) {
- return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter, index) {
- return index == 0 ? letter.toLowerCase() : letter.toUpperCase();
- }).replace(/\s+/g, '');
-};
-
-var closest = function(thisTag, stopTag) {
- while(thisTag && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){
- thisTag = thisTag.parentNode;
- }
- return thisTag;
-};
-
-var isDropDownParts = function(target) {
- if(!target || target.tagName === 'HTML') { return false; }
- return (
- target.hasAttribute(DATA_TRIGGER) ||
- target.hasAttribute(DATA_DROPDOWN)
- );
-};
-
-module.exports = {
- toDataCamelCase: toDataCamelCase,
- t: t,
- camelize: camelize,
- closest: closest,
- isDropDownParts: isDropDownParts,
-};
-
-},{"./constants":1}],11:[function(require,module,exports){
-module.exports = function(callback) {
- return (function() {
- callback(this);
- }).call(null);
-};
-
-},{}]},{},[8])(8)
-});
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
deleted file mode 100644
index 020f8b4ac65..00000000000
--- a/app/assets/javascripts/droplab/droplab_ajax.js
+++ /dev/null
@@ -1,103 +0,0 @@
-/* eslint-disable */
-(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
-/* global droplab */
-
-require('../window')(function(w){
- function droplabAjaxException(message) {
- this.message = message;
- }
-
- w.droplabAjax = {
- _loadUrlData: function _loadUrlData(url) {
- var self = this;
- return new Promise(function(resolve, reject) {
- var xhr = new XMLHttpRequest;
- xhr.open('GET', url, true);
- xhr.onreadystatechange = function () {
- if(xhr.readyState === XMLHttpRequest.DONE) {
- if (xhr.status === 200) {
- var data = JSON.parse(xhr.responseText);
- self.cache[url] = data;
- return resolve(data);
- } else {
- return reject([xhr.responseText, xhr.status]);
- }
- }
- };
- xhr.send();
- });
- },
-
- _loadData: function _loadData(data, config, self) {
- if (config.loadingTemplate) {
- var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
-
- if (dataLoadingTemplate) {
- dataLoadingTemplate.outerHTML = self.listTemplate;
- }
- }
-
- if (!self.destroyed) {
- self.hook.list[config.method].call(self.hook.list, data);
- }
- },
-
- init: function init(hook) {
- var self = this;
- self.destroyed = false;
- self.cache = self.cache || {};
- var config = hook.config.droplabAjax;
- this.hook = hook;
-
- if (!config || !config.endpoint || !config.method) {
- return;
- }
-
- if (config.method !== 'setData' && config.method !== 'addData') {
- return;
- }
-
- if (config.loadingTemplate) {
- var dynamicList = hook.list.list.querySelector('[data-dynamic]');
-
- var loadingTemplate = document.createElement('div');
- loadingTemplate.innerHTML = config.loadingTemplate;
- loadingTemplate.setAttribute('data-loading-template', '');
-
- this.listTemplate = dynamicList.outerHTML;
- dynamicList.outerHTML = loadingTemplate.outerHTML;
- }
-
- if (self.cache[config.endpoint]) {
- self._loadData(self.cache[config.endpoint], config, self);
- } else {
- this._loadUrlData(config.endpoint)
- .then(function(d) {
- self._loadData(d, config, self);
- }, function(xhrError) {
- // TODO: properly handle errors due to XHR cancellation
- return;
- }).catch(function(e) {
- throw new droplabAjaxException(e.message || e);
- });
- }
- },
-
- destroy: function() {
- var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
- this.destroyed = true;
- if (this.listTemplate && dynamicList) {
- dynamicList.outerHTML = this.listTemplate;
- }
- }
- };
-});
-},{"../window":2}],2:[function(require,module,exports){
-module.exports = function(callback) {
- return (function() {
- callback(this);
- }).call(null);
-};
-
-},{}]},{},[1])(1)
-});
diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js
deleted file mode 100644
index 05eba7aef56..00000000000
--- a/app/assets/javascripts/droplab/droplab_ajax_filter.js
+++ /dev/null
@@ -1,164 +0,0 @@
-/* eslint-disable */
-(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
-/* global droplab */
-
-require('../window')(function(w){
- w.droplabAjaxFilter = {
- init: function(hook) {
- this.destroyed = false;
- this.hook = hook;
- this.notLoading();
-
- this.debounceTriggerWrapper = this.debounceTrigger.bind(this);
- this.hook.trigger.addEventListener('keydown.dl', this.debounceTriggerWrapper);
- this.hook.trigger.addEventListener('focus', this.debounceTriggerWrapper);
- this.trigger(true);
- },
-
- notLoading: function notLoading() {
- this.loading = false;
- },
-
- debounceTrigger: function debounceTrigger(e) {
- var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93];
- var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1;
- var focusEvent = e.type === 'focus';
-
- if (invalidKeyPressed || this.loading) {
- return;
- }
-
- if (this.timeout) {
- clearTimeout(this.timeout);
- }
-
- this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200);
- },
-
- trigger: function trigger(getEntireList) {
- var config = this.hook.config.droplabAjaxFilter;
- var searchValue = this.trigger.value;
-
- if (!config || !config.endpoint || !config.searchKey) {
- return;
- }
-
- if (config.searchValueFunction) {
- searchValue = config.searchValueFunction();
- }
-
- if (config.loadingTemplate && this.hook.list.data === undefined ||
- this.hook.list.data.length === 0) {
- var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
-
- var loadingTemplate = document.createElement('div');
- loadingTemplate.innerHTML = config.loadingTemplate;
- loadingTemplate.setAttribute('data-loading-template', true);
-
- this.listTemplate = dynamicList.outerHTML;
- dynamicList.outerHTML = loadingTemplate.outerHTML;
- }
-
- if (getEntireList) {
- searchValue = '';
- }
-
- if (config.searchKey === searchValue) {
- return this.list.show();
- }
-
- this.loading = true;
-
- var params = config.params || {};
- params[config.searchKey] = searchValue;
- var self = this;
- self.cache = self.cache || {};
- var url = config.endpoint + this.buildParams(params);
- var urlCachedData = self.cache[url];
-
- if (urlCachedData) {
- self._loadData(urlCachedData, config, self);
- } else {
- this._loadUrlData(url)
- .then(function(data) {
- self._loadData(data, config, self);
- }, function(xhrError) {
- // TODO: properly handle errors due to XHR cancellation
- return;
- });
- }
- },
-
- _loadUrlData: function _loadUrlData(url) {
- var self = this;
- return new Promise(function(resolve, reject) {
- var xhr = new XMLHttpRequest;
- xhr.open('GET', url, true);
- xhr.onreadystatechange = function () {
- if(xhr.readyState === XMLHttpRequest.DONE) {
- if (xhr.status === 200) {
- var data = JSON.parse(xhr.responseText);
- self.cache[url] = data;
- return resolve(data);
- } else {
- return reject([xhr.responseText, xhr.status]);
- }
- }
- };
- xhr.send();
- });
- },
-
- _loadData: function _loadData(data, config, self) {
- if (config.loadingTemplate && self.hook.list.data === undefined ||
- self.hook.list.data.length === 0) {
- const dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
-
- if (dataLoadingTemplate) {
- dataLoadingTemplate.outerHTML = self.listTemplate;
- }
- }
-
- if (!self.destroyed) {
- var hookListChildren = self.hook.list.list.children;
- var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic');
-
- if (onlyDynamicList && data.length === 0) {
- self.hook.list.hide();
- }
-
- self.hook.list.setData.call(self.hook.list, data);
- }
- self.notLoading();
- self.hook.list.currentIndex = 0;
- },
-
- buildParams: function(params) {
- if (!params) return '';
- var paramsArray = Object.keys(params).map(function(param) {
- return param + '=' + (params[param] || '');
- });
- return '?' + paramsArray.join('&');
- },
-
- destroy: function destroy() {
- if (this.timeout) {
- clearTimeout(this.timeout);
- }
-
- this.destroyed = true;
-
- this.hook.trigger.removeEventListener('keydown.dl', this.debounceTriggerWrapper);
- this.hook.trigger.removeEventListener('focus', this.debounceTriggerWrapper);
- }
- };
-});
-},{"../window":2}],2:[function(require,module,exports){
-module.exports = function(callback) {
- return (function() {
- callback(this);
- }).call(null);
-};
-
-},{}]},{},[1])(1)
-});
diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js
deleted file mode 100644
index 7f7d93f3e27..00000000000
--- a/app/assets/javascripts/droplab/droplab_filter.js
+++ /dev/null
@@ -1,76 +0,0 @@
-/* eslint-disable */
-(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.filter||(g.filter = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
-/* global droplab */
-
-require('../window')(function(w){
- w.droplabFilter = {
-
- keydownWrapper: function(e){
- var hiddenCount = 0;
- var dataHiddenCount = 0;
- var list = e.detail.hook.list;
- var data = list.data;
- var value = e.detail.hook.trigger.value.toLowerCase();
- var config = e.detail.hook.config.droplabFilter;
- var matches = [];
- var filterFunction;
- // will only work on dynamically set data
- if(!data){
- return;
- }
-
- if (config && config.filterFunction && typeof config.filterFunction === 'function') {
- filterFunction = config.filterFunction;
- } else {
- filterFunction = function(o){
- // cheap string search
- o.droplab_hidden = o[config.template].toLowerCase().indexOf(value) === -1;
- return o;
- };
- }
-
- dataHiddenCount = data.filter(function(o) {
- return !o.droplab_hidden;
- }).length;
-
- matches = data.map(function(o) {
- return filterFunction(o, value);
- });
-
- hiddenCount = matches.filter(function(o) {
- return !o.droplab_hidden;
- }).length;
-
- if (dataHiddenCount !== hiddenCount) {
- list.render(matches);
- list.currentIndex = 0;
- }
- },
-
- init: function init(hookInput) {
- var config = hookInput.config.droplabFilter;
-
- if (!config || (!config.template && !config.filterFunction)) {
- return;
- }
-
- this.hookInput = hookInput;
- this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper);
- this.hookInput.trigger.addEventListener('mousedown.dl', this.keydownWrapper);
- },
-
- destroy: function destroy(){
- this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper);
- this.hookInput.trigger.removeEventListener('mousedown.dl', this.keydownWrapper);
- }
- };
-});
-},{"../window":2}],2:[function(require,module,exports){
-module.exports = function(callback) {
- return (function() {
- callback(this);
- }).call(null);
-};
-
-},{}]},{},[1])(1)
-});
diff --git a/app/assets/javascripts/droplab/hook.js b/app/assets/javascripts/droplab/hook.js
new file mode 100644
index 00000000000..2f840083571
--- /dev/null
+++ b/app/assets/javascripts/droplab/hook.js
@@ -0,0 +1,22 @@
+/* eslint-disable */
+
+import DropDown from './drop_down';
+
+var Hook = function(trigger, list, plugins, config){
+ this.trigger = trigger;
+ this.list = new DropDown(list);
+ this.type = 'Hook';
+ this.event = 'click';
+ this.plugins = plugins || [];
+ this.config = config || {};
+ this.id = trigger.id;
+};
+
+Object.assign(Hook.prototype, {
+
+ addEvents: function(){},
+
+ constructor: Hook,
+});
+
+export default Hook;
diff --git a/app/assets/javascripts/droplab/hook_button.js b/app/assets/javascripts/droplab/hook_button.js
new file mode 100644
index 00000000000..be8aead1303
--- /dev/null
+++ b/app/assets/javascripts/droplab/hook_button.js
@@ -0,0 +1,65 @@
+/* eslint-disable */
+
+import Hook from './hook';
+
+var HookButton = function(trigger, list, plugins, config) {
+ Hook.call(this, trigger, list, plugins, config);
+
+ this.type = 'button';
+ this.event = 'click';
+
+ this.eventWrapper = {};
+
+ this.addEvents();
+ this.addPlugins();
+};
+
+HookButton.prototype = Object.create(Hook.prototype);
+
+Object.assign(HookButton.prototype, {
+ addPlugins: function() {
+ this.plugins.forEach(plugin => plugin.init(this));
+ },
+
+ clicked: function(e){
+ var buttonEvent = new CustomEvent('click.dl', {
+ detail: {
+ hook: this,
+ },
+ bubbles: true,
+ cancelable: true
+ });
+ e.target.dispatchEvent(buttonEvent);
+
+ this.list.toggle();
+ },
+
+ addEvents: function(){
+ this.eventWrapper.clicked = this.clicked.bind(this);
+ this.trigger.addEventListener('click', this.eventWrapper.clicked);
+ },
+
+ removeEvents: function(){
+ this.trigger.removeEventListener('click', this.eventWrapper.clicked);
+ },
+
+ restoreInitialState: function() {
+ this.list.list.innerHTML = this.list.initialState;
+ },
+
+ removePlugins: function() {
+ this.plugins.forEach(plugin => plugin.destroy());
+ },
+
+ destroy: function() {
+ this.restoreInitialState();
+
+ this.removeEvents();
+ this.removePlugins();
+ },
+
+ constructor: HookButton,
+});
+
+
+export default HookButton;
diff --git a/app/assets/javascripts/droplab/hook_input.js b/app/assets/javascripts/droplab/hook_input.js
new file mode 100644
index 00000000000..05082334045
--- /dev/null
+++ b/app/assets/javascripts/droplab/hook_input.js
@@ -0,0 +1,119 @@
+/* eslint-disable */
+
+import Hook from './hook';
+
+var HookInput = function(trigger, list, plugins, config) {
+ Hook.call(this, trigger, list, plugins, config);
+
+ this.type = 'input';
+ this.event = 'input';
+
+ this.eventWrapper = {};
+
+ this.addEvents();
+ this.addPlugins();
+};
+
+Object.assign(HookInput.prototype, {
+ addPlugins: function() {
+ this.plugins.forEach(plugin => plugin.init(this));
+ },
+
+ addEvents: function(){
+ this.eventWrapper.mousedown = this.mousedown.bind(this);
+ this.eventWrapper.input = this.input.bind(this);
+ this.eventWrapper.keyup = this.keyup.bind(this);
+ this.eventWrapper.keydown = this.keydown.bind(this);
+
+ this.trigger.addEventListener('mousedown', this.eventWrapper.mousedown);
+ this.trigger.addEventListener('input', this.eventWrapper.input);
+ this.trigger.addEventListener('keyup', this.eventWrapper.keyup);
+ this.trigger.addEventListener('keydown', this.eventWrapper.keydown);
+ },
+
+ removeEvents: function() {
+ this.hasRemovedEvents = true;
+
+ this.trigger.removeEventListener('mousedown', this.eventWrapper.mousedown);
+ this.trigger.removeEventListener('input', this.eventWrapper.input);
+ this.trigger.removeEventListener('keyup', this.eventWrapper.keyup);
+ this.trigger.removeEventListener('keydown', this.eventWrapper.keydown);
+ },
+
+ input: function(e) {
+ if(this.hasRemovedEvents) return;
+
+ this.list.show();
+
+ const inputEvent = new CustomEvent('input.dl', {
+ detail: {
+ hook: this,
+ text: e.target.value,
+ },
+ bubbles: true,
+ cancelable: true
+ });
+ e.target.dispatchEvent(inputEvent);
+ },
+
+ mousedown: function(e) {
+ if (this.hasRemovedEvents) return;
+
+ const mouseEvent = new CustomEvent('mousedown.dl', {
+ detail: {
+ hook: this,
+ text: e.target.value,
+ },
+ bubbles: true,
+ cancelable: true,
+ });
+ e.target.dispatchEvent(mouseEvent);
+ },
+
+ keyup: function(e) {
+ if (this.hasRemovedEvents) return;
+
+ this.keyEvent(e, 'keyup.dl');
+ },
+
+ keydown: function(e) {
+ if (this.hasRemovedEvents) return;
+
+ this.keyEvent(e, 'keydown.dl');
+ },
+
+ keyEvent: function(e, eventName) {
+ this.list.show();
+
+ const keyEvent = new CustomEvent(eventName, {
+ detail: {
+ hook: this,
+ text: e.target.value,
+ which: e.which,
+ key: e.key,
+ },
+ bubbles: true,
+ cancelable: true,
+ });
+ e.target.dispatchEvent(keyEvent);
+ },
+
+ restoreInitialState: function() {
+ this.list.list.innerHTML = this.list.initialState;
+ },
+
+ removePlugins: function() {
+ this.plugins.forEach(plugin => plugin.destroy());
+ },
+
+ destroy: function() {
+ this.restoreInitialState();
+
+ this.removeEvents();
+ this.removePlugins();
+
+ this.list.destroy();
+ }
+});
+
+export default HookInput;
diff --git a/app/assets/javascripts/droplab/keyboard.js b/app/assets/javascripts/droplab/keyboard.js
new file mode 100644
index 00000000000..36740a430e1
--- /dev/null
+++ b/app/assets/javascripts/droplab/keyboard.js
@@ -0,0 +1,113 @@
+/* eslint-disable */
+
+import { ACTIVE_CLASS } from './constants';
+
+const Keyboard = function () {
+ var currentKey;
+ var currentFocus;
+ var isUpArrow = false;
+ var isDownArrow = false;
+ var removeHighlight = function removeHighlight(list) {
+ var itemElements = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider)'), 0);
+ var listItems = [];
+ for(var i = 0; i < itemElements.length; i++) {
+ var listItem = itemElements[i];
+ listItem.classList.remove(ACTIVE_CLASS);
+
+ if (listItem.style.display !== 'none') {
+ listItems.push(listItem);
+ }
+ }
+ return listItems;
+ };
+
+ var setMenuForArrows = function setMenuForArrows(list) {
+ var listItems = removeHighlight(list);
+ if(list.currentIndex>0){
+ if(!listItems[list.currentIndex-1]){
+ list.currentIndex = list.currentIndex-1;
+ }
+
+ if (listItems[list.currentIndex-1]) {
+ var el = listItems[list.currentIndex-1];
+ var filterDropdownEl = el.closest('.filter-dropdown');
+ el.classList.add(ACTIVE_CLASS);
+
+ if (filterDropdownEl) {
+ var filterDropdownBottom = filterDropdownEl.offsetHeight;
+ var elOffsetTop = el.offsetTop - 30;
+
+ if (elOffsetTop > filterDropdownBottom) {
+ filterDropdownEl.scrollTop = elOffsetTop - filterDropdownBottom;
+ }
+ }
+ }
+ }
+ };
+
+ var mousedown = function mousedown(e) {
+ var list = e.detail.hook.list;
+ removeHighlight(list);
+ list.show();
+ list.currentIndex = 0;
+ isUpArrow = false;
+ isDownArrow = false;
+ };
+ var selectItem = function selectItem(list) {
+ var listItems = removeHighlight(list);
+ var currentItem = listItems[list.currentIndex-1];
+ var listEvent = new CustomEvent('click.dl', {
+ detail: {
+ list: list,
+ selected: currentItem,
+ data: currentItem.dataset,
+ },
+ });
+ list.list.dispatchEvent(listEvent);
+ list.hide();
+ }
+
+ var keydown = function keydown(e){
+ var typedOn = e.target;
+ var list = e.detail.hook.list;
+ var currentIndex = list.currentIndex;
+ isUpArrow = false;
+ isDownArrow = false;
+
+ if(e.detail.which){
+ currentKey = e.detail.which;
+ if(currentKey === 13){
+ selectItem(e.detail.hook.list);
+ return;
+ }
+ if(currentKey === 38) {
+ isUpArrow = true;
+ }
+ if(currentKey === 40) {
+ isDownArrow = true;
+ }
+ } else if(e.detail.key) {
+ currentKey = e.detail.key;
+ if(currentKey === 'Enter'){
+ selectItem(e.detail.hook.list);
+ return;
+ }
+ if(currentKey === 'ArrowUp') {
+ isUpArrow = true;
+ }
+ if(currentKey === 'ArrowDown') {
+ isDownArrow = true;
+ }
+ }
+ if(isUpArrow){ currentIndex--; }
+ if(isDownArrow){ currentIndex++; }
+ if(currentIndex < 0){ currentIndex = 0; }
+ list.currentIndex = currentIndex;
+ setMenuForArrows(e.detail.hook.list);
+ };
+
+ document.addEventListener('mousedown.dl', mousedown);
+ document.addEventListener('keydown.dl', keydown);
+};
+
+export default Keyboard;
diff --git a/app/assets/javascripts/droplab/plugins/ajax.js b/app/assets/javascripts/droplab/plugins/ajax.js
new file mode 100644
index 00000000000..12afe53ed76
--- /dev/null
+++ b/app/assets/javascripts/droplab/plugins/ajax.js
@@ -0,0 +1,65 @@
+/* eslint-disable */
+
+const Ajax = {
+ _loadUrlData: function _loadUrlData(url) {
+ var self = this;
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest;
+ xhr.open('GET', url, true);
+ xhr.onreadystatechange = function () {
+ if(xhr.readyState === XMLHttpRequest.DONE) {
+ if (xhr.status === 200) {
+ var data = JSON.parse(xhr.responseText);
+ self.cache[url] = data;
+ return resolve(data);
+ } else {
+ return reject([xhr.responseText, xhr.status]);
+ }
+ }
+ };
+ xhr.send();
+ });
+ },
+ _loadData: function _loadData(data, config, self) {
+ if (config.loadingTemplate) {
+ var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
+ if (dataLoadingTemplate) dataLoadingTemplate.outerHTML = self.listTemplate;
+ }
+
+ if (!self.destroyed) self.hook.list[config.method].call(self.hook.list, data);
+ },
+ init: function init(hook) {
+ var self = this;
+ self.destroyed = false;
+ self.cache = self.cache || {};
+ var config = hook.config.Ajax;
+ this.hook = hook;
+ if (!config || !config.endpoint || !config.method) {
+ return;
+ }
+ if (config.method !== 'setData' && config.method !== 'addData') {
+ return;
+ }
+ if (config.loadingTemplate) {
+ var dynamicList = hook.list.list.querySelector('[data-dynamic]');
+ var loadingTemplate = document.createElement('div');
+ loadingTemplate.innerHTML = config.loadingTemplate;
+ loadingTemplate.setAttribute('data-loading-template', '');
+ this.listTemplate = dynamicList.outerHTML;
+ dynamicList.outerHTML = loadingTemplate.outerHTML;
+ }
+ if (self.cache[config.endpoint]) {
+ self._loadData(self.cache[config.endpoint], config, self);
+ } else {
+ this._loadUrlData(config.endpoint)
+ .then(function(d) {
+ self._loadData(d, config, self);
+ }, config.onError).catch(config.onError);
+ }
+ },
+ destroy: function() {
+ this.destroyed = true;
+ }
+};
+
+export default Ajax;
diff --git a/app/assets/javascripts/droplab/plugins/ajax_filter.js b/app/assets/javascripts/droplab/plugins/ajax_filter.js
new file mode 100644
index 00000000000..cfd7e2ca189
--- /dev/null
+++ b/app/assets/javascripts/droplab/plugins/ajax_filter.js
@@ -0,0 +1,133 @@
+/* eslint-disable */
+
+const AjaxFilter = {
+ init: function(hook) {
+ this.destroyed = false;
+ this.hook = hook;
+ this.notLoading();
+
+ this.eventWrapper = {};
+ this.eventWrapper.debounceTrigger = this.debounceTrigger.bind(this);
+ this.hook.trigger.addEventListener('keydown.dl', this.eventWrapper.debounceTrigger);
+ this.hook.trigger.addEventListener('focus', this.eventWrapper.debounceTrigger);
+
+ this.trigger(true);
+ },
+
+ notLoading: function notLoading() {
+ this.loading = false;
+ },
+
+ debounceTrigger: function debounceTrigger(e) {
+ var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93];
+ var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1;
+ var focusEvent = e.type === 'focus';
+ if (invalidKeyPressed || this.loading) {
+ return;
+ }
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+ this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200);
+ },
+
+ trigger: function trigger(getEntireList) {
+ var config = this.hook.config.AjaxFilter;
+ var searchValue = this.trigger.value;
+ if (!config || !config.endpoint || !config.searchKey) {
+ return;
+ }
+ if (config.searchValueFunction) {
+ searchValue = config.searchValueFunction();
+ }
+ if (config.loadingTemplate && this.hook.list.data === undefined ||
+ this.hook.list.data.length === 0) {
+ var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ var loadingTemplate = document.createElement('div');
+ loadingTemplate.innerHTML = config.loadingTemplate;
+ loadingTemplate.setAttribute('data-loading-template', true);
+ this.listTemplate = dynamicList.outerHTML;
+ dynamicList.outerHTML = loadingTemplate.outerHTML;
+ }
+ if (getEntireList) {
+ searchValue = '';
+ }
+ if (config.searchKey === searchValue) {
+ return this.list.show();
+ }
+ this.loading = true;
+ var params = config.params || {};
+ params[config.searchKey] = searchValue;
+ var self = this;
+ self.cache = self.cache || {};
+ var url = config.endpoint + this.buildParams(params);
+ var urlCachedData = self.cache[url];
+ if (urlCachedData) {
+ self._loadData(urlCachedData, config, self);
+ } else {
+ this._loadUrlData(url)
+ .then(function(data) {
+ self._loadData(data, config, self);
+ }, config.onError).catch(config.onError);
+ }
+ },
+
+ _loadUrlData: function _loadUrlData(url) {
+ var self = this;
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest;
+ xhr.open('GET', url, true);
+ xhr.onreadystatechange = function () {
+ if(xhr.readyState === XMLHttpRequest.DONE) {
+ if (xhr.status === 200) {
+ var data = JSON.parse(xhr.responseText);
+ self.cache[url] = data;
+ return resolve(data);
+ } else {
+ return reject([xhr.responseText, xhr.status]);
+ }
+ }
+ };
+ xhr.send();
+ });
+ },
+
+ _loadData: function _loadData(data, config, self) {
+ const list = self.hook.list;
+ if (config.loadingTemplate && list.data === undefined ||
+ list.data.length === 0) {
+ const dataLoadingTemplate = list.list.querySelector('[data-loading-template]');
+ if (dataLoadingTemplate) {
+ dataLoadingTemplate.outerHTML = self.listTemplate;
+ }
+ }
+ if (!self.destroyed) {
+ var hookListChildren = list.list.children;
+ var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic');
+ if (onlyDynamicList && data.length === 0) {
+ list.hide();
+ }
+ list.setData.call(list, data);
+ }
+ self.notLoading();
+ list.currentIndex = 0;
+ },
+
+ buildParams: function(params) {
+ if (!params) return '';
+ var paramsArray = Object.keys(params).map(function(param) {
+ return param + '=' + (params[param] || '');
+ });
+ return '?' + paramsArray.join('&');
+ },
+
+ destroy: function destroy() {
+ if (this.timeout)clearTimeout(this.timeout);
+ this.destroyed = true;
+
+ this.hook.trigger.removeEventListener('keydown.dl', this.eventWrapper.debounceTrigger);
+ this.hook.trigger.removeEventListener('focus', this.eventWrapper.debounceTrigger);
+ }
+};
+
+export default AjaxFilter;
diff --git a/app/assets/javascripts/droplab/plugins/filter.js b/app/assets/javascripts/droplab/plugins/filter.js
new file mode 100644
index 00000000000..d6a1aadd49c
--- /dev/null
+++ b/app/assets/javascripts/droplab/plugins/filter.js
@@ -0,0 +1,95 @@
+/* eslint-disable */
+
+const Filter = {
+ keydown: function(e){
+ if (this.destroyed) return;
+
+ var hiddenCount = 0;
+ var dataHiddenCount = 0;
+
+ var list = e.detail.hook.list;
+ var data = list.data;
+ var value = e.detail.hook.trigger.value.toLowerCase();
+ var config = e.detail.hook.config.Filter;
+ var matches = [];
+ var filterFunction;
+ // will only work on dynamically set data
+ if(!data){
+ return;
+ }
+
+ if (config && config.filterFunction && typeof config.filterFunction === 'function') {
+ filterFunction = config.filterFunction;
+ } else {
+ filterFunction = function(o){
+ // cheap string search
+ o.droplab_hidden = o[config.template].toLowerCase().indexOf(value) === -1;
+ return o;
+ };
+ }
+
+ dataHiddenCount = data.filter(function(o) {
+ return !o.droplab_hidden;
+ }).length;
+
+ matches = data.map(function(o) {
+ return filterFunction(o, value);
+ });
+
+ hiddenCount = matches.filter(function(o) {
+ return !o.droplab_hidden;
+ }).length;
+
+ if (dataHiddenCount !== hiddenCount) {
+ list.setData(matches);
+ list.currentIndex = 0;
+ }
+ },
+
+ debounceKeydown: function debounceKeydown(e) {
+ if ([
+ 13, // enter
+ 16, // shift
+ 17, // ctrl
+ 18, // alt
+ 20, // caps lock
+ 37, // left arrow
+ 38, // up arrow
+ 39, // right arrow
+ 40, // down arrow
+ 91, // left window
+ 92, // right window
+ 93, // select
+ ].indexOf(e.detail.which || e.detail.keyCode) > -1) return;
+
+ if (this.timeout) clearTimeout(this.timeout);
+ this.timeout = setTimeout(this.keydown.bind(this, e), 200);
+ },
+
+ init: function init(hook) {
+ var config = hook.config.Filter;
+
+ if (!config || !config.template) return;
+
+ this.hook = hook;
+ this.destroyed = false;
+
+ this.eventWrapper = {};
+ this.eventWrapper.debounceKeydown = this.debounceKeydown.bind(this);
+
+ this.hook.trigger.addEventListener('keydown.dl', this.eventWrapper.debounceKeydown);
+ this.hook.trigger.addEventListener('mousedown.dl', this.eventWrapper.debounceKeydown);
+
+ this.debounceKeydown({ detail: { hook: this.hook } });
+ },
+
+ destroy: function destroy() {
+ if (this.timeout) clearTimeout(this.timeout);
+ this.destroyed = true;
+
+ this.hook.trigger.removeEventListener('keydown.dl', this.eventWrapper.debounceKeydown);
+ this.hook.trigger.removeEventListener('mousedown.dl', this.eventWrapper.debounceKeydown);
+ }
+};
+
+export default Filter;
diff --git a/app/assets/javascripts/droplab/plugins/input_setter.js b/app/assets/javascripts/droplab/plugins/input_setter.js
new file mode 100644
index 00000000000..b1716c72810
--- /dev/null
+++ b/app/assets/javascripts/droplab/plugins/input_setter.js
@@ -0,0 +1,51 @@
+/* eslint-disable */
+
+const InputSetter = {
+ init(hook) {
+ this.hook = hook;
+ this.destroyed = false;
+ this.config = hook.config.InputSetter || (this.hook.config.InputSetter = {});
+
+ this.eventWrapper = {};
+
+ this.addEvents();
+ },
+
+ addEvents() {
+ this.eventWrapper.setInputs = this.setInputs.bind(this);
+ this.hook.list.list.addEventListener('click.dl', this.eventWrapper.setInputs);
+ },
+
+ removeEvents() {
+ this.hook.list.list.removeEventListener('click.dl', this.eventWrapper.setInputs);
+ },
+
+ setInputs(e) {
+ if (this.destroyed) return;
+
+ const selectedItem = e.detail.selected;
+
+ if (!Array.isArray(this.config)) this.config = [this.config];
+
+ this.config.forEach(config => this.setInput(config, selectedItem));
+ },
+
+ setInput(config, selectedItem) {
+ const input = config.input || this.hook.trigger;
+ const newValue = selectedItem.getAttribute(config.valueAttribute);
+
+ if (input.tagName === 'INPUT') {
+ input.value = newValue;
+ } else {
+ input.textContent = newValue;
+ }
+ },
+
+ destroy() {
+ this.destroyed = true;
+
+ this.removeEvents();
+ },
+};
+
+export default InputSetter;
diff --git a/app/assets/javascripts/droplab/utils.js b/app/assets/javascripts/droplab/utils.js
new file mode 100644
index 00000000000..c149a33a1e9
--- /dev/null
+++ b/app/assets/javascripts/droplab/utils.js
@@ -0,0 +1,38 @@
+/* eslint-disable */
+
+import { DATA_TRIGGER, DATA_DROPDOWN } from './constants';
+
+const utils = {
+ toCamelCase(attr) {
+ return this.camelize(attr.split('-').slice(1).join(' '));
+ },
+
+ t(s, d) {
+ for (const p in d) {
+ if (Object.prototype.hasOwnProperty.call(d, p)) {
+ s = s.replace(new RegExp(`{{${p}}}`, 'g'), d[p]);
+ }
+ }
+ return s;
+ },
+
+ camelize(str) {
+ return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => {
+ return index === 0 ? letter.toLowerCase() : letter.toUpperCase();
+ }).replace(/\s+/g, '');
+ },
+
+ closest(thisTag, stopTag) {
+ while (thisTag && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML') {
+ thisTag = thisTag.parentNode;
+ }
+ return thisTag;
+ },
+
+ isDropDownParts(target) {
+ if (!target || target.tagName === 'HTML') return false;
+ return target.hasAttribute(DATA_TRIGGER) || target.hasAttribute(DATA_DROPDOWN);
+ },
+};
+
+export default utils;
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 3f041172ff3..59d6508fc02 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -55,14 +55,19 @@ window.FilesCommentButton = (function() {
textFileElement = this.getTextFileElement($currentTarget);
buttonParentElement.append(this.buildButton({
+ discussionID: lineContentElement.attr('data-discussion-id'),
+ lineType: lineContentElement.attr('data-line-type'),
+
noteableType: textFileElement.attr('data-noteable-type'),
noteableID: textFileElement.attr('data-noteable-id'),
commitID: textFileElement.attr('data-commit-id'),
noteType: lineContentElement.attr('data-note-type'),
- position: lineContentElement.attr('data-position'),
- lineType: lineContentElement.attr('data-line-type'),
- discussionID: lineContentElement.attr('data-discussion-id'),
- lineCode: lineContentElement.attr('data-line-code')
+
+ // LegacyDiffNote
+ lineCode: lineContentElement.attr('data-line-code'),
+
+ // DiffNote
+ position: lineContentElement.attr('data-position')
}));
};
@@ -76,14 +81,19 @@ window.FilesCommentButton = (function() {
FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
return $commentButtonTemplate.clone().attr({
+ 'data-discussion-id': buttonAttributes.discussionID,
+ 'data-line-type': buttonAttributes.lineType,
+
'data-noteable-type': buttonAttributes.noteableType,
'data-noteable-id': buttonAttributes.noteableID,
'data-commit-id': buttonAttributes.commitID,
'data-note-type': buttonAttributes.noteType,
+
+ // LegacyDiffNote
'data-line-code': buttonAttributes.lineCode,
- 'data-position': buttonAttributes.position,
- 'data-discussion-id': buttonAttributes.discussionID,
- 'data-line-type': buttonAttributes.lineType
+
+ // DiffNote
+ 'data-position': buttonAttributes.position
});
};
@@ -121,7 +131,7 @@ window.FilesCommentButton = (function() {
};
FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
- return lineContentElement.attr('data-discussion-id') && lineContentElement.attr('data-discussion-id') !== '';
+ return lineContentElement.attr('data-note-type') && lineContentElement.attr('data-note-type') !== '';
};
return FilesCommentButton;
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 64d7153e547..79314dd9c6f 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -1,13 +1,13 @@
-require('./filtered_search_dropdown');
+import Filter from '~/droplab/plugins/filter';
-/* global droplabFilter */
+require('./filtered_search_dropdown');
(() => {
class DropdownHint extends gl.FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter) {
super(droplab, dropdown, input, filter);
this.config = {
- droplabFilter: {
+ Filter: {
template: 'hint',
filterFunction: gl.DropdownUtils.filterHint.bind(null, input),
},
@@ -55,7 +55,6 @@ require('./filtered_search_dropdown');
renderContent() {
const dropdownData = [];
-
[].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
const { icon, hint, tag, type } = dropdownMenu.dataset;
if (icon && hint && tag) {
@@ -69,12 +68,12 @@ require('./filtered_search_dropdown');
}
});
- this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config);
+ this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config);
this.droplab.setData(this.hookId, dropdownData);
}
init() {
- this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init();
+ this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init();
}
}
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index b3dc3e502c5..6296965b911 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -1,7 +1,9 @@
-require('./filtered_search_dropdown');
+/* global Flash */
+
+import Ajax from '~/droplab/plugins/ajax';
+import Filter from '~/droplab/plugins/filter';
-/* global droplabAjax */
-/* global droplabFilter */
+require('./filtered_search_dropdown');
(() => {
class DropdownNonUser extends gl.FilteredSearchDropdown {
@@ -9,13 +11,19 @@ require('./filtered_search_dropdown');
super(droplab, dropdown, input, filter);
this.symbol = symbol;
this.config = {
- droplabAjax: {
+ Ajax: {
endpoint,
method: 'setData',
loadingTemplate: this.loadingTemplate,
+ onError() {
+ /* eslint-disable no-new */
+ new Flash('An error occured fetching the dropdown data.');
+ /* eslint-enable no-new */
+ },
},
- droplabFilter: {
+ Filter: {
filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
+ template: 'title',
},
};
}
@@ -29,13 +37,13 @@ require('./filtered_search_dropdown');
renderContent(forceShowList = false) {
this.droplab
- .changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config);
+ .changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config);
super.renderContent(forceShowList);
}
init() {
this.droplab
- .addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init();
+ .addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
}
}
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index 04e2afad02f..38b5d315bcf 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -1,13 +1,15 @@
-require('./filtered_search_dropdown');
+/* global Flash */
+
+import AjaxFilter from '~/droplab/plugins/ajax_filter';
-/* global droplabAjaxFilter */
+require('./filtered_search_dropdown');
(() => {
class DropdownUser extends gl.FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter) {
super(droplab, dropdown, input, filter);
this.config = {
- droplabAjaxFilter: {
+ AjaxFilter: {
endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
searchKey: 'search',
params: {
@@ -18,6 +20,11 @@ require('./filtered_search_dropdown');
},
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 */
+ },
},
};
}
@@ -28,7 +35,7 @@ require('./filtered_search_dropdown');
}
renderContent(forceShowList = false) {
- this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config);
+ this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config);
super.renderContent(forceShowList);
}
@@ -56,7 +63,7 @@ require('./filtered_search_dropdown');
}
init() {
- this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init();
+ this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init();
}
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index e7bf530d343..d58eeeebf81 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -4,7 +4,7 @@
class FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter) {
this.droplab = droplab;
- this.hookId = input && input.getAttribute('data-id');
+ this.hookId = input && input.id;
this.input = input;
this.filter = filter;
this.dropdown = dropdown;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index 5fbe0450bb8..ec481b9ef97 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -1,4 +1,4 @@
-/* global DropLab */
+import DropLab from '~/droplab/drop_lab';
import FilteredSearchContainer from './container';
(() => {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 2a8a6b81b3f..b93a8f1d322 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -154,7 +154,7 @@ import eventHub from './event_hub';
if (e.keyCode === 13) {
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
const dropdownEl = dropdown.element;
- const activeElements = dropdownEl.querySelectorAll('.dropdown-active');
+ const activeElements = dropdownEl.querySelectorAll('.droplab-item-active');
e.preventDefault();
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index e7c98e16581..ff10f19a4fe 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -29,7 +29,8 @@ GLForm.prototype.setupForm = function() {
this.form.find('.div-dropzone').remove();
this.form.addClass('gfm-form');
// remove notify commit author checkbox for non-commit notes
- gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
+ gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
+
gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
new DropzoneInput(this.form);
autosize(this.textarea);
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 5b50bc62876..3f92d4ee6cf 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -75,12 +75,6 @@ import './u2f/error';
import './u2f/register';
import './u2f/util';
-// droplab
-import './droplab/droplab';
-import './droplab/droplab_ajax';
-import './droplab/droplab_ajax_filter';
-import './droplab/droplab_filter';
-
// everything else
import './abuse_reports';
import './activities';
@@ -285,7 +279,7 @@ $(function () {
// Disable form buttons while a form is submitting
$body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) {
var buttons;
- buttons = $('[type="submit"]', this);
+ buttons = $('[type="submit"], .js-disable-on-submit', this);
switch (e.type) {
case 'ajax:beforeSend':
case 'submit':
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 1d563c63f39..66e89b7607a 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -5,6 +5,7 @@
/* global mrRefreshWidgetUrl */
import Cookies from 'js-cookie';
+import CommentTypeToggle from './comment_type_toggle';
require('./autosave');
window.autosize = require('vendor/autosize');
@@ -110,7 +111,6 @@ require('./task_list');
$(document).on("visibilitychange", this.visibilityChange);
// when issue status changes, we need to refresh data
$(document).on("issuable:change", this.refresh);
-
// when a key is clicked on the notes
return $(document).on("keydown", ".js-note-text", this.keydownNoteText);
};
@@ -137,6 +137,18 @@ require('./task_list');
$(document).off("click", '.system-note-commit-list-toggler');
};
+ Notes.prototype.initCommentTypeToggle = function (form) {
+ this.commentTypeToggle = new CommentTypeToggle(
+ form.querySelector('.js-comment-type-dropdown .dropdown-toggle'),
+ form.querySelector('.js-comment-type-dropdown .dropdown-menu'),
+ form.querySelector('#note_type'),
+ form.querySelector('.js-comment-type-dropdown .js-comment-submit-button'),
+ form.querySelector('.js-note-target-close'),
+ );
+
+ this.commentTypeToggle.initDroplab();
+ };
+
Notes.prototype.keydownNoteText = function(e) {
var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText;
if (gl.utils.isMetaKey(e)) {
@@ -192,7 +204,7 @@ require('./task_list');
};
Notes.prototype.refresh = function() {
- if (!document.hidden && document.URL.indexOf(this.noteable_url) === 0) {
+ if (!document.hidden) {
return this.getContent();
}
};
@@ -213,11 +225,7 @@ require('./task_list');
_this.last_fetched_at = data.last_fetched_at;
_this.setPollingInterval(data.notes.length);
return $.each(notes, function(i, note) {
- if (note.discussion_html != null) {
- return _this.renderDiscussionNote(note);
- } else {
- return _this.renderNote(note);
- }
+ _this.renderNote(note);
});
};
})(this)
@@ -276,8 +284,12 @@ require('./task_list');
Note: for rendering inline notes use renderDiscussionNote
*/
- Notes.prototype.renderNote = function(note) {
+ Notes.prototype.renderNote = function(note, $form) {
var $notesList;
+ if (note.discussion_html != null) {
+ return this.renderDiscussionNote(note, $form);
+ }
+
if (!note.valid) {
if (note.errors.commands_only) {
new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
@@ -317,16 +329,13 @@ require('./task_list');
Note: for rendering inline notes use renderDiscussionNote
*/
- Notes.prototype.renderDiscussionNote = function(note) {
+ Notes.prototype.renderDiscussionNote = function(note, $form) {
var discussionContainer, form, note_html, row, lineType, diffAvatarContainer;
if (!this.isNewNote(note)) {
return;
}
this.note_ids.push(note.id);
- form = $("#new-discussion-note-form-" + note.discussion_id);
- if ((note.original_discussion_id != null) && form.length === 0) {
- form = $("#new-discussion-note-form-" + note.original_discussion_id);
- }
+ form = $form || $(".js-discussion-note-form[data-discussion-id='" + note.discussion_id + "']");
row = form.closest("tr");
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
@@ -334,8 +343,8 @@ require('./task_list');
note_html.renderGFM();
// is this the first note of discussion?
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
- if ((note.original_discussion_id != null) && discussionContainer.length === 0) {
- discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']");
+ if (!discussionContainer.length) {
+ discussionContainer = form.closest('.discussion').find('.notes');
}
if (discussionContainer.length === 0) {
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) {
@@ -363,7 +372,7 @@ require('./task_list');
// Add note to 'Changes' page discussions
discussionContainer.append(note_html);
// Init discussion on 'Discussion' page if it is merge request page
- if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) {
+ if ($('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) {
$('ul.main-notes-list').append(note.discussion_html).renderGFM();
}
} else {
@@ -371,7 +380,7 @@ require('./task_list');
discussionContainer.append(note_html);
}
- if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_id) {
+ if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_resolvable) {
gl.diffNotesCompileComponents();
this.renderDiscussionAvatar(diffAvatarContainer, note);
}
@@ -455,9 +464,14 @@ require('./task_list');
form.addClass("js-main-target-form");
form.find("#note_line_code").remove();
form.find("#note_position").remove();
- form.find("#note_type").remove();
+ form.find("#note_type").val('');
+ form.find("#in_reply_to_discussion_id").remove();
form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove();
- return this.parentTimeline = form.parents('.timeline');
+ this.parentTimeline = form.parents('.timeline');
+
+ if (form.length) {
+ this.initCommentTypeToggle(form.get(0));
+ }
};
/*
@@ -470,10 +484,24 @@ require('./task_list');
*/
Notes.prototype.setupNoteForm = function(form) {
- var textarea;
+ var textarea, key;
new gl.GLForm(form);
textarea = form.find(".js-note-text");
- return new Autosave(textarea, ["Note", form.find("#note_noteable_type").val(), form.find("#note_noteable_id").val(), form.find("#note_commit_id").val(), form.find("#note_type").val(), form.find("#note_line_code").val(), form.find("#note_position").val()]);
+ key = [
+ "Note",
+ form.find("#note_noteable_type").val(),
+ form.find("#note_noteable_id").val(),
+ form.find("#note_commit_id").val(),
+ form.find("#note_type").val(),
+ form.find("#in_reply_to_discussion_id").val(),
+
+ // LegacyDiffNote
+ form.find("#note_line_code").val(),
+
+ // DiffNote
+ form.find("#note_position").val()
+ ];
+ return new Autosave(textarea, key);
};
/*
@@ -510,7 +538,7 @@ require('./task_list');
}
}
- this.renderDiscussionNote(note);
+ this.renderNote(note, $form);
// cleanup after successfully creating a diff/discussion note
this.removeDiscussionNoteForm($form);
};
@@ -656,7 +684,7 @@ require('./task_list');
return function(i, el) {
var note, notes;
note = $(el);
- notes = note.closest(".notes");
+ notes = note.closest(".discussion-notes");
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
if (gl.diffNoteApps[noteElId]) {
@@ -673,14 +701,13 @@ require('./task_list');
// "Discussions" tab
notes.closest(".timeline-entry").remove();
- if (!_this.isParallelView() || notesTr.find('.note').length === 0) {
- // "Changes" tab / commit view
- notesTr.remove();
+ // The notes tr can contain multiple lists of notes, like on the parallel diff
+ if (notesTr.find('.discussion-notes').length > 1) {
+ notes.remove();
} else {
- notes.closest('.content').empty();
+ notesTr.remove();
}
}
- return note.remove();
};
})(this));
// Decrement the "Discussions" counter only once
@@ -711,7 +738,7 @@ require('./task_list');
Notes.prototype.replyToDiscussionNote = function(e) {
var form, replyLink;
- form = this.formClone.clone();
+ form = this.cleanForm(this.formClone.clone());
replyLink = $(e.target).closest(".js-discussion-reply-button");
// insert the form after the button
replyLink
@@ -727,29 +754,44 @@ require('./task_list');
Sets some hidden fields in the form.
- Note: dataHolder must have the "discussionId", "lineCode", "noteableType"
- and "noteableId" data attributes set.
+ Note: dataHolder must have the "discussionId" and "lineCode" data attributes set.
*/
Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) {
// setup note target
- form.attr('id', "new-discussion-note-form-" + (dataHolder.data("discussionId")));
+ var discussionID = dataHolder.data("discussionId");
+
+ if (discussionID) {
+ form.attr("data-discussion-id", discussionID);
+ form.find("#in_reply_to_discussion_id").val(discussionID);
+ }
+
form.attr("data-line-code", dataHolder.data("lineCode"));
- form.find("#note_type").val(dataHolder.data("noteType"));
form.find("#line_type").val(dataHolder.data("lineType"));
+
+ form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
+ form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
form.find("#note_commit_id").val(dataHolder.data("commitId"));
+ form.find("#note_type").val(dataHolder.data("noteType"));
+
+ // LegacyDiffNote
form.find("#note_line_code").val(dataHolder.data("lineCode"));
+
+ // DiffNote
form.find("#note_position").val(dataHolder.attr("data-position"));
- form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
- form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
+
form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text'));
form.find('.js-note-target-close').remove();
+ form.find('.js-note-new-discussion').remove();
this.setupNoteForm(form);
+ form
+ .removeClass('js-main-target-form')
+ .addClass("discussion-form js-discussion-note-form");
+
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
var $commentBtn = form.find('comment-and-resolve-btn');
- $commentBtn
- .attr(':discussion-id', "'" + dataHolder.data('discussionId') + "'");
+ $commentBtn.attr(':discussion-id', "'" + discussionID + "'");
gl.diffNotesCompileComponents();
}
@@ -757,10 +799,7 @@ require('./task_list');
form.find(".js-note-text").focus();
form
.find('.js-comment-resolve-button')
- .attr('data-discussion-id', dataHolder.data('discussionId'));
- form
- .removeClass('js-main-target-form')
- .addClass("discussion-form js-discussion-note-form");
+ .attr('data-discussion-id', discussionID);
};
/*
@@ -823,7 +862,7 @@ require('./task_list');
}
if (addForm) {
- newForm = this.formClone.clone();
+ newForm = this.cleanForm(this.formClone.clone());
newForm.appendTo(notesContent);
// show the form
return this.setupDiscussionNoteForm($link, newForm);
@@ -902,7 +941,7 @@ require('./task_list');
discardbtn = form.find('.js-note-discard');
if (textarea.val().trim().length > 0) {
reopentext = reopenbtn.data('alternative-text');
- closetext = closebtn.data('alternative-text');
+ closetext = closebtn.attr('data-alternative-text');
if (reopenbtn.text() !== reopentext) {
reopenbtn.text(reopentext);
}
@@ -1009,6 +1048,20 @@ require('./task_list');
});
};
+ Notes.prototype.cleanForm = function($form) {
+ // Remove JS classes that are not needed here
+ $form
+ .find('.js-comment-type-dropdown')
+ .removeClass('btn-group');
+
+ // Remove dropdown
+ $form
+ .find('.dropdown-menu')
+ .remove();
+
+ return $form;
+ };
+
return Notes;
})();
}).call(window);
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 484df6214d3..8be2e3fc70c 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -446,7 +446,7 @@
}
}
-.filter-dropdown-item.dropdown-active {
+.filter-dropdown-item.droplab-item-active {
.btn {
@extend %filter-dropdown-item-btn-hover;
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 927bf9805ce..09c178c5a14 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -310,3 +310,58 @@
margin-bottom: 10px;
}
}
+
+.comment-type-dropdown {
+ .dropdown-toggle .fa {
+ color: $white-light;
+ padding-right: 2px;
+ margin-top: 2px;
+ pointer-events: none;
+ }
+
+ .dropdown-menu {
+ top: initial;
+ bottom: 40px;
+ width: 298px;
+ }
+
+ .description {
+ display: inline-block;
+ white-space: normal;
+ margin-left: 8px;
+ padding-right: 33px;
+ }
+
+ li {
+ white-space: nowrap;
+ border-radius: 0;
+ cursor: pointer;
+ padding-top: 6px;
+
+ &:hover,
+ &:focus {
+ background-color: $dropdown-hover-color;
+ color: $white-light;
+ }
+
+ &.droplab-item-selected i {
+ visibility: visible;
+ }
+
+ i {
+ visibility: hidden;
+ }
+ }
+
+ i {
+ display: inline-block;
+ vertical-align: top;
+ padding-top: 2px;
+ }
+
+ .divider {
+ margin: 0 8px;
+ padding: 0;
+ border-top: $gray-darkest;
+ }
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 603ef461ffe..191e3ded8c3 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -294,6 +294,18 @@ ul.notes {
border-width: 1px;
}
+ .discussion-notes {
+ &:not(:first-child) {
+ border-top: 1px solid $white-normal;
+ margin-top: 20px;
+ }
+
+ &:not(:last-child) {
+ border-bottom: 1px solid $white-normal;
+ margin-bottom: 20px;
+ }
+ }
+
.notes {
background-color: $white-light;
}
diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb
new file mode 100644
index 00000000000..dd21066ac13
--- /dev/null
+++ b/app/controllers/concerns/renders_notes.rb
@@ -0,0 +1,20 @@
+module RendersNotes
+ def prepare_notes_for_rendering(notes)
+ preload_noteable_for_regular_notes(notes)
+ preload_max_access_for_authors(notes, @project)
+ Banzai::NoteRenderer.render(notes, @project, current_user)
+
+ notes
+ end
+
+ private
+
+ def preload_max_access_for_authors(notes, project)
+ user_ids = notes.map(&:author_id)
+ project.team.max_member_access_for_user_ids(user_ids)
+ end
+
+ def preload_noteable_for_regular_notes(notes)
+ ActiveRecord::Associations::Preloader.new.preload(notes.reject(&:for_commit?), :noteable)
+ end
+end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index cc67f688d51..8362bfc39dd 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -2,6 +2,7 @@
#
# Not to be confused with CommitsController, plural.
class Projects::CommitController < Projects::ApplicationController
+ include RendersNotes
include CreatesCommit
include DiffForPath
include DiffHelper
@@ -111,22 +112,19 @@ class Projects::CommitController < Projects::ApplicationController
end
def define_note_vars
- @grouped_diff_discussions = commit.notes.grouped_diff_discussions
- @notes = commit.notes.non_diff_notes.fresh
-
- Banzai::NoteRenderer.render(
- @grouped_diff_discussions.values.flat_map(&:notes) + @notes,
- @project,
- current_user,
- )
-
+ @noteable = @commit
@note = @project.build_commit_note(commit)
- @noteable = @commit
- @comments_target = {
+ @new_diff_note_attrs = {
noteable_type: 'Commit',
commit_id: @commit.id
}
+
+ @grouped_diff_discussions = commit.grouped_diff_discussions
+ @discussions = commit.discussions
+
+ @notes = (@grouped_diff_discussions.values.flatten + @discussions).flat_map(&:notes)
+ @notes = prepare_notes_for_rendering(@notes)
end
def assign_change_commit_vars
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index 1349b015a63..f4a18a5e8f7 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -28,7 +28,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
end
def discussion
- @discussion ||= @merge_request.find_diff_discussion(params[:id]) || render_404
+ @discussion ||= @merge_request.find_discussion(params[:id]) || render_404
end
def authorize_resolve_discussion!
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index a50e16fa4ff..cbf67137261 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -1,5 +1,5 @@
class Projects::IssuesController < Projects::ApplicationController
- include NotesHelper
+ include RendersNotes
include ToggleSubscriptionAction
include IssuableActions
include ToggleAwardEmoji
@@ -84,15 +84,11 @@ class Projects::IssuesController < Projects::ApplicationController
end
def show
- raw_notes = @issue.notes.inc_relations_for_view.fresh
-
- @notes = Banzai::NoteRenderer.
- render(raw_notes, @project, current_user, @path, @project_wiki, @ref)
-
- @note = @project.notes.new(noteable: @issue)
@noteable = @issue
+ @note = @project.notes.new(noteable: @issue)
- preload_max_access_for_authors(@notes, @project)
+ @discussions = @issue.discussions
+ @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
respond_to do |format|
format.html
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index c337534b297..f5bc75d02c9 100755
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -3,7 +3,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
include DiffForPath
include DiffHelper
include IssuableActions
- include NotesHelper
+ include RendersNotes
include ToggleAwardEmoji
include IssuableCollections
@@ -570,20 +570,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@note = @project.notes.new(noteable: @merge_request)
@discussions = @merge_request.discussions
-
- preload_noteable_for_regular_notes(@discussions.flat_map(&:notes))
-
- # This is not executed lazily
- @notes = Banzai::NoteRenderer.render(
- @discussions.flat_map(&:notes),
- @project,
- current_user,
- @path,
- @project_wiki,
- @ref
- )
-
- preload_max_access_for_authors(@notes, @project)
+ @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
end
def define_widget_vars
@@ -596,22 +583,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def define_diff_comment_vars
- @comments_target = {
+ @new_diff_note_attrs = {
noteable_type: 'MergeRequest',
noteable_id: @merge_request.id
}
@use_legacy_diff_notes = !@merge_request.has_complete_diff_refs?
- @grouped_diff_discussions = @merge_request.notes.inc_relations_for_view.grouped_diff_discussions
-
- Banzai::NoteRenderer.render(
- @grouped_diff_discussions.values.flat_map(&:notes),
- @project,
- current_user,
- @path,
- @project_wiki,
- @ref
- )
+
+ @grouped_diff_discussions = @merge_request.grouped_diff_discussions
+ @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes))
end
def define_pipelines_vars
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index d00177e7612..405ea3c0a4f 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -1,4 +1,5 @@
class Projects::NotesController < Projects::ApplicationController
+ include RendersNotes
include ToggleAwardEmoji
# Authorize
@@ -6,13 +7,15 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
- before_action :find_current_user_notes, only: [:index]
def index
current_fetched_at = Time.now.to_i
notes_json = { notes: [], last_fetched_at: current_fetched_at }
+ @notes = notes_finder.execute.inc_relations_for_view
+ @notes = prepare_notes_for_rendering(@notes)
+
@notes.each do |note|
next if note.cross_reference_not_visible_for?(current_user)
@@ -23,7 +26,10 @@ class Projects::NotesController < Projects::ApplicationController
end
def create
- create_params = note_params.merge(merge_request_diff_head_sha: params[:merge_request_diff_head_sha])
+ create_params = note_params.merge(
+ merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
+ in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
+ )
@note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note)
@@ -111,6 +117,17 @@ class Projects::NotesController < Projects::ApplicationController
)
end
+ def discussion_html(discussion)
+ return if discussion.individual_note?
+
+ render_to_string(
+ "discussions/_discussion",
+ layout: false,
+ formats: [:html],
+ locals: { discussion: discussion }
+ )
+ end
+
def diff_discussion_html(discussion)
return unless discussion.diff_discussion?
@@ -118,13 +135,13 @@ class Projects::NotesController < Projects::ApplicationController
template = "discussions/_parallel_diff_discussion"
locals =
if params[:line_type] == 'old'
- { discussion_left: discussion, discussion_right: nil }
+ { discussions_left: [discussion], discussions_right: nil }
else
- { discussion_left: nil, discussion_right: discussion }
+ { discussions_left: nil, discussions_right: [discussion] }
end
else
template = "discussions/_diff_discussion"
- locals = { discussion: discussion }
+ locals = { discussions: [discussion] }
end
render_to_string(
@@ -135,54 +152,28 @@ class Projects::NotesController < Projects::ApplicationController
)
end
- def discussion_html(discussion)
- return unless discussion.diff_discussion?
-
- render_to_string(
- "discussions/_discussion",
- layout: false,
- formats: [:html],
- locals: { discussion: discussion }
- )
- end
-
def note_json(note)
attrs = {
- id: note.id
+ commands_changes: note.commands_changes
}
if note.persisted?
- Banzai::NoteRenderer.render([note], @project, current_user)
-
attrs.merge!(
valid: true,
- discussion_id: note.discussion_id,
+ id: note.id,
+ discussion_id: note.discussion_id(noteable),
html: note_html(note),
note: note.note
)
- if note.diff_note?
- discussion = note.to_discussion
-
+ discussion = note.to_discussion(noteable)
+ unless discussion.individual_note?
attrs.merge!(
+ discussion_resolvable: discussion.resolvable?,
+
diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion)
)
-
- # The discussion_id is used to add the comment to the correct discussion
- # element on the merge request page. Among other things, the discussion_id
- # contains the sha of head commit of the merge request.
- # When new commits are pushed into the merge request after the initial
- # load of the merge request page, the discussion elements will still have
- # the old discussion_ids, with the old head commit sha. The new comment,
- # however, will have the new discussion_id with the new commit sha.
- # To ensure that these new comments will still end up in the correct
- # discussion element, we also send the original discussion_id, with the
- # old commit sha, along, and fall back on this value when no discussion
- # element with the new discussion_id could be found.
- if note.new_diff_note? && note.position != note.original_position
- attrs[:original_discussion_id] = note.original_discussion_id
- end
end
else
attrs.merge!(
@@ -191,7 +182,6 @@ class Projects::NotesController < Projects::ApplicationController
)
end
- attrs[:commands_changes] = note.commands_changes
attrs
end
@@ -205,14 +195,30 @@ class Projects::NotesController < Projects::ApplicationController
def note_params
params.require(:note).permit(
- :note, :noteable, :noteable_id, :noteable_type, :project_id,
- :attachment, :line_code, :commit_id, :type, :position
+ :project_id,
+ :noteable_type,
+ :noteable_id,
+ :commit_id,
+ :noteable,
+ :type,
+
+ :note,
+ :attachment,
+
+ # LegacyDiffNote
+ :line_code,
+
+ # DiffNote
+ :position
)
end
- def find_current_user_notes
- @notes = NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
- .execute.inc_author
+ def notes_finder
+ @notes_finder ||= NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
+ end
+
+ def noteable
+ @noteable ||= notes_finder.target
end
def last_fetched_at
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index ea1a97b7cf0..5c9e0d4d1a1 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -1,4 +1,5 @@
class Projects::SnippetsController < Projects::ApplicationController
+ include RendersNotes
include ToggleAwardEmoji
include SpammableActions
include SnippetsActions
@@ -55,8 +56,10 @@ class Projects::SnippetsController < Projects::ApplicationController
def show
@note = @project.notes.new(noteable: @snippet)
- @notes = Banzai::NoteRenderer.render(@snippet.notes.fresh, @project, current_user)
@noteable = @snippet
+
+ @discussions = @snippet.discussions
+ @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
end
def destroy
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 6630c6384f2..3c499184b41 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -17,29 +17,46 @@ class NotesFinder
@project = project
@current_user = current_user
@params = params
- init_collection
end
def execute
- @notes = since_fetch_at(@params[:last_fetched_at]) if @params[:last_fetched_at]
- @notes
+ notes = init_collection
+ notes = since_fetch_at(notes)
+ notes.fresh
end
- private
+ def target
+ return @target if defined?(@target)
- def init_collection
- @notes =
- if @params[:target_id]
- on_target(@params[:target_type], @params[:target_id])
+ target_type = @params[:target_type]
+ target_id = @params[:target_id]
+
+ return @target = nil unless target_type && target_id
+
+ @target =
+ if target_type == "commit"
+ if Ability.allowed?(@current_user, :download_code, @project)
+ @project.commit(target_id)
+ end
else
- notes_of_any_type
+ noteables_for_type(target_type).find(target_id)
end
end
+ private
+
+ def init_collection
+ if target
+ notes_on_target
+ else
+ notes_of_any_type
+ end
+ end
+
def notes_of_any_type
types = %w(commit issue merge_request snippet)
note_relations = types.map { |t| notes_for_type(t) }
- note_relations.map!{ |notes| search(@params[:search], notes) } if @params[:search]
+ note_relations.map! { |notes| search(notes) }
UnionFinder.new.find_union(note_relations, Note)
end
@@ -69,17 +86,11 @@ class NotesFinder
end
end
- def on_target(target_type, target_id)
- if target_type == "commit"
- notes_for_type('commit').for_commit_id(target_id)
+ def notes_on_target
+ if target.respond_to?(:related_notes)
+ target.related_notes
else
- target = noteables_for_type(target_type).find(target_id)
-
- if target.respond_to?(:related_notes)
- target.related_notes
- else
- target.notes
- end
+ target.notes
end
end
@@ -87,17 +98,21 @@ class NotesFinder
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
- def search(query, notes_relation = @notes)
+ def search(notes)
+ query = @params[:search]
+ return notes unless query
+
pattern = "%#{query}%"
- notes_relation.where(Note.arel_table[:note].matches(pattern))
+ notes.where(Note.arel_table[:note].matches(pattern))
end
# Notes changed since last fetch
# Uses overlapping intervals to avoid worrying about race conditions
- def since_fetch_at(fetch_time)
+ def since_fetch_at(notes)
+ return notes unless @params[:last_fetched_at]
+
# Default to 0 to remain compatible with old clients
last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i)
-
- @notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
+ notes.updated_after(last_fetched_at - FETCH_OVERLAP)
end
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index aed1d7c839f..5e0886cc599 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -62,19 +62,19 @@ module DiffHelper
end
def parallel_diff_discussions(left, right, diff_file)
- discussion_left = discussion_right = nil
+ discussions_left = discussions_right = nil
if left && (left.unchanged? || left.removed?)
line_code = diff_file.line_code(left)
- discussion_left = @grouped_diff_discussions[line_code]
+ discussions_left = @grouped_diff_discussions[line_code]
end
if right && right.added?
line_code = diff_file.line_code(right)
- discussion_right = @grouped_diff_discussions[line_code]
+ discussions_right = @grouped_diff_discussions[line_code]
end
- [discussion_left, discussion_right]
+ [discussions_left, discussions_right]
end
def inline_diff_btn
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index b0331f36a2f..5f3d89cf6cb 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -24,57 +24,24 @@ module NotesHelper
end
def diff_view_data
- return {} unless @comments_target
+ return {} unless @new_diff_note_attrs
- @comments_target.slice(:noteable_id, :noteable_type, :commit_id)
+ @new_diff_note_attrs.slice(:noteable_id, :noteable_type, :commit_id)
end
def diff_view_line_data(line_code, position, line_type)
return if @diff_notes_disabled
- use_legacy_diff_note = @use_legacy_diff_notes
- # If the controller doesn't force the use of legacy diff notes, we
- # determine this on a line-by-line basis by seeing if there already exist
- # active legacy diff notes at this line, in which case newly created notes
- # will use the legacy technology as well.
- # We do this because the discussion_id values of legacy and "new" diff
- # notes, which are used to group notes on the merge request discussion tab,
- # are incompatible.
- # If we didn't, diff notes that would show for the same line on the changes
- # tab, would show in different discussions on the discussion tab.
- use_legacy_diff_note ||= begin
- discussion = @grouped_diff_discussions[line_code]
- discussion && discussion.legacy_diff_discussion?
- end
-
data = {
line_code: line_code,
line_type: line_type,
}
- if use_legacy_diff_note
- discussion_id = LegacyDiffNote.discussion_id(
- @comments_target[:noteable_type],
- @comments_target[:noteable_id] || @comments_target[:commit_id],
- line_code
- )
-
- data.merge!(
- note_type: LegacyDiffNote.name,
- discussion_id: discussion_id
- )
+ if @use_legacy_diff_notes
+ data[:note_type] = LegacyDiffNote.name
else
- discussion_id = DiffNote.discussion_id(
- @comments_target[:noteable_type],
- @comments_target[:noteable_id] || @comments_target[:commit_id],
- position
- )
-
- data.merge!(
- position: position.to_json,
- note_type: DiffNote.name,
- discussion_id: discussion_id
- )
+ data[:note_type] = DiffNote.name
+ data[:position] = position.to_json
end
data
@@ -83,21 +50,12 @@ module NotesHelper
def link_to_reply_discussion(discussion, line_type = nil)
return unless current_user
- data = discussion.reply_attributes.merge(line_type: line_type)
+ data = { discussion_id: discussion.id, line_type: line_type }
button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
data: data, title: 'Add a reply'
end
- def preload_max_access_for_authors(notes, project)
- user_ids = notes.map(&:author_id)
- project.team.max_member_access_for_user_ids(user_ids)
- end
-
- def preload_noteable_for_regular_notes(notes)
- ActiveRecord::Associations::Preloader.new.preload(notes.select { |note| !note.for_commit? }, :noteable)
- end
-
def note_max_access_for_user(note)
note.project.team.human_max_access(note.author_id)
end
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 46fa6fd9f6d..00707a0023e 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -4,13 +4,8 @@ module Emails
setup_note_mail(note_id, recipient_id)
@commit = @note.noteable
- @discussion = @note.to_discussion if @note.diff_note?
@target_url = namespace_project_commit_url(*note_target_url_options)
-
- mail_answer_thread(@commit,
- from: sender(@note.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@commit.title} (#{@commit.short_id})"))
+ mail_answer_thread(@commit, note_thread_options(recipient_id))
end
def note_issue_email(recipient_id, note_id)
@@ -25,7 +20,6 @@ module Emails
setup_note_mail(note_id, recipient_id)
@merge_request = @note.noteable
- @discussion = @note.to_discussion if @note.diff_note?
@target_url = namespace_project_merge_request_url(*note_target_url_options)
mail_answer_thread(@merge_request, note_thread_options(recipient_id))
end
@@ -56,15 +50,18 @@ module Emails
{
from: sender(@note.author_id),
to: recipient(recipient_id),
- subject: subject("#{@note.noteable.title} (#{@note.noteable.to_reference})")
+ subject: subject("#{@note.noteable.title} (#{@note.noteable.reference_link_text})")
}
end
def setup_note_mail(note_id, recipient_id)
- @note = Note.find(note_id)
+ # `note_id` is a `Note` when originating in `NotifyPreview`
+ @note = note_id.is_a?(Note) ? note_id : Note.find(note_id)
@project = @note.project
- @sent_notification = SentNotification.record_note(@note, recipient_id, reply_key)
+ if @project && @note.persisted?
+ @sent_notification = SentNotification.record_note(@note, recipient_id, reply_key)
+ end
end
end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 14df6f8f0a3..f315e38bcaa 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -111,7 +111,7 @@ class Notify < BaseMailer
headers["X-GitLab-#{model.class.name}-ID"] = model.id
headers['X-GitLab-Reply-Key'] = reply_key
- if Gitlab::IncomingEmail.enabled?
+ if Gitlab::IncomingEmail.enabled? && @sent_notification
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace
@@ -176,6 +176,6 @@ class Notify < BaseMailer
end
headers['List-Unsubscribe'] = list_unsubscribe_methods.map { |e| "<#{e}>" }.join(',')
- @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
+ @unsubscribe_url = unsubscribe_sent_notification_url(@sent_notification)
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index ce92cc369ad..5c452f78546 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -2,6 +2,7 @@ class Commit
extend ActiveModel::Naming
include ActiveModel::Conversion
+ include Noteable
include Participable
include Mentionable
include Referable
@@ -203,6 +204,10 @@ class Commit
project.notes.for_commit_id(self.id)
end
+ def discussion_notes
+ notes.non_diff_notes
+ end
+
def notes_with_associations
notes.includes(:author)
end
diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb
new file mode 100644
index 00000000000..87db0c810c3
--- /dev/null
+++ b/app/models/concerns/discussion_on_diff.rb
@@ -0,0 +1,57 @@
+# Contains functionality shared between `DiffDiscussion` and `LegacyDiffDiscussion`.
+module DiscussionOnDiff
+ extend ActiveSupport::Concern
+
+ included do
+ NUMBER_OF_TRUNCATED_DIFF_LINES = 16
+
+ memoized_values << :active
+
+ delegate :line_code,
+ :original_line_code,
+ :diff_file,
+ :diff_line,
+ :for_line?,
+ :active?,
+
+ to: :first_note
+
+ delegate :file_path,
+ :blob,
+ :highlighted_diff_lines,
+ :diff_lines,
+
+ to: :diff_file,
+ allow_nil: true
+ end
+
+ def diff_discussion?
+ true
+ end
+
+ def active?
+ return @active if @active.present?
+
+ @active = first_note.active?
+ end
+
+ # Returns an array of at most 16 highlighted lines above a diff note
+ def truncated_diff_lines(highlight: true)
+ lines = highlight ? highlighted_diff_lines : diff_lines
+ prev_lines = []
+
+ lines.each do |line|
+ if line.meta?
+ prev_lines.clear
+ else
+ prev_lines << line
+
+ break if for_line?(line)
+
+ prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES
+ end
+ end
+
+ prev_lines
+ end
+end
diff --git a/app/models/concerns/ignorable_column.rb b/app/models/concerns/ignorable_column.rb
new file mode 100644
index 00000000000..eb9f3423e48
--- /dev/null
+++ b/app/models/concerns/ignorable_column.rb
@@ -0,0 +1,28 @@
+# Module that can be included into a model to make it easier to ignore database
+# columns.
+#
+# Example:
+#
+# class User < ActiveRecord::Base
+# include IgnorableColumn
+#
+# ignore_column :updated_at
+# end
+#
+module IgnorableColumn
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def columns
+ super.reject { |column| ignored_columns.include?(column.name) }
+ end
+
+ def ignored_columns
+ @ignored_columns ||= Set.new
+ end
+
+ def ignore_column(name)
+ ignored_columns << name.to_s
+ end
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index b4dded7e27e..3d2258d5e3e 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -292,17 +292,6 @@ module Issuable
self.class.to_ability_name
end
- # Convert this Issuable class name to a format usable by notifications.
- #
- # Examples:
- #
- # issuable.class # => MergeRequest
- # issuable.human_class_name # => "merge request"
-
- def human_class_name
- @human_class_name ||= self.class.name.titleize.downcase
- end
-
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb
index b8dd27a7afe..1a5a7007a2b 100644
--- a/app/models/concerns/note_on_diff.rb
+++ b/app/models/concerns/note_on_diff.rb
@@ -1,3 +1,4 @@
+# Contains functionality shared between `DiffNote` and `LegacyDiffNote`.
module NoteOnDiff
extend ActiveSupport::Concern
@@ -24,12 +25,4 @@ module NoteOnDiff
def diff_attributes
raise NotImplementedError
end
-
- def can_be_award_emoji?
- false
- end
-
- def to_discussion
- Discussion.new([self])
- end
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
new file mode 100644
index 00000000000..772ff6a6d2f
--- /dev/null
+++ b/app/models/concerns/noteable.rb
@@ -0,0 +1,68 @@
+module Noteable
+ # Names of all implementers of `Noteable` that support resolvable notes.
+ RESOLVABLE_TYPES = %w(MergeRequest).freeze
+
+ def base_class_name
+ self.class.base_class.name
+ end
+
+ # Convert this Noteable class name to a format usable by notifications.
+ #
+ # Examples:
+ #
+ # noteable.class # => MergeRequest
+ # noteable.human_class_name # => "merge request"
+ def human_class_name
+ @human_class_name ||= base_class_name.titleize.downcase
+ end
+
+ def supports_resolvable_notes?
+ RESOLVABLE_TYPES.include?(base_class_name)
+ end
+
+ def supports_discussions?
+ DiscussionNote::NOTEABLE_TYPES.include?(base_class_name)
+ end
+
+ def discussion_notes
+ notes
+ end
+
+ delegate :find_discussion, to: :discussion_notes
+
+ def discussions
+ @discussions ||= discussion_notes
+ .inc_relations_for_view
+ .discussions(self)
+ end
+
+ def grouped_diff_discussions
+ # Doesn't use `discussion_notes`, because this may include commit diff notes
+ # besides MR diff notes, that we do no want to display on the MR Changes tab.
+ notes.inc_relations_for_view.grouped_diff_discussions
+ end
+
+ def resolvable_discussions
+ @resolvable_discussions ||= discussion_notes.resolvable.discussions(self)
+ end
+
+ def discussions_resolvable?
+ resolvable_discussions.any?(&:resolvable?)
+ end
+
+ def discussions_resolved?
+ discussions_resolvable? && resolvable_discussions.none?(&:to_be_resolved?)
+ end
+
+ def discussions_to_be_resolved?
+ discussions_resolvable? && !discussions_resolved?
+ end
+
+ def discussions_to_be_resolved
+ @discussions_to_be_resolved ||= resolvable_discussions.select(&:to_be_resolved?)
+ end
+
+ def discussions_can_be_resolved_by?(user)
+ discussions_to_be_resolved.all? { |discussion| discussion.can_resolve?(user) }
+ end
+end
diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb
new file mode 100644
index 00000000000..dd979e7bb17
--- /dev/null
+++ b/app/models/concerns/resolvable_discussion.rb
@@ -0,0 +1,103 @@
+module ResolvableDiscussion
+ extend ActiveSupport::Concern
+
+ included do
+ # A number of properties of this `Discussion`, like `first_note` and `resolvable?`, are memoized.
+ # When this discussion is resolved or unresolved, the values of these properties potentially change.
+ # To make sure all memoized values are reset when this happens, `update` resets all instance variables with names in
+ # `memoized_variables`. If you add a memoized method in `ResolvableDiscussion` or any `Discussion` subclass,
+ # please make sure the instance variable name is added to `memoized_values`, like below.
+ cattr_accessor :memoized_values, instance_accessor: false do
+ []
+ end
+
+ memoized_values.push(
+ :resolvable,
+ :resolved,
+ :first_note,
+ :first_note_to_resolve,
+ :last_resolved_note,
+ :last_note
+ )
+
+ delegate :potentially_resolvable?, to: :first_note
+
+ delegate :resolved_at,
+ :resolved_by,
+
+ to: :last_resolved_note,
+ allow_nil: true
+ end
+
+ def resolvable?
+ return @resolvable if @resolvable.present?
+
+ @resolvable = potentially_resolvable? && notes.any?(&:resolvable?)
+ end
+
+ def resolved?
+ return @resolved if @resolved.present?
+
+ @resolved = resolvable? && notes.none?(&:to_be_resolved?)
+ end
+
+ def first_note
+ @first_note ||= notes.first
+ end
+
+ def first_note_to_resolve
+ return unless resolvable?
+
+ @first_note_to_resolve ||= notes.find(&:to_be_resolved?)
+ end
+
+ def last_resolved_note
+ return unless resolved?
+
+ @last_resolved_note ||= resolved_notes.sort_by(&:resolved_at).last
+ end
+
+ def resolved_notes
+ notes.select(&:resolved?)
+ end
+
+ def to_be_resolved?
+ resolvable? && !resolved?
+ end
+
+ def can_resolve?(current_user)
+ return false unless current_user
+ return false unless resolvable?
+
+ current_user == self.noteable.author ||
+ current_user.can?(:resolve_note, self.project)
+ end
+
+ def resolve!(current_user)
+ return unless resolvable?
+
+ update { |notes| notes.resolve!(current_user) }
+ end
+
+ def unresolve!
+ return unless resolvable?
+
+ update { |notes| notes.unresolve! }
+ end
+
+ private
+
+ def update
+ # Do not select `Note.resolvable`, so that system notes remain in the collection
+ notes_relation = Note.where(id: notes.map(&:id))
+
+ yield(notes_relation)
+
+ # Set the notes array to the updated notes
+ @notes = notes_relation.fresh.to_a
+
+ self.class.memoized_values.each do |var|
+ instance_variable_set(:"@#{var}", nil)
+ end
+ end
+end
diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb
new file mode 100644
index 00000000000..05eb6f86704
--- /dev/null
+++ b/app/models/concerns/resolvable_note.rb
@@ -0,0 +1,72 @@
+module ResolvableNote
+ extend ActiveSupport::Concern
+
+ # Names of all subclasses of `Note` that can be resolvable.
+ RESOLVABLE_TYPES = %w(DiffNote DiscussionNote).freeze
+
+ included do
+ belongs_to :resolved_by, class_name: "User"
+
+ validates :resolved_by, presence: true, if: :resolved?
+
+ # Keep this scope in sync with `#potentially_resolvable?`
+ scope :potentially_resolvable, -> { where(type: RESOLVABLE_TYPES).where(noteable_type: Noteable::RESOLVABLE_TYPES) }
+ # Keep this scope in sync with `#resolvable?`
+ scope :resolvable, -> { potentially_resolvable.user }
+
+ scope :resolved, -> { resolvable.where.not(resolved_at: nil) }
+ scope :unresolved, -> { resolvable.where(resolved_at: nil) }
+ end
+
+ module ClassMethods
+ # This method must be kept in sync with `#resolve!`
+ def resolve!(current_user)
+ unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id)
+ end
+
+ # This method must be kept in sync with `#unresolve!`
+ def unresolve!
+ resolved.update_all(resolved_at: nil, resolved_by_id: nil)
+ end
+ end
+
+ # Keep this method in sync with the `potentially_resolvable` scope
+ def potentially_resolvable?
+ RESOLVABLE_TYPES.include?(self.class.name) && noteable.supports_resolvable_notes?
+ end
+
+ # Keep this method in sync with the `resolvable` scope
+ def resolvable?
+ potentially_resolvable? && !system?
+ end
+
+ def resolved?
+ return false unless resolvable?
+
+ self.resolved_at.present?
+ end
+
+ def to_be_resolved?
+ resolvable? && !resolved?
+ end
+
+ # If you update this method remember to also update `.resolve!`
+ def resolve!(current_user)
+ return unless resolvable?
+ return if resolved?
+
+ self.resolved_at = Time.now
+ self.resolved_by = current_user
+ save!
+ end
+
+ # If you update this method remember to also update `.unresolve!`
+ def unresolve!
+ return unless resolvable?
+ return unless resolved?
+
+ self.resolved_at = nil
+ self.resolved_by = nil
+ save!
+ end
+end
diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb
new file mode 100644
index 00000000000..d9b7e484e0f
--- /dev/null
+++ b/app/models/diff_discussion.rb
@@ -0,0 +1,26 @@
+# A discussion on merge request or commit diffs consisting of `DiffNote` notes.
+#
+# A discussion of this type can be resolvable.
+class DiffDiscussion < Discussion
+ include DiscussionOnDiff
+
+ def self.note_class
+ DiffNote
+ end
+
+ delegate :position,
+ :original_position,
+
+ to: :first_note
+
+ def legacy_diff_discussion?
+ false
+ end
+
+ def reply_attributes
+ super.merge(
+ original_position: original_position.to_json,
+ position: position.to_json,
+ )
+ end
+end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 895a91139c9..1523244f8a8 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -1,6 +1,11 @@
+# A note on merge request or commit diffs
+#
+# A note of this type can be resolvable.
class DiffNote < Note
include NoteOnDiff
+ NOTEABLE_TYPES = %w(MergeRequest Commit).freeze
+
serialize :original_position, Gitlab::Diff::Position
serialize :position, Gitlab::Diff::Position
@@ -8,59 +13,31 @@ class DiffNote < Note
validates :position, presence: true
validates :diff_line, presence: true
validates :line_code, presence: true, line_code: true
- validates :noteable_type, inclusion: { in: %w(Commit MergeRequest) }
- validates :resolved_by, presence: true, if: :resolved?
+ validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
validate :positions_complete
validate :verify_supported
- # Keep this scope in sync with the logic in `#resolvable?`
- scope :resolvable, -> { user.where(noteable_type: 'MergeRequest') }
- scope :resolved, -> { resolvable.where.not(resolved_at: nil) }
- scope :unresolved, -> { resolvable.where(resolved_at: nil) }
-
- after_initialize :ensure_original_discussion_id
before_validation :set_original_position, :update_position, on: :create
- before_validation :set_line_code, :set_original_discussion_id
- # We need to do this again, because it's already in `Note`, but is affected by
- # `update_position` and needs to run after that.
- before_validation :set_discussion_id
+ before_validation :set_line_code
after_save :keep_around_commits
- class << self
- def build_discussion_id(noteable_type, noteable_id, position)
- [super(noteable_type, noteable_id), *position.key].join("-")
- end
-
- # This method must be kept in sync with `#resolve!`
- def resolve!(current_user)
- unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id)
- end
-
- # This method must be kept in sync with `#unresolve!`
- def unresolve!
- resolved.update_all(resolved_at: nil, resolved_by_id: nil)
- end
- end
-
- def new_diff_note?
- true
+ def discussion_class(*)
+ DiffDiscussion
end
- def diff_attributes
- { position: position.to_json }
- end
+ %i(original_position position).each do |meth|
+ define_method "#{meth}=" do |new_position|
+ if new_position.is_a?(String)
+ new_position = JSON.parse(new_position) rescue nil
+ end
- def position=(new_position)
- if new_position.is_a?(String)
- new_position = JSON.parse(new_position) rescue nil
- end
+ if new_position.is_a?(Hash)
+ new_position = new_position.with_indifferent_access
+ new_position = Gitlab::Diff::Position.new(new_position)
+ end
- if new_position.is_a?(Hash)
- new_position = new_position.with_indifferent_access
- new_position = Gitlab::Diff::Position.new(new_position)
+ super(new_position)
end
-
- super(new_position)
end
def diff_file
@@ -88,43 +65,6 @@ class DiffNote < Note
self.position.diff_refs == diff_refs
end
- # If you update this method remember to also update the scope `resolvable`
- def resolvable?
- !system? && for_merge_request?
- end
-
- def resolved?
- return false unless resolvable?
-
- self.resolved_at.present?
- end
-
- # If you update this method remember to also update `.resolve!`
- def resolve!(current_user)
- return unless resolvable?
- return if resolved?
-
- self.resolved_at = Time.now
- self.resolved_by = current_user
- save!
- end
-
- # If you update this method remember to also update `.unresolve!`
- def unresolve!
- return unless resolvable?
- return unless resolved?
-
- self.resolved_at = nil
- self.resolved_by = nil
- save!
- end
-
- def discussion
- return unless resolvable?
-
- self.noteable.find_diff_discussion(self.discussion_id)
- end
-
private
def supported?
@@ -140,33 +80,13 @@ class DiffNote < Note
end
def set_original_position
- self.original_position = self.position.dup
+ self.original_position = self.position.dup unless self.original_position&.complete?
end
def set_line_code
self.line_code = self.position.line_code(self.project.repository)
end
- def ensure_original_discussion_id
- return unless self.persisted?
- return if self.original_discussion_id
-
- set_original_discussion_id
- update_column(:original_discussion_id, self.original_discussion_id)
- end
-
- def set_original_discussion_id
- self.original_discussion_id = Digest::SHA1.hexdigest(build_original_discussion_id)
- end
-
- def build_discussion_id
- self.class.build_discussion_id(noteable_type, noteable_id || commit_id, position)
- end
-
- def build_original_discussion_id
- self.class.build_discussion_id(noteable_type, noteable_id || commit_id, original_position)
- end
-
def update_position
return unless supported?
return if for_commit?
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index bbe813db823..2c0e6379e6a 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -1,7 +1,10 @@
+# A non-diff discussion on an issue, merge request, commit, or snippet, consisting of `DiscussionNote` notes.
+#
+# A discussion of this type can be resolvable.
class Discussion
- NUMBER_OF_TRUNCATED_DIFF_LINES = 16
+ include ResolvableDiscussion
- attr_reader :notes
+ attr_reader :notes, :noteable
delegate :created_at,
:project,
@@ -11,43 +14,62 @@ class Discussion
:for_commit?,
:for_merge_request?,
- :line_code,
- :original_line_code,
- :diff_file,
- :for_line?,
- :active?,
-
to: :first_note
- delegate :resolved_at,
- :resolved_by,
+ def self.build(notes, noteable = nil)
+ notes.first.discussion_class(noteable).new(notes, noteable)
+ end
- to: :last_resolved_note,
- allow_nil: true
+ def self.build_collection(notes, noteable = nil)
+ notes.group_by { |n| n.discussion_id(noteable) }.values.map { |notes| build(notes, noteable) }
+ end
- delegate :blob,
- :highlighted_diff_lines,
- :diff_lines,
+ # Returns an alphanumeric discussion ID based on `build_discussion_id`
+ def self.discussion_id(note)
+ Digest::SHA1.hexdigest(build_discussion_id(note).join("-"))
+ end
- to: :diff_file,
- allow_nil: true
+ # Returns an array of discussion ID components
+ def self.build_discussion_id(note)
+ [*base_discussion_id(note), SecureRandom.hex]
+ end
- def self.for_notes(notes)
- notes.group_by(&:discussion_id).values.map { |notes| new(notes) }
+ def self.base_discussion_id(note)
+ noteable_id = note.noteable_id || note.commit_id
+ [:discussion, note.noteable_type.try(:underscore), noteable_id]
end
- def self.for_diff_notes(notes)
- notes.group_by(&:line_code).values.map { |notes| new(notes) }
+ # When notes on a commit are displayed in context of a merge request that contains that commit,
+ # these notes are to be displayed as if they were part of one discussion, even though they were actually
+ # individual notes on the commit with different discussion IDs, so that it's clear that these are not
+ # notes on the merge request itself.
+ #
+ # To turn a list of notes into a list of discussions, they are grouped by discussion ID, so to
+ # get these out-of-context notes to end up in the same discussion, we need to get them to return the same
+ # `discussion_id` when this grouping happens. To enable this, `Note#discussion_id` calls out
+ # to the `override_discussion_id` method on the appropriate `Discussion` subclass, as determined by
+ # the `discussion_class` method on `Note` or a subclass of `Note`.
+ #
+ # If no override is necessary, return `nil`.
+ # For the case described above, see `OutOfContextDiscussion.override_discussion_id`.
+ def self.override_discussion_id(note)
+ nil
end
- def initialize(notes)
- @notes = notes
+ def self.note_class
+ DiscussionNote
end
- def last_resolved_note
- return unless resolved?
+ def initialize(notes, noteable = nil)
+ @notes = notes
+ @noteable = noteable
+ end
- @last_resolved_note ||= resolved_notes.sort_by(&:resolved_at).last
+ def ==(other)
+ other.class == self.class &&
+ other.noteable == self.noteable &&
+ other.id == self.id &&
+ other.notes == self.notes
end
def last_updated_at
@@ -59,91 +81,29 @@ class Discussion
end
def id
- first_note.discussion_id
+ first_note.discussion_id(noteable)
end
alias_method :to_param, :id
def diff_discussion?
- first_note.diff_note?
- end
-
- def legacy_diff_discussion?
- notes.any?(&:legacy_diff_note?)
+ false
end
- def resolvable?
- return @resolvable if @resolvable.present?
-
- @resolvable = diff_discussion? && notes.any?(&:resolvable?)
+ def individual_note?
+ false
end
- def resolved?
- return @resolved if @resolved.present?
-
- @resolved = resolvable? && notes.none?(&:to_be_resolved?)
- end
-
- def first_note
- @first_note ||= @notes.first
- end
-
- def first_note_to_resolve
- @first_note_to_resolve ||= notes.detect(&:to_be_resolved?)
+ def new_discussion?
+ notes.length == 1
end
def last_note
- @last_note ||= @notes.last
- end
-
- def resolved_notes
- notes.select(&:resolved?)
- end
-
- def to_be_resolved?
- resolvable? && !resolved?
- end
-
- def can_resolve?(current_user)
- return false unless current_user
- return false unless resolvable?
-
- current_user == self.noteable.author ||
- current_user.can?(:resolve_note, self.project)
- end
-
- def resolve!(current_user)
- return unless resolvable?
-
- update { |notes| notes.resolve!(current_user) }
- end
-
- def unresolve!
- return unless resolvable?
-
- update { |notes| notes.unresolve! }
- end
-
- def for_target?(target)
- self.noteable == target && !diff_discussion?
- end
-
- def active?
- return @active if @active.present?
-
- @active = first_note.active?
+ @last_note ||= notes.last
end
def collapsed?
- return false unless diff_discussion?
-
- if resolvable?
- # New diff discussions only disappear once they are marked resolved
- resolved?
- else
- # Old diff discussions disappear once they become outdated
- !active?
- end
+ resolved?
end
def expanded?
@@ -151,52 +111,6 @@ class Discussion
end
def reply_attributes
- data = {
- noteable_type: first_note.noteable_type,
- noteable_id: first_note.noteable_id,
- commit_id: first_note.commit_id,
- discussion_id: self.id,
- }
-
- if diff_discussion?
- data[:note_type] = first_note.type
-
- data.merge!(first_note.diff_attributes)
- end
-
- data
- end
-
- # Returns an array of at most 16 highlighted lines above a diff note
- def truncated_diff_lines(highlight: true)
- lines = highlight ? highlighted_diff_lines : diff_lines
- prev_lines = []
-
- lines.each do |line|
- if line.meta?
- prev_lines.clear
- else
- prev_lines << line
-
- break if for_line?(line)
-
- prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES
- end
- end
-
- prev_lines
- end
-
- private
-
- def update
- notes_relation = DiffNote.where(id: notes.map(&:id)).fresh
- yield(notes_relation)
-
- # Set the notes array to the updated notes
- @notes = notes_relation.to_a
-
- # Reset the memoized values
- @last_resolved_note = @resolvable = @resolved = @first_note = @last_note = nil
+ first_note.slice(:type, :noteable_type, :noteable_id, :commit_id, :discussion_id)
end
end
diff --git a/app/models/discussion_note.rb b/app/models/discussion_note.rb
new file mode 100644
index 00000000000..e660b024083
--- /dev/null
+++ b/app/models/discussion_note.rb
@@ -0,0 +1,13 @@
+# A note in a non-diff discussion on an issue, merge request, commit, or snippet.
+#
+# A note of this type can be resolvable.
+class DiscussionNote < Note
+ # Names of all implementers of `Noteable` that support discussions.
+ NOTEABLE_TYPES = %w(MergeRequest Issue Commit Snippet).freeze
+
+ validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
+
+ def discussion_class(*)
+ Discussion
+ end
+end
diff --git a/app/models/individual_note_discussion.rb b/app/models/individual_note_discussion.rb
new file mode 100644
index 00000000000..c3f21c55240
--- /dev/null
+++ b/app/models/individual_note_discussion.rb
@@ -0,0 +1,13 @@
+# A discussion to wrap a single `Note` note on the root of an issue, merge request,
+# commit, or snippet, that is not displayed as a discussion.
+#
+# A discussion of this type is never resolvable.
+class IndividualNoteDiscussion < Discussion
+ def self.note_class
+ Note
+ end
+
+ def individual_note?
+ true
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index f9704b0d754..d8d9db477d2 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -3,6 +3,7 @@ require 'carrierwave/orm/activerecord'
class Issue < ActiveRecord::Base
include InternalId
include Issuable
+ include Noteable
include Referable
include Sortable
include Spammable
diff --git a/app/models/legacy_diff_discussion.rb b/app/models/legacy_diff_discussion.rb
new file mode 100644
index 00000000000..cb2651a03f8
--- /dev/null
+++ b/app/models/legacy_diff_discussion.rb
@@ -0,0 +1,25 @@
+# A discussion on merge request or commit diffs consisting of `LegacyDiffNote` notes.
+#
+# All new diff discussions are of the type `DiffDiscussion`, but any diff discussions created
+# before the introduction of the new implementation still use `LegacyDiffDiscussion`.
+#
+# A discussion of this type is never resolvable.
+class LegacyDiffDiscussion < Discussion
+ include DiscussionOnDiff
+
+ def legacy_diff_discussion?
+ true
+ end
+
+ def self.note_class
+ LegacyDiffNote
+ end
+
+ def collapsed?
+ !active?
+ end
+
+ def reply_attributes
+ super.merge(line_code: line_code)
+ end
+end
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index 40277a9b139..9a77557ebcd 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -1,3 +1,9 @@
+# A note on merge request or commit diffs, using the legacy implementation.
+#
+# All new diff notes are of the type `DiffNote`, but any diff notes created
+# before the introduction of the new implementation still use `LegacyDiffNote`.
+#
+# A note of this type is never resolvable.
class LegacyDiffNote < Note
include NoteOnDiff
@@ -7,18 +13,8 @@ class LegacyDiffNote < Note
before_create :set_diff
- class << self
- def build_discussion_id(noteable_type, noteable_id, line_code)
- [super(noteable_type, noteable_id), line_code].join("-")
- end
- end
-
- def legacy_diff_note?
- true
- end
-
- def diff_attributes
- { line_code: line_code }
+ def discussion_class(*)
+ LegacyDiffDiscussion
end
def project_repository
@@ -119,8 +115,4 @@ class LegacyDiffNote < Note
diffs = noteable.raw_diffs(Commit.max_diff_options)
diffs.find { |d| d.new_path == self.diff.new_path }
end
-
- def build_discussion_id
- self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
- end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 8d740adb771..77c902fdf09 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1,6 +1,7 @@
class MergeRequest < ActiveRecord::Base
include InternalId
include Issuable
+ include Noteable
include Referable
include Sortable
@@ -475,43 +476,7 @@ class MergeRequest < ActiveRecord::Base
)
end
- def discussions
- @discussions ||= self.related_notes.
- inc_relations_for_view.
- fresh.
- discussions
- end
-
- def diff_discussions
- @diff_discussions ||= self.notes.diff_notes.discussions
- end
-
- def resolvable_discussions
- @resolvable_discussions ||= diff_discussions.select(&:to_be_resolved?)
- end
-
- def discussions_can_be_resolved_by?(user)
- resolvable_discussions.all? { |discussion| discussion.can_resolve?(user) }
- end
-
- def find_diff_discussion(discussion_id)
- notes = self.notes.diff_notes.where(discussion_id: discussion_id).fresh.to_a
- return if notes.empty?
-
- Discussion.new(notes)
- end
-
- def discussions_resolvable?
- diff_discussions.any?(&:resolvable?)
- end
-
- def discussions_resolved?
- discussions_resolvable? && diff_discussions.none?(&:to_be_resolved?)
- end
-
- def discussions_to_be_resolved?
- discussions_resolvable? && !discussions_resolved?
- end
+ alias_method :discussion_notes, :related_notes
def mergeable_discussions_state?
return true unless project.only_allow_merge_if_all_discussions_are_resolved?
@@ -857,8 +822,8 @@ class MergeRequest < ActiveRecord::Base
return unless has_complete_diff_refs?
return if new_diff_refs == old_diff_refs
- active_diff_notes = self.notes.diff_notes.select do |note|
- note.new_diff_note? && note.active?(old_diff_refs)
+ active_diff_notes = self.notes.new_diff_notes.select do |note|
+ note.active?(old_diff_refs)
end
return if active_diff_notes.empty?
diff --git a/app/models/note.rb b/app/models/note.rb
index 16d66cb1427..401e3d7bcbc 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -1,3 +1,6 @@
+# A note on the root of an issue, merge request, commit, or snippet.
+#
+# A note of this type is never resolvable.
class Note < ActiveRecord::Base
extend ActiveModel::Naming
include Gitlab::CurrentSettings
@@ -8,6 +11,10 @@ class Note < ActiveRecord::Base
include FasterCacheKeys
include CacheMarkdownField
include AfterCommitQueue
+ include ResolvableNote
+ include IgnorableColumn
+
+ ignore_column :original_discussion_id
cache_markdown_field :note, pipeline: :note
@@ -32,9 +39,6 @@ class Note < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :updated_by, class_name: "User"
- # Only used by DiffNote, but defined here so that it can be used in `Note.includes`
- belongs_to :resolved_by, class_name: "User"
-
has_many :todos, dependent: :destroy
has_many :events, as: :target, dependent: :destroy
has_one :system_note_metadata
@@ -54,10 +58,11 @@ class Note < ActiveRecord::Base
validates :noteable_id, presence: true, unless: [:for_commit?, :importing?]
validates :commit_id, presence: true, if: :for_commit?
validates :author, presence: true
+ validates :discussion_id, presence: true, format: { with: /\A\h{40}\z/ }
validate unless: [:for_commit?, :importing?, :for_personal_snippet?] do |note|
unless note.noteable.try(:project) == note.project
- errors.add(:invalid_project, 'Note and noteable project mismatch')
+ errors.add(:project, 'does not match noteable project')
end
end
@@ -69,6 +74,7 @@ class Note < ActiveRecord::Base
scope :user, ->{ where(system: false) }
scope :common, ->{ where(noteable_type: ["", nil]) }
scope :fresh, ->{ order(created_at: :asc, id: :asc) }
+ scope :updated_after, ->(time){ where('updated_at > ?', time) }
scope :inc_author_project, ->{ includes(:project, :author) }
scope :inc_author, ->{ includes(:author) }
scope :inc_relations_for_view, -> do
@@ -76,7 +82,8 @@ class Note < ActiveRecord::Base
end
scope :diff_notes, ->{ where(type: %w(LegacyDiffNote DiffNote)) }
- scope :non_diff_notes, ->{ where(type: ['Note', nil]) }
+ scope :new_diff_notes, ->{ where(type: 'DiffNote') }
+ scope :non_diff_notes, ->{ where(type: ['Note', 'DiscussionNote', nil]) }
scope :with_associations, -> do
# FYI noteable cannot be loaded for LegacyDiffNote for commits
@@ -86,7 +93,7 @@ class Note < ActiveRecord::Base
after_initialize :ensure_discussion_id
before_validation :nullify_blank_type, :nullify_blank_line_code
- before_validation :set_discussion_id
+ before_validation :set_discussion_id, on: :create
after_save :keep_around_commit, unless: :for_personal_snippet?
after_save :expire_etag_cache
@@ -95,22 +102,23 @@ class Note < ActiveRecord::Base
ActiveModel::Name.new(self, nil, 'note')
end
- def build_discussion_id(noteable_type, noteable_id)
- [:discussion, noteable_type.try(:underscore), noteable_id].join("-")
+ def discussions(noteable = nil)
+ Discussion.build_collection(fresh, noteable)
end
- def discussion_id(*args)
- Digest::SHA1.hexdigest(build_discussion_id(*args))
- end
+ def find_discussion(discussion_id)
+ notes = where(discussion_id: discussion_id).fresh.to_a
+ return if notes.empty?
- def discussions
- Discussion.for_notes(fresh)
+ Discussion.build(notes)
end
def grouped_diff_discussions
- active_notes = diff_notes.fresh.select(&:active?)
- Discussion.for_diff_notes(active_notes).
- map { |d| [d.line_code, d] }.to_h
+ diff_notes.
+ fresh.
+ discussions.
+ select(&:active?).
+ group_by(&:line_code)
end
def count_for_collection(ids, type)
@@ -121,37 +129,17 @@ class Note < ActiveRecord::Base
end
def cross_reference?
- system && SystemNoteService.cross_reference?(note)
+ system? && SystemNoteService.cross_reference?(note)
end
def diff_note?
false
end
- def legacy_diff_note?
- false
- end
-
- def new_diff_note?
- false
- end
-
def active?
true
end
- def resolvable?
- false
- end
-
- def resolved?
- false
- end
-
- def to_be_resolved?
- resolvable? && !resolved?
- end
-
def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i
end
@@ -228,7 +216,7 @@ class Note < ActiveRecord::Base
end
def can_be_award_emoji?
- noteable.is_a?(Awardable)
+ noteable.is_a?(Awardable) && !part_of_discussion?
end
def contains_emoji_only?
@@ -239,6 +227,63 @@ class Note < ActiveRecord::Base
for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore
end
+ def can_be_discussion_note?
+ self.noteable.supports_discussions? && !part_of_discussion?
+ end
+
+ def discussion_class(noteable = nil)
+ # When commit notes are rendered on an MR's Discussion page, they are
+ # displayed in one discussion instead of individually.
+ # See also `#discussion_id` and `Discussion.override_discussion_id`.
+ if noteable && noteable != self.noteable
+ OutOfContextDiscussion
+ else
+ IndividualNoteDiscussion
+ end
+ end
+
+ # See `Discussion.override_discussion_id` for details.
+ def discussion_id(noteable = nil)
+ discussion_class(noteable).override_discussion_id(self) || super()
+ end
+
+ # Returns a discussion containing just this note.
+ # This method exists as an alternative to `#discussion` to use when the methods
+ # we intend to call on the Discussion object don't require it to have all of its notes,
+ # and just depend on the first note or the type of discussion. This saves us a DB query.
+ def to_discussion(noteable = nil)
+ Discussion.build([self], noteable)
+ end
+
+ # Returns the entire discussion this note is part of.
+ # Consider using `#to_discussion` if we do not need to render the discussion
+ # and all its notes and if we don't care about the discussion's resolvability status.
+ def discussion
+ full_discussion = self.noteable.notes.find_discussion(self.discussion_id) if part_of_discussion?
+ full_discussion || to_discussion
+ end
+
+ def part_of_discussion?
+ !to_discussion.individual_note?
+ end
+
+ def in_reply_to?(other)
+ case other
+ when Note
+ if part_of_discussion?
+ in_reply_to?(other.noteable) && in_reply_to?(other.to_discussion)
+ else
+ in_reply_to?(other.noteable)
+ end
+ when Discussion
+ self.discussion_id == other.id
+ when Noteable
+ self.noteable == other
+ else
+ false
+ end
+ end
+
private
def keep_around_commit
@@ -264,17 +309,7 @@ class Note < ActiveRecord::Base
end
def set_discussion_id
- self.discussion_id = Digest::SHA1.hexdigest(build_discussion_id)
- end
-
- def build_discussion_id
- if for_merge_request?
- # Notes on merge requests are always in a discussion of their own,
- # so we generate a unique discussion ID.
- [:discussion, :note, SecureRandom.hex].join("-")
- else
- self.class.build_discussion_id(noteable_type, noteable_id || commit_id)
- end
+ self.discussion_id ||= discussion_class.discussion_id(self)
end
def expire_etag_cache
diff --git a/app/models/out_of_context_discussion.rb b/app/models/out_of_context_discussion.rb
new file mode 100644
index 00000000000..85794630f70
--- /dev/null
+++ b/app/models/out_of_context_discussion.rb
@@ -0,0 +1,22 @@
+# When notes on a commit are displayed in the context of a merge request that
+# contains that commit, they are displayed as if they were a discussion.
+#
+# This represents one of those discussions, consisting of `Note` notes.
+#
+# A discussion of this type is never resolvable.
+class OutOfContextDiscussion < Discussion
+ # Returns an array of discussion ID components
+ def self.build_discussion_id(note)
+ base_discussion_id(note)
+ end
+
+ # To make sure all out-of-context notes end up grouped as one discussion,
+ # we override the discussion ID to be a newly generated but consistent ID.
+ def self.override_discussion_id(note)
+ discussion_id(note)
+ end
+
+ def self.note_class
+ Note
+ end
+end
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index f4bcb49b34d..bfaf0eb2fae 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -5,10 +5,11 @@ class SentNotification < ActiveRecord::Base
belongs_to :noteable, polymorphic: true
belongs_to :recipient, class_name: "User"
- validates :project, :recipient, :reply_key, presence: true
- validates :reply_key, uniqueness: true
+ validates :project, :recipient, presence: true
+ validates :reply_key, presence: true, uniqueness: true
validates :noteable_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
+ validates :in_reply_to_discussion_id, format: { with: /\A\h{40}\z/, allow_nil: true }
validate :note_valid
after_save :keep_around_commit
@@ -22,9 +23,7 @@ class SentNotification < ActiveRecord::Base
find_by(reply_key: reply_key)
end
- def record(noteable, recipient_id, reply_key, attrs = {})
- return unless reply_key
-
+ def record(noteable, recipient_id, reply_key = self.reply_key, attrs = {})
noteable_id = nil
commit_id = nil
if noteable.is_a?(Commit)
@@ -34,23 +33,20 @@ class SentNotification < ActiveRecord::Base
end
attrs.reverse_merge!(
- project: noteable.project,
- noteable_type: noteable.class.name,
- noteable_id: noteable_id,
- commit_id: commit_id,
- recipient_id: recipient_id,
- reply_key: reply_key
+ project: noteable.project,
+ recipient_id: recipient_id,
+ reply_key: reply_key,
+
+ noteable_type: noteable.class.name,
+ noteable_id: noteable_id,
+ commit_id: commit_id,
)
create(attrs)
end
- def record_note(note, recipient_id, reply_key, attrs = {})
- if note.diff_note?
- attrs[:note_type] = note.type
-
- attrs.merge!(note.diff_attributes)
- end
+ def record_note(note, recipient_id, reply_key = self.reply_key, attrs = {})
+ attrs[:in_reply_to_discussion_id] = note.discussion_id
record(note.noteable, recipient_id, reply_key, attrs)
end
@@ -89,31 +85,45 @@ class SentNotification < ActiveRecord::Base
self.reply_key
end
- def note_attributes
- {
- project: self.project,
- author: self.recipient,
- type: self.note_type,
- noteable_type: self.noteable_type,
- noteable_id: self.noteable_id,
- commit_id: self.commit_id,
- line_code: self.line_code,
- position: self.position.to_json
- }
- end
-
- def create_note(note)
- Notes::CreateService.new(
- self.project,
- self.recipient,
- self.note_attributes.merge(note: note)
- ).execute
+ def create_reply(message, dryrun: false)
+ klass = dryrun ? Notes::BuildService : Notes::CreateService
+ klass.new(self.project, self.recipient, reply_params.merge(note: message)).execute
end
private
+ def reply_params
+ attrs = {
+ noteable_type: self.noteable_type,
+ noteable_id: self.noteable_id,
+ commit_id: self.commit_id
+ }
+
+ if self.in_reply_to_discussion_id.present?
+ attrs[:in_reply_to_discussion_id] = self.in_reply_to_discussion_id
+ else
+ # Remove in GitLab 10.0, when we will not support replying to SentNotifications
+ # that don't have `in_reply_to_discussion_id` anymore.
+ attrs.merge!(
+ type: self.note_type,
+
+ # LegacyDiffNote
+ line_code: self.line_code,
+
+ # DiffNote
+ position: self.position.to_json
+ )
+ end
+
+ attrs
+ end
+
def note_valid
- Note.new(note_attributes.merge(note: "Test")).valid?
+ note = create_reply('Test', dryrun: true)
+
+ unless note.valid?
+ self.errors.add(:base, "Note parameters are invalid: #{note.errors.full_messages.to_sentence}")
+ end
end
def keep_around_commit
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 30aca62499c..380835707e8 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -2,6 +2,7 @@ class Snippet < ActiveRecord::Base
include Gitlab::VisibilityLevel
include Linguist::BlobHelper
include CacheMarkdownField
+ include Noteable
include Participable
include Referable
include Sortable
diff --git a/app/services/concerns/issues/resolve_discussions.rb b/app/services/concerns/issues/resolve_discussions.rb
index 297c7d696c3..910a2a15e5d 100644
--- a/app/services/concerns/issues/resolve_discussions.rb
+++ b/app/services/concerns/issues/resolve_discussions.rb
@@ -21,11 +21,11 @@ module Issues
@discussions_to_resolve ||=
if discussion_to_resolve_id
discussion_or_nil = merge_request_to_resolve_discussions_of
- .find_diff_discussion(discussion_to_resolve_id)
+ .find_discussion(discussion_to_resolve_id)
Array(discussion_or_nil)
else
merge_request_to_resolve_discussions_of
- .resolvable_discussions
+ .discussions_to_be_resolved
end
end
end
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
index 77bced4bd5c..3a4f7b159f1 100644
--- a/app/services/issues/build_service.rb
+++ b/app/services/issues/build_service.rb
@@ -35,14 +35,19 @@ module Issues
end
def item_for_discussion(discussion)
- first_note = discussion.first_note_to_resolve || discussion.first_note
+ first_note_to_resolve = discussion.first_note_to_resolve || discussion.first_note
+
+ is_very_first_note = first_note_to_resolve == discussion.first_note
+ action = is_very_first_note ? "started" : "commented on"
+
+ note_url = Gitlab::UrlBuilder.build(first_note_to_resolve)
+
other_note_count = discussion.notes.size - 1
- note_url = Gitlab::UrlBuilder.build(first_note)
- discussion_info = "- [ ] #{first_note.author.to_reference} commented on a [discussion](#{note_url}): "
+ discussion_info = "- [ ] #{first_note_to_resolve.author.to_reference} #{action} a [discussion](#{note_url}): "
discussion_info << " (+#{other_note_count} #{'comment'.pluralize(other_note_count)})" if other_note_count > 0
- note_without_block_quotes = Banzai::Filter::BlockquoteFenceFilter.new(first_note.note).call
+ note_without_block_quotes = Banzai::Filter::BlockquoteFenceFilter.new(first_note_to_resolve.note).call
spaces = ' ' * 4
quote = note_without_block_quotes.lines.map { |line| "#{spaces}> #{line}" }.join
diff --git a/app/services/notes/build_service.rb b/app/services/notes/build_service.rb
new file mode 100644
index 00000000000..ea7cacc956c
--- /dev/null
+++ b/app/services/notes/build_service.rb
@@ -0,0 +1,25 @@
+module Notes
+ class BuildService < ::BaseService
+ def execute
+ in_reply_to_discussion_id = params.delete(:in_reply_to_discussion_id)
+
+ if project && in_reply_to_discussion_id.present?
+ discussion = project.notes.find_discussion(in_reply_to_discussion_id)
+
+ unless discussion
+ note = Note.new
+ note.errors.add(:base, 'Discussion to reply to cannot be found')
+ return note
+ end
+
+ params.merge!(discussion.reply_attributes)
+ end
+
+ note = Note.new(params)
+ note.project = project
+ note.author = current_user
+
+ note
+ end
+ end
+end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 61d66a26932..f3954f6f8c4 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -1,12 +1,10 @@
module Notes
- class CreateService < BaseService
+ class CreateService < ::BaseService
def execute
merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
- note = Note.new(params)
- note.project = project
- note.author = current_user
- note.system = false
+ note = Notes::BuildService.new(project, current_user, params).execute
+ return note unless note.valid?
# We execute commands (extracted from `params[:note]`) on the noteable
# **before** we save the note because if the note consists of commands
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 35cfcc3682e..c9e25c7aaa2 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -228,12 +228,10 @@ module SystemNoteService
def discussion_continued_in_issue(discussion, project, author, issue)
body = "created #{issue.to_reference} to continue this discussion"
+ note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
- note_params = discussion.reply_attributes.merge(project: project, author: author, note: body)
- note_params[:type] = note_params.delete(:note_type)
-
- note = Note.create(note_params.merge(system: true))
- note.system_note_metadata = SystemNoteMetadata.new({ action: 'discussion' })
+ note = Note.create(note_attributes.merge(system: true))
+ note.system_note_metadata = SystemNoteMetadata.new(action: 'discussion')
note
end
diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml
index ee452add394..e6d307e5568 100644
--- a/app/views/discussions/_diff_discussion.html.haml
+++ b/app/views/discussions/_diff_discussion.html.haml
@@ -3,4 +3,4 @@
%td.notes_line{ colspan: 2 }
%td.notes_content
.content{ class: ('hide' unless expanded) }
- = render "discussions/notes", discussion: discussion
+ = render partial: "discussions/notes", collection: discussions, as: :discussion
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index 94408b92374..549364761e6 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -7,7 +7,7 @@
.diff-content.code.js-syntax-highlight
%table
- - discussions = { discussion.original_line_code => discussion }
+ - discussions = { discussion.original_line_code => [discussion] }
= render partial: "projects/diffs/line",
collection: discussion.truncated_diff_lines,
as: :line,
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 2d78c55211e..e04958817e4 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -5,7 +5,7 @@
= link_to user_path(discussion.author) do
= image_tag avatar_icon(discussion.author), class: "avatar s40"
.timeline-content
- .discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } }
+ .discussion.js-toggle-container{ data: { discussion_id: discussion.id } }
.discussion-header
.discussion-actions
%button.note-action-button.discussion-toggle-button.js-toggle-button{ type: "button" }
@@ -18,19 +18,21 @@
.inline.discussion-headline-light
= discussion.author.to_reference
- started a discussion on
+ started a discussion
- - if discussion.for_commit?
+ - if discussion.for_commit? && @noteable != discussion.noteable
+ on
- commit = discussion.noteable
- if commit
commit
- = link_to commit.short_id, namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code), class: 'monospace'
+ - anchor = discussion.line_code if discussion.diff_discussion?
+ = link_to commit.short_id, namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: anchor), class: 'monospace'
- else
a deleted commit
- - else
+ - elsif discussion.diff_discussion?
+ on
- if discussion.active?
- = link_to diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code) do
- the diff
+ = link_to 'the diff', discussion_diff_path(discussion)
- else
an outdated diff
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index 2789391819c..34789808f10 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -1,18 +1,20 @@
-%ul.notes{ data: { discussion_id: discussion.id } }
- = render partial: "projects/notes/note", collection: discussion.notes, as: :note
+.discussion-notes
+ %ul.notes{ data: { discussion_id: discussion.id } }
+ = render partial: "projects/notes/note", collection: discussion.notes, as: :note
-- if current_user
- .discussion-reply-holder
- - if discussion.diff_discussion?
- - line_type = local_assigns.fetch(:line_type, nil)
+ - if current_user
+ .discussion-reply-holder
+ - if discussion.potentially_resolvable?
+ - line_type = local_assigns.fetch(:line_type, nil)
+
+ .btn-group-justified.discussion-with-resolve-btn{ role: "group" }
+ .btn-group{ role: "group" }
+ = link_to_reply_discussion(discussion, line_type)
+
+ = render "discussions/resolve_all", discussion: discussion
- .btn-group-justified.discussion-with-resolve-btn{ role: "group" }
- .btn-group{ role: "group" }
- = link_to_reply_discussion(discussion, line_type)
- = render "discussions/resolve_all", discussion: discussion
- - if discussion.for_merge_request?
.btn-group.discussion-actions
= render "discussions/new_issue_for_discussion", discussion: discussion, merge_request: discussion.noteable
= render "discussions/jump_to_next", discussion: discussion
- - else
- = link_to_reply_discussion(discussion)
+ - else
+ = link_to_reply_discussion(discussion)
diff --git a/app/views/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml
index 3a19e021643..253cd336882 100644
--- a/app/views/discussions/_parallel_diff_discussion.html.haml
+++ b/app/views/discussions/_parallel_diff_discussion.html.haml
@@ -1,20 +1,20 @@
-- expanded = discussion_left.try(:expanded?) || discussion_right.try(:expanded?)
+- expanded = [*discussions_left, *discussions_right].any?(&:expanded?)
%tr.notes_holder{ class: ('hide' unless expanded) }
- - if discussion_left
+ - if discussions_left
%td.notes_line.old
%td.notes_content.parallel.old
- .content{ class: ('hide' unless discussion_left.expanded?) }
- = render "discussions/notes", discussion: discussion_left, line_type: 'old'
+ .content{ class: ('hide' unless discussions_left.any?(&:expanded?)) }
+ = render partial: "discussions/notes", collection: discussions_left, as: :discussion, line_type: 'old'
- else
%td.notes_line.old= ("")
%td.notes_content.parallel.old
.content
- - if discussion_right
+ - if discussions_right
%td.notes_line.new
%td.notes_content.parallel.new
- .content{ class: ('hide' unless discussion_right.expanded?) }
- = render "discussions/notes", discussion: discussion_right, line_type: 'new'
+ .content{ class: ('hide' unless discussions_right.any?(&:expanded?)) }
+ = render partial: "discussions/notes", collection: discussions_right, as: :discussion, line_type: 'new'
- else
%td.notes_line.new= ("")
%td.notes_content.parallel.new
diff --git a/app/views/discussions/_resolve_all.html.haml b/app/views/discussions/_resolve_all.html.haml
index e30ee1b0e05..689a22acd27 100644
--- a/app/views/discussions/_resolve_all.html.haml
+++ b/app/views/discussions/_resolve_all.html.haml
@@ -1,9 +1,8 @@
-- if discussion.for_merge_request?
- %resolve-discussion-btn{ ":discussion-id" => "'#{discussion.id}'",
- ":merge-request-id" => discussion.noteable.iid,
- ":can-resolve" => discussion.can_resolve?(current_user),
- "inline-template" => true }
- .btn-group{ role: "group", "v-if" => "showButton" }
- %button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading", "v-cloak" => "true" }
- = icon("spinner spin", "v-show" => "loading")
- {{ buttonText }}
+%resolve-discussion-btn{ ":discussion-id" => "'#{discussion.id}'",
+ ":merge-request-id" => discussion.noteable.iid,
+ ":can-resolve" => discussion.can_resolve?(current_user),
+ "inline-template" => true }
+ .btn-group{ role: "group", "v-if" => "showButton" }
+ %button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading", "v-cloak" => "true" }
+ = icon("spinner spin", "v-show" => "loading")
+ {{ buttonText }}
diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb
new file mode 100644
index 00000000000..198f30a1dc4
--- /dev/null
+++ b/app/views/layouts/mailer.text.erb
@@ -0,0 +1,4 @@
+<%= yield -%>
+
+---
+You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
diff --git a/app/views/layouts/mailer.text.haml b/app/views/layouts/mailer.text.haml
deleted file mode 100644
index 6a9c6ced9cc..00000000000
--- a/app/views/layouts/mailer.text.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-= yield
-
-You're receiving this email because of your account on #{Gitlab.config.gitlab.host}.
-Manage all notifications: #{profile_notifications_url}
-Help: #{help_url}
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 76268c1b705..40bf45cece7 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -25,8 +25,8 @@
- if @labels_url
adjust your #{link_to 'label subscriptions', @labels_url}.
- else
- - if @sent_notification_url
- = link_to "unsubscribe", @sent_notification_url
+ - if @unsubscribe_url
+ = link_to "unsubscribe", @unsubscribe_url
from this thread or
adjust your notification settings.
diff --git a/app/views/layouts/notify.text.erb b/app/views/layouts/notify.text.erb
new file mode 100644
index 00000000000..b4ce02eead8
--- /dev/null
+++ b/app/views/layouts/notify.text.erb
@@ -0,0 +1,12 @@
+<%= yield -%>
+
+---
+<% if @target_url -%>
+<% if @reply_by_email -%>
+<%= "Reply to this email directly or view it on GitLab: #{@target_url}" -%>
+<% else -%>
+<%= "View it on GitLab: #{@target_url}" -%>
+<% end -%>
+<% end -%>
+
+You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
diff --git a/app/views/notify/_note_email.html.haml b/app/views/notify/_note_email.html.haml
new file mode 100644
index 00000000000..a80518f7986
--- /dev/null
+++ b/app/views/notify/_note_email.html.haml
@@ -0,0 +1,37 @@
+- discussion = @note.discussion if @note.part_of_discussion?
+- if discussion
+ %p.details
+ = succeed ':' do
+ = link_to @note.author_name, user_url(@note.author)
+
+ - if discussion.diff_discussion?
+ - if discussion.new_discussion?
+ started a new discussion
+ - else
+ commented on a discussion
+
+ on #{link_to discussion.file_path, @target_url}
+ - else
+ - if discussion.new_discussion?
+ started a new discussion
+ - else
+ commented on a #{link_to 'discussion', @target_url}
+
+- elsif current_application_settings.email_author_in_body
+ %p.details
+ #{link_to @note.author_name, user_url(@note.author)} commented:
+
+- if discussion&.diff_discussion?
+ = content_for :head do
+ = stylesheet_link_tag 'mailers/highlighted_diff_email'
+
+ %table
+ = render partial: "projects/diffs/line",
+ collection: discussion.truncated_diff_lines,
+ as: :line,
+ locals: { diff_file: discussion.diff_file,
+ plain: true,
+ email: true }
+
+%div
+ = markdown(@note.note, pipeline: :email, author: @note.author)
diff --git a/app/views/notify/_note_email.text.erb b/app/views/notify/_note_email.text.erb
new file mode 100644
index 00000000000..cb2e7fab6d5
--- /dev/null
+++ b/app/views/notify/_note_email.text.erb
@@ -0,0 +1,26 @@
+<% discussion = @note.discussion if @note.part_of_discussion? -%>
+<% if discussion && !discussion.individual_note? -%>
+<%= @note.author_name -%>
+<% if discussion.new_discussion? -%>
+<%= " started a new discussion" -%>
+<% else -%>
+<%= " commented on a discussion" -%>
+<% end -%>
+<% if discussion.diff_discussion? -%>
+<%= " on #{discussion.file_path}" -%>
+<% end -%>
+<%= ":" -%>
+
+
+<% elsif current_application_settings.email_author_in_body -%>
+<%= "#{@note.author_name} commented:" -%>
+
+
+<% end -%>
+<% if discussion&.diff_discussion? -%>
+<% discussion.truncated_diff_lines(highlight: false).each do |line| -%>
+<%= "> #{line.text}\n" -%>
+<% end -%>
+
+<% end -%>
+<%= @note.note -%>
diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml
deleted file mode 100644
index e9c66170877..00000000000
--- a/app/views/notify/_note_message.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-- if current_application_settings.email_author_in_body
- %div
- #{link_to @note.author_name, user_url(@note.author)} wrote:
-%div
- = markdown(@note.note, pipeline: :email, author: @note.author)
diff --git a/app/views/notify/_note_message.text.erb b/app/views/notify/_note_message.text.erb
deleted file mode 100644
index f82cbc9a3fc..00000000000
--- a/app/views/notify/_note_message.text.erb
+++ /dev/null
@@ -1,5 +0,0 @@
-<% if current_application_settings.email_author_in_body %>
- <%= @note.author_name %> wrote:
-<% end -%>
-
-<%= @note.note %>
diff --git a/app/views/notify/_note_mr_or_commit_email.html.haml b/app/views/notify/_note_mr_or_commit_email.html.haml
deleted file mode 100644
index edf8dfe7e9e..00000000000
--- a/app/views/notify/_note_mr_or_commit_email.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-= content_for :head do
- = stylesheet_link_tag 'mailers/highlighted_diff_email'
-
-New comment
-
-- if @discussion && @discussion.diff_file
- on
- = link_to @note.diff_file.file_path, @target_url, class: 'details'
- \:
- %table
- = render partial: "projects/diffs/line",
- collection: @discussion.truncated_diff_lines,
- as: :line,
- locals: { diff_file: @note.diff_file,
- plain: true,
- email: true }
-
-= render 'note_message'
diff --git a/app/views/notify/_note_mr_or_commit_email.text.erb b/app/views/notify/_note_mr_or_commit_email.text.erb
deleted file mode 100644
index b4fcdf6b1e9..00000000000
--- a/app/views/notify/_note_mr_or_commit_email.text.erb
+++ /dev/null
@@ -1,8 +0,0 @@
-<% if @discussion && @discussion.diff_file -%>
- on <%= @note.diff_file.file_path -%>
-<% end -%>:
-
-<%= url %>
-
-<%= render 'simple_diff' if @discussion -%>
-<%= render 'note_message' %>
diff --git a/app/views/notify/_simple_diff.text.erb b/app/views/notify/_simple_diff.text.erb
deleted file mode 100644
index c28d1cc34d3..00000000000
--- a/app/views/notify/_simple_diff.text.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<% @discussion.truncated_diff_lines(highlight: false).each do |line| %>
-> <%= line.text %>
-<% end %>
diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml
index d1855568215..c762578971a 100644
--- a/app/views/notify/new_issue_email.html.haml
+++ b/app/views/notify/new_issue_email.html.haml
@@ -1,9 +1,11 @@
- if current_application_settings.email_author_in_body
- %div
- #{link_to @issue.author_name, user_url(@issue.author)} wrote:
-- if @issue.description
- = markdown(@issue.description, pipeline: :email, author: @issue.author)
+ %p.details
+ #{link_to @issue.author_name, user_url(@issue.author)} created an issue:
- if @issue.assignee_id.present?
%p
Assignee: #{@issue.assignee_name}
+
+- if @issue.description
+ %div
+ = markdown(@issue.description, pipeline: :email, author: @issue.author)
diff --git a/app/views/notify/new_mention_in_issue_email.html.haml b/app/views/notify/new_mention_in_issue_email.html.haml
index 02f21baa368..6b45ac265f7 100644
--- a/app/views/notify/new_mention_in_issue_email.html.haml
+++ b/app/views/notify/new_mention_in_issue_email.html.haml
@@ -1,12 +1,4 @@
%p
You have been mentioned in an issue.
-- if current_application_settings.email_author_in_body
- %div
- #{link_to @issue.author_name, user_url(@issue.author)} wrote:
-- if @issue.description
- = markdown(@issue.description, pipeline: :email, author: @issue.author)
-
-- if @issue.assignee_id.present?
- %p
- Assignee: #{@issue.assignee_name}
+= render template: 'notify/new_issue_email'
diff --git a/app/views/notify/new_mention_in_merge_request_email.html.haml b/app/views/notify/new_mention_in_merge_request_email.html.haml
index cbd434be02a..b061f9c106e 100644
--- a/app/views/notify/new_mention_in_merge_request_email.html.haml
+++ b/app/views/notify/new_mention_in_merge_request_email.html.haml
@@ -1,15 +1,4 @@
%p
You have been mentioned in Merge Request #{@merge_request.to_reference}
-- if current_application_settings.email_author_in_body
- %div
- #{link_to @merge_request.author_name, user_url(@merge_request.author)} wrote:
-%p.details
- != merge_path_description(@merge_request, '&rarr;')
-
-- if @merge_request.assignee_id.present?
- %p
- Assignee: #{@merge_request.author_name} &rarr; #{@merge_request.assignee_name}
-
-- if @merge_request.description
- = markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
+= render template: 'notify/new_merge_request_email'
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 8890b300f7d..951c96bdb9c 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -1,12 +1,14 @@
- if current_application_settings.email_author_in_body
- %div
- #{link_to @merge_request.author_name, user_url(@merge_request.author)} wrote:
+ %p.details
+ #{link_to @merge_request.author_name, user_url(@merge_request.author)} created a merge request:
+
%p.details
!= merge_path_description(@merge_request, '&rarr;')
- if @merge_request.assignee_id.present?
%p
- Assignee: #{@merge_request.author_name} &rarr; #{@merge_request.assignee_name}
+ Assignee: #{@merge_request.assignee_name}
- if @merge_request.description
- = markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
+ %div
+ = markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
diff --git a/app/views/notify/note_commit_email.html.haml b/app/views/notify/note_commit_email.html.haml
index 0a650e3b2ca..5e69f01a486 100644
--- a/app/views/notify/note_commit_email.html.haml
+++ b/app/views/notify/note_commit_email.html.haml
@@ -1,2 +1 @@
-%p.details
- = render 'note_mr_or_commit_email'
+= render 'note_email'
diff --git a/app/views/notify/note_commit_email.text.erb b/app/views/notify/note_commit_email.text.erb
index 6aa085a172e..413d9e6e9ac 100644
--- a/app/views/notify/note_commit_email.text.erb
+++ b/app/views/notify/note_commit_email.text.erb
@@ -1,2 +1 @@
-New comment for Commit <%= @commit.short_id -%>
-<%= render partial: 'note_mr_or_commit_email', locals: { url: @target_url } %>
+<%= render 'note_email' %>
diff --git a/app/views/notify/note_issue_email.html.haml b/app/views/notify/note_issue_email.html.haml
index 2fa2f784661..5e69f01a486 100644
--- a/app/views/notify/note_issue_email.html.haml
+++ b/app/views/notify/note_issue_email.html.haml
@@ -1 +1 @@
-= render 'note_message'
+= render 'note_email'
diff --git a/app/views/notify/note_issue_email.text.erb b/app/views/notify/note_issue_email.text.erb
index e33cbcd70f2..413d9e6e9ac 100644
--- a/app/views/notify/note_issue_email.text.erb
+++ b/app/views/notify/note_issue_email.text.erb
@@ -1,9 +1 @@
-New comment for Issue <%= @issue.iid %>
-
-<%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue, anchor: "note_#{@note.id}")) %>
-
-
-Author: <%= @note.author_name %>
-
-<%= @note.note %>
-
+<%= render 'note_email' %>
diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml
index 0a650e3b2ca..5e69f01a486 100644
--- a/app/views/notify/note_merge_request_email.html.haml
+++ b/app/views/notify/note_merge_request_email.html.haml
@@ -1,2 +1 @@
-%p.details
- = render 'note_mr_or_commit_email'
+= render 'note_email'
diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb
index 2ce64c494cf..413d9e6e9ac 100644
--- a/app/views/notify/note_merge_request_email.text.erb
+++ b/app/views/notify/note_merge_request_email.text.erb
@@ -1,2 +1 @@
-New comment for Merge Request <%= @merge_request.to_reference -%>
-<%= render partial: 'note_mr_or_commit_email', locals: { url: @target_url }%>
+<%= render 'note_email' %>
diff --git a/app/views/notify/note_personal_snippet_email.html.haml b/app/views/notify/note_personal_snippet_email.html.haml
index 2fa2f784661..5e69f01a486 100644
--- a/app/views/notify/note_personal_snippet_email.html.haml
+++ b/app/views/notify/note_personal_snippet_email.html.haml
@@ -1 +1 @@
-= render 'note_message'
+= render 'note_email'
diff --git a/app/views/notify/note_personal_snippet_email.text.erb b/app/views/notify/note_personal_snippet_email.text.erb
index b2a8809a23b..413d9e6e9ac 100644
--- a/app/views/notify/note_personal_snippet_email.text.erb
+++ b/app/views/notify/note_personal_snippet_email.text.erb
@@ -1,8 +1 @@
-New comment for Snippet <%= @snippet.id %>
-
-<%= url_for(snippet_url(@snippet, anchor: "note_#{@note.id}")) %>
-
-
-Author: <%= @note.author_name %>
-
-<%= @note.note %>
+<%= render 'note_email' %>
diff --git a/app/views/notify/note_snippet_email.html.haml b/app/views/notify/note_snippet_email.html.haml
index 2fa2f784661..5e69f01a486 100644
--- a/app/views/notify/note_snippet_email.html.haml
+++ b/app/views/notify/note_snippet_email.html.haml
@@ -1 +1 @@
-= render 'note_message'
+= render 'note_email'
diff --git a/app/views/notify/note_snippet_email.text.erb b/app/views/notify/note_snippet_email.text.erb
index 4d5a406f4b0..413d9e6e9ac 100644
--- a/app/views/notify/note_snippet_email.text.erb
+++ b/app/views/notify/note_snippet_email.text.erb
@@ -1,8 +1 @@
-New comment for Snippet <%= @snippet.id %>
-
-<%= url_for(namespace_project_snippet_url(@snippet.project.namespace, @snippet.project, @snippet, anchor: "note_#{@note.id}")) %>
-
-
-Author: <%= @note.author_name %>
-
-<%= @note.note %>
+<%= render 'note_email' %>
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index d5fc283aa8d..0d11da2451a 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -10,6 +10,7 @@
- else
.block-connector
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment
+
= render "projects/notes/notes_with_form"
- if can_collaborate_with_project?
- %w(revert cherry-pick).each do |type|
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index c09c7b87e24..3e426ee9e7d 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -4,7 +4,7 @@
- type = line.type
- line_code = diff_file.line_code(line)
- if discussions && !line.meta?
- - discussion = discussions[line_code]
+ - line_discussions = discussions[line_code]
%tr.line_holder{ class: type, id: (line_code unless plain) }
- case type
- when 'match'
@@ -20,6 +20,7 @@
= link_text
- else
%a{ href: "##{line_code}", data: { linenumber: link_text } }
+ - discussion = line_discussions.try(:first)
- if discussion && discussion.resolvable? && !plain
%diff-note-avatars{ "discussion-id" => discussion.id }
%td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
@@ -34,6 +35,6 @@
- else
= diff_line_content(line.text)
-- if discussion
- - discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?)
- = render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded
+- if line_discussions
+ - discussion_expanded = local_assigns.fetch(:discussion_expanded, line_discussions.any?(&:expanded?))
+ = render "discussions/diff_discussion", discussions: line_discussions, expanded: discussion_expanded
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index b7346f27ddb..f920f359de2 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -6,7 +6,7 @@
- right = line[:right]
- last_line = right.new_pos if right
- unless @diff_notes_disabled
- - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file)
+ - discussions_left, discussions_right = parallel_diff_discussions(left, right, diff_file)
%tr.line_holder.parallel
- if left
- case left.type
@@ -20,6 +20,7 @@
- left_position = diff_file.position(left)
%td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } }
%a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } }
+ - discussion_left = discussions_left.try(:first)
- if discussion_left && discussion_left.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_left.id }
%td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text)
@@ -39,6 +40,7 @@
- right_position = diff_file.position(right)
%td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } }
%a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } }
+ - discussion_right = discussions_right.try(:first)
- if discussion_right && discussion_right.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_right.id }
%td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text)
@@ -46,8 +48,8 @@
%td.old_line.diff-line-num.empty-cell
%td.line_content.parallel
- - if discussion_left || discussion_right
- = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right
+ - if discussions_left || discussions_right
+ = render "discussions/parallel_diff_discussion", discussions_left: discussions_left, discussions_right: discussions_right
- if !diff_file.new_file && !diff_file.deleted_file && diff_file.diff_lines.any?
- last_line = diff_file.diff_lines.last
- if last_line.new_pos < total_lines
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index cfb44bd206c..15b5a51c1d0 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -1,9 +1,9 @@
- content_for :note_actions do
- if can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.open?
- = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"}
+ = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: { original_text: "Close merge request", alternative_text: "Comment & close merge request"}
- if @merge_request.reopenable?
- = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
+ = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-close js-note-target-reopen", title: "Reopen merge request", data: { original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
%comment-and-resolve-btn{ "inline-template" => true, ":discussion-id" => "" }
%button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } }
{{ buttonText }}
diff --git a/app/views/projects/notes/_comment_button.html.haml b/app/views/projects/notes/_comment_button.html.haml
new file mode 100644
index 00000000000..008363263b3
--- /dev/null
+++ b/app/views/projects/notes/_comment_button.html.haml
@@ -0,0 +1,28 @@
+- noteable_name = @note.noteable.human_class_name
+
+.pull-left.btn-group.append-right-10.comment-type-dropdown.js-comment-type-dropdown
+ %input.btn.btn-nr.btn-create.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: 'Comment' }
+
+ - if @note.can_be_discussion_note?
+ = button_tag type: 'button', class: 'btn btn-nr dropdown-toggle comment-btn js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => 'Open comment type dropdown' do
+ = icon('caret-down')
+
+ %ul#resolvable-comment-menu.dropdown-menu{ data: { dropdown: true } }
+ %li#comment.droplab-item-selected{ data: { value: '', 'button-text' => 'Comment', 'secondary-button-text' => "Comment & close #{noteable_name}" } }
+ = icon('check')
+ .description
+ %strong Comment
+ %p
+ Add a general comment to this #{noteable_name}.
+
+ %li.divider
+
+ %li#discussion{ data: { value: 'DiscussionNote', 'button-text' => 'Start discussion', 'secondary-button-text' => "Start discussion & close #{noteable_name}" } }
+ = icon('check')
+ .description
+ %strong Start discussion
+ %p
+ = succeed '.' do
+ Discuss a specific suggestion or question
+ - if @note.noteable.supports_resolvable_notes?
+ that needs to be resolved
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index b561052e721..0d835a9e949 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -4,12 +4,18 @@
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
= hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha)
+ = hidden_field_tag :in_reply_to_discussion_id
+
= note_target_fields(@note)
- = f.hidden_field :commit_id
- = f.hidden_field :line_code
- = f.hidden_field :noteable_id
= f.hidden_field :noteable_type
+ = f.hidden_field :noteable_id
+ = f.hidden_field :commit_id
= f.hidden_field :type
+
+ -# LegacyDiffNote
+ = f.hidden_field :line_code
+
+ -# DiffNote
= f.hidden_field :position
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
@@ -22,7 +28,9 @@
.error-alert
.note-form-actions.clearfix
- = f.submit 'Comment', class: "btn btn-nr btn-create append-right-10 comment-btn js-comment-button"
+ = render partial: 'projects/notes/comment_button'
+
= yield(:note_actions)
+
%a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } }
Discard draft
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 18afa811bad..b1b1b66b7f2 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -31,7 +31,7 @@
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
%resolve-btn{ "project-path" => project_path(note.project),
- "discussion-id" => note.discussion_id,
+ "discussion-id" => note.discussion_id(@noteable),
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/projects/notes/_notes.html.haml
index 022578bd6db..2b2bab09c74 100644
--- a/app/views/projects/notes/_notes.html.haml
+++ b/app/views/projects/notes/_notes.html.haml
@@ -1,7 +1,7 @@
-- if @discussions.present?
+- if defined?(@discussions)
- @discussions.each do |discussion|
- - if discussion.for_target?(@noteable)
- = render partial: "projects/notes/note", object: discussion.first_note, as: :note
+ - if discussion.individual_note?
+ = render partial: "projects/notes/note", collection: discussion.notes, as: :note
- else
= render 'discussions/discussion', discussion: discussion
- else
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 9e241c3ea12..b447996a8ab 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -23,7 +23,7 @@
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
- %input.form-control.filtered-search{ placeholder: 'Search or filter results...', data: { id: "filtered-search-#{type.to_s}", 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } }
+ %input.form-control.filtered-search{ id: "filtered-search-#{type.to_s}", placeholder: 'Search or filter results...', data: { 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } }
= icon('filter')
%button.clear-search.hidden{ type: 'button' }
= icon('times')
diff --git a/changelogs/unreleased/new-resolvable-discussion.yml b/changelogs/unreleased/new-resolvable-discussion.yml
new file mode 100644
index 00000000000..f4dc4ea3ede
--- /dev/null
+++ b/changelogs/unreleased/new-resolvable-discussion.yml
@@ -0,0 +1,4 @@
+---
+title: Add option to start a new resolvable discussion in an MR
+merge_request: 7527
+author:
diff --git a/db/migrate/20161128095517_add_in_reply_to_discussion_id_to_sent_notifications.rb b/db/migrate/20161128095517_add_in_reply_to_discussion_id_to_sent_notifications.rb
new file mode 100644
index 00000000000..d56d83ca1d3
--- /dev/null
+++ b/db/migrate/20161128095517_add_in_reply_to_discussion_id_to_sent_notifications.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddInReplyToDiscussionIdToSentNotifications < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :sent_notifications, :in_reply_to_discussion_id, :string
+ end
+end
diff --git a/db/post_migrate/20170404170532_remove_notes_original_discussion_id.rb b/db/post_migrate/20170404170532_remove_notes_original_discussion_id.rb
new file mode 100644
index 00000000000..0c3b3bd5eb3
--- /dev/null
+++ b/db/post_migrate/20170404170532_remove_notes_original_discussion_id.rb
@@ -0,0 +1,23 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveNotesOriginalDiscussionId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ remove_column :notes, :original_discussion_id, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index a7a8f5bd38f..c9dca702465 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,8 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170405080720) do
-
+ActiveRecord::Schema.define(version: 20170404170532) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
enable_extension "pg_trgm"
@@ -752,7 +751,6 @@ ActiveRecord::Schema.define(version: 20170405080720) do
t.datetime "resolved_at"
t.integer "resolved_by_id"
t.string "discussion_id"
- t.string "original_discussion_id"
t.text "note_html"
end
@@ -1028,6 +1026,7 @@ ActiveRecord::Schema.define(version: 20170405080720) do
t.string "line_code"
t.string "note_type"
t.text "position"
+ t.string "in_reply_to_discussion_id"
end
add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 5510c65265a..a67b3222af3 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -347,6 +347,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I should see a discussion by user "John Doe" has started on diff' do
+ # Trigger a refresh of notes
+ execute_script("$(document).trigger('visibilitychange');")
+ wait_for_ajax
page.within(".notes .discussion") do
page.should have_content "#{user_exists("John Doe").name} #{user_exists("John Doe").to_reference} started a discussion"
page.should have_content sample_commit.line_code_path
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 114656958e3..0a15c6d9358 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -33,6 +33,10 @@ module Gitlab
new_pos unless removed? || meta?
end
+ def line
+ new_line || old_line
+ end
+
def unchanged?
type.nil?
end
diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb
index d87ba427f4b..0e22f2189ee 100644
--- a/lib/gitlab/email/handler/create_note_handler.rb
+++ b/lib/gitlab/email/handler/create_note_handler.rb
@@ -1,4 +1,3 @@
-
require 'gitlab/email/handler/base_handler'
require 'gitlab/email/handler/reply_processing'
@@ -42,17 +41,7 @@ module Gitlab
end
def create_note
- Notes::CreateService.new(
- project,
- author,
- note: message,
- noteable_type: sent_notification.noteable_type,
- noteable_id: sent_notification.noteable_id,
- commit_id: sent_notification.commit_id,
- line_code: sent_notification.line_code,
- position: sent_notification.position,
- type: sent_notification.note_type
- ).execute
+ sent_notification.create_reply(message)
end
end
end
diff --git a/package.json b/package.json
index 312e38f7407..5be299da699 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
+ "@gitlab-org/droplab": "^0.1.0",
"babel-core": "^6.22.1",
"babel-loader": "^6.2.10",
"babel-plugin-transform-define": "^1.2.0",
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index b223a22ae60..69e4706dc71 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -266,8 +266,8 @@ describe Projects::CommitController do
diff_for_path(id: commit2.id, old_path: existing_path, new_path: existing_path)
expect(assigns(:diff_notes_disabled)).to be_falsey
- expect(assigns(:comments_target)).to eq(noteable_type: 'Commit',
- commit_id: commit2.id)
+ expect(assigns(:new_diff_note_attrs)).to eq(noteable_type: 'Commit',
+ commit_id: commit2.id)
end
it 'only renders the diffs for the path given' do
diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb
index 79ab364a6f3..fe62898fa9b 100644
--- a/spec/controllers/projects/discussions_controller_spec.rb
+++ b/spec/controllers/projects/discussions_controller_spec.rb
@@ -4,7 +4,7 @@ describe Projects::DiscussionsController do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.source_project }
- let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+ let(:note) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
let(:discussion) { note.discussion }
let(:request_params) do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index d5f1d46bf7f..79034b8d24d 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -519,7 +519,7 @@ describe Projects::IssuesController do
end
context 'resolving discussions in MergeRequest' do
- let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
+ let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
let(:merge_request) { discussion.noteable }
let(:project) { merge_request.source_project }
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 99d5583e683..1739d40ab88 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -586,8 +586,8 @@ describe Projects::MergeRequestsController do
diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path)
expect(assigns(:diff_notes_disabled)).to be_falsey
- expect(assigns(:comments_target)).to eq(noteable_type: 'MergeRequest',
- noteable_id: merge_request.id)
+ expect(assigns(:new_diff_note_attrs)).to eq(noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id)
end
it 'only renders the diffs for the path given' do
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index d80780b1d90..f140eaef5d5 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -14,6 +14,109 @@ describe Projects::NotesController do
}
end
+ describe 'GET index' do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ target_type: 'issue',
+ target_id: issue.id,
+ format: 'json'
+ }
+ end
+
+ let(:parsed_response) { JSON.parse(response.body).with_indifferent_access }
+ let(:note_json) { parsed_response[:notes].first }
+
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it 'passes last_fetched_at from headers to NotesFinder' do
+ last_fetched_at = 3.hours.ago.to_i
+
+ request.headers['X-Last-Fetched-At'] = last_fetched_at
+
+ expect(NotesFinder).to receive(:new)
+ .with(anything, anything, hash_including(last_fetched_at: last_fetched_at))
+ .and_call_original
+
+ get :index, request_params
+ end
+
+ context 'for a discussion note' do
+ let!(:note) { create(:discussion_note_on_issue, noteable: issue, project: project) }
+
+ it 'responds with the expected attributes' do
+ get :index, request_params
+
+ expect(note_json[:id]).to eq(note.id)
+ expect(note_json[:discussion_html]).not_to be_nil
+ expect(note_json[:diff_discussion_html]).to be_nil
+ end
+ end
+
+ context 'for a diff discussion note' do
+ let(:project) { create(:project, :repository) }
+ let!(:note) { create(:diff_note_on_merge_request, project: project) }
+
+ let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) }
+
+ it 'responds with the expected attributes' do
+ get :index, params
+
+ expect(note_json[:id]).to eq(note.id)
+ expect(note_json[:discussion_html]).not_to be_nil
+ expect(note_json[:diff_discussion_html]).not_to be_nil
+ end
+ end
+
+ context 'for a commit note' do
+ let(:project) { create(:project, :repository) }
+ let!(:note) { create(:note_on_commit, project: project) }
+
+ context 'when displayed on a merge request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ let(:params) { request_params.merge(target_type: 'merge_request', target_id: merge_request.id) }
+
+ it 'responds with the expected attributes' do
+ get :index, params
+
+ expect(note_json[:id]).to eq(note.id)
+ expect(note_json[:discussion_html]).not_to be_nil
+ expect(note_json[:diff_discussion_html]).to be_nil
+ end
+ end
+
+ context 'when displayed on the commit' do
+ let(:params) { request_params.merge(target_type: 'commit', target_id: note.commit_id) }
+
+ it 'responds with the expected attributes' do
+ get :index, params
+
+ expect(note_json[:id]).to eq(note.id)
+ expect(note_json[:discussion_html]).to be_nil
+ expect(note_json[:diff_discussion_html]).to be_nil
+ end
+ end
+ end
+
+ context 'for a regular note' do
+ let!(:note) { create(:note, noteable: issue, project: project) }
+
+ it 'responds with the expected attributes' do
+ get :index, request_params
+
+ expect(note_json[:id]).to eq(note.id)
+ expect(note_json[:html]).not_to be_nil
+ expect(note_json[:discussion_html]).to be_nil
+ expect(note_json[:diff_discussion_html]).to be_nil
+ end
+ end
+ end
+
describe 'POST create' do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.source_project }
@@ -49,7 +152,8 @@ describe Projects::NotesController do
note: 'some note',
noteable_id: merge_request.id.to_s,
noteable_type: 'MergeRequest',
- merge_request_diff_head_sha: 'sha'
+ merge_request_diff_head_sha: 'sha',
+ in_reply_to_discussion_id: nil
}
expect(Notes::CreateService).to receive(:new).with(project, user, service_params).and_return(double(execute: true))
@@ -200,31 +304,4 @@ describe Projects::NotesController do
end
end
end
-
- describe 'GET index' do
- let(:last_fetched_at) { '1487756246' }
- let(:request_params) do
- {
- namespace_id: project.namespace,
- project_id: project,
- target_type: 'issue',
- target_id: issue.id
- }
- end
-
- before do
- sign_in(user)
- project.team << [user, :developer]
- end
-
- it 'passes last_fetched_at from headers to NotesFinder' do
- request.headers['X-Last-Fetched-At'] = last_fetched_at
-
- expect(NotesFinder).to receive(:new)
- .with(anything, anything, hash_including(last_fetched_at: last_fetched_at))
- .and_call_original
-
- get :index, request_params
- end
- end
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index fe19a404e16..90c35e2c7f8 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -16,10 +16,21 @@ FactoryGirl.define do
factory :note_on_personal_snippet, traits: [:on_personal_snippet]
factory :system_note, traits: [:system]
- factory :legacy_diff_note_on_commit, traits: [:on_commit, :legacy_diff_note], class: LegacyDiffNote do
+ factory :discussion_note_on_merge_request, traits: [:on_merge_request], class: DiscussionNote do
association :project, :repository
+
+ trait :resolved do
+ resolved_at { Time.now }
+ resolved_by { create(:user) }
+ end
end
+ factory :discussion_note_on_issue, traits: [:on_issue], class: DiscussionNote
+
+ factory :discussion_note_on_commit, traits: [:on_commit], class: DiscussionNote
+
+ factory :legacy_diff_note_on_commit, traits: [:on_commit, :legacy_diff_note], class: LegacyDiffNote
+
factory :legacy_diff_note_on_merge_request, traits: [:on_merge_request, :legacy_diff_note], class: LegacyDiffNote do
association :project, :repository
end
@@ -37,7 +48,7 @@ FactoryGirl.define do
new_path: "files/ruby/popen.rb",
old_line: nil,
new_line: line_number,
- diff_refs: noteable.diff_refs
+ diff_refs: noteable.try(:diff_refs)
)
end
@@ -108,5 +119,18 @@ FactoryGirl.define do
trait :with_svg_attachment do
attachment { fixture_file_upload(Rails.root + "spec/fixtures/unsanitized.svg", "image/svg+xml") }
end
+
+ transient do
+ in_reply_to nil
+ end
+
+ before(:create) do |note, evaluator|
+ discussion = evaluator.in_reply_to
+ next unless discussion
+ discussion = discussion.to_discussion if discussion.is_a?(Note)
+ next unless discussion
+
+ note.assign_attributes(discussion.reply_attributes.merge(project: discussion.project))
+ end
end
end
diff --git a/spec/factories/sent_notifications.rb b/spec/factories/sent_notifications.rb
index 6287c40afe9..99253be5a22 100644
--- a/spec/factories/sent_notifications.rb
+++ b/spec/factories/sent_notifications.rb
@@ -2,7 +2,7 @@ FactoryGirl.define do
factory :sent_notification do
project factory: :empty_project
recipient factory: :user
- noteable factory: :issue
- reply_key "0123456789abcdef" * 2
+ noteable { create(:issue, project: project) }
+ reply_key { SentNotification.reply_key }
end
end
diff --git a/spec/features/discussion_comments_spec.rb b/spec/features/discussion_comments_spec.rb
new file mode 100644
index 00000000000..1c3cc086f8c
--- /dev/null
+++ b/spec/features/discussion_comments_spec.rb
@@ -0,0 +1,254 @@
+require 'spec_helper'
+
+shared_examples 'discussion comments' do |resource_name|
+ let(:form_selector) { '.js-main-target-form' }
+ let(:dropdown_selector) { "#{form_selector} .comment-type-dropdown" }
+ let(:toggle_selector) { "#{dropdown_selector} .dropdown-toggle" }
+ let(:menu_selector) { "#{dropdown_selector} .dropdown-menu" }
+ let(:submit_selector) { "#{form_selector} .js-comment-submit-button" }
+ let(:close_selector) { "#{form_selector} .btn-comment-and-close" }
+
+ it 'should show a comment type toggle' do
+ expect(page).to have_selector toggle_selector
+ end
+
+ describe 'when the toggle is clicked' do
+ before do
+ find("#{form_selector} .note-textarea").send_keys('a')
+
+ find(toggle_selector).click
+ end
+
+ it 'opens a comment type dropdown with "Comment" and "Start discussion"' do
+ expect(page).to have_selector menu_selector
+ end
+
+ it 'has a "Comment" item' do
+ menu = find(menu_selector)
+
+ expect(menu).to have_content 'Comment'
+ expect(menu).to have_content "Add a general comment to this #{resource_name}."
+ end
+
+ it 'has a "Start discussion" item' do
+ menu = find(menu_selector)
+
+ expect(menu).to have_content 'Start discussion'
+ expect(menu).to have_content 'Discuss a specific suggestion or question that needs to be resolved.'
+ end
+
+ it 'has the "Comment" item selected by default' do
+ find("#{menu_selector} li", match: :first)
+ items = all("#{menu_selector} li")
+
+ expect(items.first).to have_content 'Comment'
+ expect(items.first).to have_selector '.fa-check'
+ expect(items.first['class']).to match 'droplab-item-selected'
+
+ expect(items.last).to have_content 'Start discussion'
+ expect(items.last).not_to have_selector '.fa-check'
+ expect(items.last['class']).not_to match 'droplab-item-selected'
+ end
+
+ it '"Comment" will post a comment' do
+ find(submit_selector).click
+
+ find('.timeline .timeline-entry', match: :first)
+ new_comment = all('.timeline .timeline-entry').last
+
+ expect(new_comment).to have_content 'a'
+ expect(new_comment).not_to have_selector '.discussion'
+ end
+
+ if resource_name =~ /(issue|merge request)/
+ it "Comment & close' will post a comment and close the #{resource_name}" do
+ find(close_selector).click
+
+ find('.timeline .timeline-entry', match: :first)
+ entries = all('.timeline .timeline-entry')
+ close_note = entries.last
+ new_comment = entries[-2]
+
+ expect(close_note).to have_content 'closed'
+ expect(new_comment).not_to have_selector '.discussion'
+ end
+ end
+
+ it 'closes the menu when clicking the toggle' do
+ find(toggle_selector).click
+
+ expect(page).not_to have_selector menu_selector
+ end
+
+ it 'closes the menu when clicking the body' do
+ find('body').click
+
+ expect(page).not_to have_selector menu_selector
+ end
+
+ describe 'when selecting "Start discussion"' do
+ before do
+ find("#{menu_selector} li", match: :first)
+ first("#{menu_selector} li").click
+ end
+
+ it 'updates the note_type input to "DiscussionNote"' do
+ expect(find("#{form_selector} #note_type").value).to be 'DiscussionNote'
+ end
+
+ it 'updates the submit button text' do
+ expect(find(dropdown_selector)).to have_content "Start discussion"
+ end
+
+ if resource_name =~ /(issue|merge request)/
+ it 'updates the close button text' do
+ expect(find(close_selector)).to have_content "Start discussion & close #{resource_name}"
+ end
+ end
+
+ it 'closes the dropdown' do
+ expect(page).not_to have_selector menu_selector
+ end
+
+ it '"Start discussion" will post a discussion' do
+ find(submit_selector).click
+
+ find('.timeline .timeline-entry', match: :first)
+ new_comment = all('.timeline .timeline-entry').last
+
+ expect(new_comment).to have_content 'a'
+ expect(new_comment).to have_selector '.discussion'
+ end
+
+ if resource_name =~ /(issue|merge request)/
+ it "'Start discussion & close' will post a discussion and close the #{resource_name}" do
+ find(close_selector).click
+
+ find('.timeline .timeline-entry', match: :first)
+ entries = all('.timeline .timeline-entry')
+ close_note = entries.last
+ new_discussion = entries[-2]
+
+ expect(close_note).to have_content 'closed'
+ expect(new_discussion).to have_selector '.discussion'
+ end
+ end
+
+ describe 'when opening the menu' do
+ before do
+ find(toggle_selector).click
+ end
+
+ it 'should have "Start discussion" selected' do
+ find("#{menu_selector} li", match: :first)
+ items = all("#{menu_selector} li")
+
+ expect(items.first).to have_content 'Comment'
+ expect(items.first).not_to have_selector '.fa-check'
+ expect(items.first).not_to have_selector '.droplab-item-selected'
+
+ expect(items.last).to have_content 'Start discussion'
+ expect(items.last).to have_selector '.fa-check'
+ expect(items.last).to have_selector '.droplab-item-selected'
+ end
+
+ describe 'when selecting "Comment"' do
+ before do
+ find("#{menu_selector} li", match: :first)
+ all("#{menu_selector} li").last.click
+ end
+
+ it 'clears the note_type input"' do
+ expect(find("#{form_selector} #note_type").value).to be ''
+ end
+
+ it 'updates the submit button text' do
+ expect(find(dropdown_selector)).to have_content "Comment"
+ end
+
+ if resource_name =~ /(issue|merge request)/
+ it 'updates the close button text' do
+ expect(find(close_selector)).to have_content "Comment & close #{resource_name}"
+ end
+ end
+
+ it 'closes the dropdown' do
+ expect(page).not_to have_selector menu_selector
+ end
+
+ it 'should have "Comment" selected when opening the menu' do
+ find(toggle_selector).click
+
+ find("#{menu_selector} li", match: :first)
+ items = all("#{menu_selector} li")
+
+ expect(items.first).to have_content 'Comment'
+ expect(items.first).to have_selector '.fa-check'
+ expect(items.first).to have_selector '.droplab-item-selected'
+
+ expect(items.last).to have_content 'Start discussion'
+ expect(items.last).not_to have_selector '.fa-check'
+ expect(items.last).not_to have_selector '.droplab-item-selected'
+ end
+ end
+ end
+ end
+ end
+
+ if resource_name =~ /(issue|merge request)/
+ describe "on a closed #{resource_name}" do
+
+ end
+ end
+end
+
+describe 'Discussion Comments', :feature, :js do
+ include RepoHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ project.team << [user, :developer]
+
+ login_as(user)
+ end
+
+ describe 'on a merge request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it_behaves_like 'discussion comments', 'merge request'
+ end
+
+ describe 'on an issue' do
+ let(:issue) { create(:issue, project: project) }
+
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it_behaves_like 'discussion comments', 'issue'
+ end
+
+ describe 'on an snippet' do
+ let(:snippet) { create(:personal_snippet, :public, author: user) }
+
+ before do
+ visit snippet_path(snippet)
+ end
+
+ it_behaves_like 'discussion comments', 'snippet'
+ end
+
+ describe 'on a commit' do
+ before do
+ visit namespace_project_commit_path(project.namespace, project, sample_commit.id)
+ end
+
+ it_behaves_like 'discussion comments', 'commit'
+ end
+end
diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
index 572bca3de21..58f897cba3e 100644
--- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
@@ -4,7 +4,7 @@ feature 'Resolving all open discussions in a merge request from an issue', featu
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project) }
- let!(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: merge_request, project: project)]).first }
+ let!(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
describe 'as a user with access to the project' do
before do
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
index bc8cbe30e66..cae01f37b6b 100644
--- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Dropdown hint', js: true, feature: true do
+describe 'Dropdown hint', :js, :feature do
include FilteredSearchHelpers
include WaitForAjax
@@ -9,10 +9,6 @@ describe 'Dropdown hint', js: true, feature: true do
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_hint) { '#js-dropdown-hint' }
- def dropdown_hint_size
- page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size
- end
-
def click_hint(text)
find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: text).click
end
@@ -46,14 +42,16 @@ describe 'Dropdown hint', js: true, feature: true do
it 'does not filter `Press Enter or click to search`' do
filtered_search.set('randomtext')
- expect(page).to have_css(js_dropdown_hint, text: 'Press Enter or click to search', visible: false)
- expect(dropdown_hint_size).to eq(0)
+ hint_dropdown = find(js_dropdown_hint)
+
+ expect(hint_dropdown).to have_content('Press Enter or click to search')
+ expect(hint_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 0)
end
it 'filters with text' do
filtered_search.set('a')
- expect(dropdown_hint_size).to eq(3)
+ expect(find(js_dropdown_hint)).to have_selector('.filter-dropdown .filter-dropdown-item', count: 3)
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index c8645e08c4b..abe5d61e38c 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -28,10 +28,6 @@ describe 'Dropdown label', js: true, feature: true do
filter_dropdown.find('.filter-dropdown-item', text: text).click
end
- def dropdown_label_size
- filter_dropdown.all('.filter-dropdown-item').size
- end
-
def clear_search_field
find('.filtered-search-box .clear-search').click
end
@@ -81,7 +77,7 @@ describe 'Dropdown label', js: true, feature: true do
filtered_search.set('label:')
expect(filter_dropdown).to have_content(bug_label.title)
- expect(dropdown_label_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown-item', count: 1)
end
end
@@ -97,7 +93,8 @@ describe 'Dropdown label', js: true, feature: true do
expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible
expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible
- expect(dropdown_label_size).to eq(2)
+
+ expect(filter_dropdown).to have_selector('.filter-dropdown-item', count: 2)
clear_search_field
init_label_search
@@ -106,14 +103,14 @@ describe 'Dropdown label', js: true, feature: true do
expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible
expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible
- expect(dropdown_label_size).to eq(2)
+ expect(filter_dropdown).to have_selector('.filter-dropdown-item', count: 2)
end
it 'filters by multiple words with or without symbol' do
filtered_search.send_keys('Hig')
expect(filter_dropdown.find('.filter-dropdown-item', text: two_words_label.title)).to be_visible
- expect(dropdown_label_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown-item', count: 1)
clear_search_field
init_label_search
@@ -121,14 +118,14 @@ describe 'Dropdown label', js: true, feature: true do
filtered_search.send_keys('~Hig')
expect(filter_dropdown.find('.filter-dropdown-item', text: two_words_label.title)).to be_visible
- expect(dropdown_label_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown-item', count: 1)
end
it 'filters by multiple words containing single quotes with or without symbol' do
filtered_search.send_keys('won\'t')
expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_single_label.title)).to be_visible
- expect(dropdown_label_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown-item', count: 1)
clear_search_field
init_label_search
@@ -136,14 +133,14 @@ describe 'Dropdown label', js: true, feature: true do
filtered_search.send_keys('~won\'t')
expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_single_label.title)).to be_visible
- expect(dropdown_label_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown-item', count: 1)
end
it 'filters by multiple words containing double quotes with or without symbol' do
filtered_search.send_keys('won"t')
expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_label.title)).to be_visible
- expect(dropdown_label_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown-item', count: 1)
clear_search_field
init_label_search
@@ -151,14 +148,14 @@ describe 'Dropdown label', js: true, feature: true do
filtered_search.send_keys('~won"t')
expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_label.title)).to be_visible
- expect(dropdown_label_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown-item', count: 1)
end
it 'filters by special characters with or without symbol' do
filtered_search.send_keys('^+')
expect(filter_dropdown.find('.filter-dropdown-item', text: special_label.title)).to be_visible
- expect(dropdown_label_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown-item', count: 1)
clear_search_field
init_label_search
@@ -166,7 +163,7 @@ describe 'Dropdown label', js: true, feature: true do
filtered_search.send_keys('~^+')
expect(filter_dropdown.find('.filter-dropdown-item', text: special_label.title)).to be_visible
- expect(dropdown_label_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown-item', count: 1)
end
end
@@ -280,13 +277,13 @@ describe 'Dropdown label', js: true, feature: true do
create(:label, project: project, title: 'bug-label')
init_label_search
- expect(dropdown_label_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown-item', count: 1)
create(:label, project: project)
clear_search_field
init_label_search
- expect(dropdown_label_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown-item', count: 1)
end
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index 0a525bc68c9..448259057b0 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -65,7 +65,7 @@ describe 'Dropdown milestone', :feature, :js do
it 'should load all the milestones when opened' do
filtered_search.set('milestone:')
- expect(dropdown_milestone_size).to be > 0
+ expect(filter_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 6)
end
end
@@ -84,37 +84,37 @@ describe 'Dropdown milestone', :feature, :js do
it 'filters by name' do
filtered_search.send_keys('v1')
- expect(dropdown_milestone_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 1)
end
it 'filters by case insensitive name' do
filtered_search.send_keys('V1')
- expect(dropdown_milestone_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 1)
end
it 'filters by name with symbol' do
filtered_search.send_keys('%v1')
- expect(dropdown_milestone_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 1)
end
it 'filters by case insensitive name with symbol' do
filtered_search.send_keys('%V1')
- expect(dropdown_milestone_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 1)
end
it 'filters by special characters' do
filtered_search.send_keys('(+')
- expect(dropdown_milestone_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 1)
end
it 'filters by special characters with symbol' do
filtered_search.send_keys('%(+')
- expect(dropdown_milestone_size).to eq(1)
+ expect(filter_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 1)
end
end
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index 48e7af67616..28137f11b92 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -26,7 +26,7 @@ describe 'Search bar', js: true, feature: true do
filtered_search.native.send_keys(:down)
page.within '#js-dropdown-hint' do
- expect(page).to have_selector('.dropdown-active')
+ expect(page).to have_selector('.droplab-item-active')
end
end
@@ -79,28 +79,30 @@ describe 'Search bar', js: true, feature: true do
filtered_search.set('author')
- expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(1)
+ expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: 1)
find('.filtered-search-box .clear-search').click
filtered_search.click
- expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(original_size)
+ expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: original_size)
end
it 'resets the dropdown filters' do
+ filtered_search.click
+
+ hint_offset = get_left_style(find('#js-dropdown-hint')['style'])
+
filtered_search.set('a')
- hint_style = page.find('#js-dropdown-hint')['style']
- hint_offset = get_left_style(hint_style)
filtered_search.set('author:')
- expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0)
+ find('#js-dropdown-hint', visible: false)
find('.filtered-search-box .clear-search').click
filtered_search.click
- expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to be > 0
- expect(get_left_style(page.find('#js-dropdown-hint')['style'])).to eq(hint_offset)
+ expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: 4)
+ expect(get_left_style(find('#js-dropdown-hint')['style'])).to eq(hint_offset)
end
end
end
diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb
index a6c72b0b3ac..218d95a88b8 100644
--- a/spec/features/merge_requests/diff_notes_avatars_spec.rb
+++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb
@@ -164,9 +164,7 @@ feature 'Diff note avatars', feature: true, js: true do
context 'multiple comments' do
before do
- create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
- create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
- create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
+ create_list(:diff_note_on_merge_request, 3, project: project, noteable: merge_request, in_reply_to: note)
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: view)
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
index 69164aabdb2..88d28b649a4 100644
--- a/spec/features/merge_requests/diff_notes_resolve_spec.rb
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -191,7 +191,7 @@ feature 'Diff notes resolve', feature: true, js: true do
context 'multiple notes' do
before do
- create(:diff_note_on_merge_request, project: project, noteable: merge_request)
+ create(:diff_note_on_merge_request, project: project, noteable: merge_request, in_reply_to: note)
visit_merge_request
end
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index fab2d532e06..783f2e93909 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -25,7 +25,7 @@ describe 'Comments', feature: true do
describe 'the note form' do
it 'is valid' do
is_expected.to have_css('.js-main-target-form', visible: true, count: 1)
- expect(find('.js-main-target-form input[type=submit]').value).
+ expect(find('.js-main-target-form .js-comment-button').value).
to eq('Comment')
page.within('.js-main-target-form') do
expect(page).not_to have_link('Cancel')
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 77a04507be1..765bf44d863 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -202,4 +202,45 @@ describe NotesFinder do
end
end
end
+
+ describe '#target' do
+ subject { described_class.new(project, user, params) }
+
+ context 'for a issue target' do
+ let(:issue) { create(:issue, project: project) }
+ let(:params) { { target_type: 'issue', target_id: issue.id } }
+
+ it 'returns the issue' do
+ expect(subject.target).to eq(issue)
+ end
+ end
+
+ context 'for a merge request target' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:params) { { target_type: 'merge_request', target_id: merge_request.id } }
+
+ it 'returns the merge_request' do
+ expect(subject.target).to eq(merge_request)
+ end
+ end
+
+ context 'for a snippet target' do
+ let(:snippet) { create(:project_snippet, project: project) }
+ let(:params) { { target_type: 'snippet', target_id: snippet.id } }
+
+ it 'returns the snippet' do
+ expect(subject.target).to eq(snippet)
+ end
+ end
+
+ context 'for a commit target' do
+ let(:project) { create(:project, :repository) }
+ let(:commit) { project.commit }
+ let(:params) { { target_type: 'commit', target_id: commit.id } }
+
+ it 'returns the commit' do
+ expect(subject.target).to eq(commit)
+ end
+ end
+ end
end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index f0554cc068d..540cb0ab1e0 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -150,7 +150,7 @@ describe IssuesHelper do
describe "when passing a discussion" do
let(:diff_note) { create(:diff_note_on_merge_request) }
let(:merge_request) { diff_note.noteable }
- let(:discussion) { Discussion.new([diff_note]) }
+ let(:discussion) { diff_note.to_discussion }
it "links to the merge request with first note if a single discussion was passed" do
expected_path = Gitlab::UrlBuilder.build(diff_note)
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index 9c577501f00..a427de32c4c 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -36,21 +36,4 @@ describe NotesHelper do
expect(helper.note_max_access_for_user(other_note)).to eq('Reporter')
end
end
-
- describe '#preload_max_access_for_authors' do
- before do
- # This method reads cache from RequestStore, so make sure it's clean.
- RequestStore.clear!
- end
-
- it 'loads multiple users' do
- expected_access = {
- owner.id => Gitlab::Access::OWNER,
- master.id => Gitlab::Access::MASTER,
- reporter.id => Gitlab::Access::REPORTER
- }
-
- expect(helper.preload_max_access_for_authors(notes, project)).to eq(expected_access)
- end
- end
end
diff --git a/spec/javascripts/comment_type_toggle_spec.js b/spec/javascripts/comment_type_toggle_spec.js
new file mode 100644
index 00000000000..818c1635710
--- /dev/null
+++ b/spec/javascripts/comment_type_toggle_spec.js
@@ -0,0 +1,124 @@
+import CommentTypeToggle from '~/comment_type_toggle';
+import * as dropLabSrc from '~/droplab/drop_lab';
+import InputSetter from '~/droplab/plugins/input_setter';
+
+describe('CommentTypeToggle', function () {
+ describe('class constructor', function () {
+ beforeEach(function () {
+ this.dropdownTrigger = {};
+ this.dropdownList = {};
+ this.noteTypeInput = {};
+ this.submitButton = {};
+ this.closeButton = {};
+
+ this.commentTypeToggle = new CommentTypeToggle(
+ this.dropdownTrigger,
+ this.dropdownList,
+ this.noteTypeInput,
+ this.submitButton,
+ this.closeButton,
+ );
+ });
+
+ it('should set .dropdownTrigger', function () {
+ expect(this.commentTypeToggle.dropdownTrigger).toBe(this.dropdownTrigger);
+ });
+
+ it('should set .dropdownList', function () {
+ expect(this.commentTypeToggle.dropdownList).toBe(this.dropdownList);
+ });
+
+ it('should set .noteTypeInput', function () {
+ expect(this.commentTypeToggle.noteTypeInput).toBe(this.noteTypeInput);
+ });
+
+ it('should set .submitButton', function () {
+ expect(this.commentTypeToggle.submitButton).toBe(this.submitButton);
+ });
+
+ it('should set .closeButton', function () {
+ expect(this.commentTypeToggle.closeButton).toBe(this.closeButton);
+ });
+ });
+
+ describe('initDroplab', function () {
+ beforeEach(function () {
+ this.commentTypeToggle = {
+ dropdownTrigger: {},
+ dropdownList: {},
+ noteTypeInput: {},
+ submitButton: {},
+ closeButton: {},
+ };
+
+ this.droplab = jasmine.createSpyObj('droplab', ['init']);
+
+ spyOn(dropLabSrc, 'default').and.returnValue(this.droplab);
+
+ this.initDroplab = CommentTypeToggle.prototype.initDroplab.call(this.commentTypeToggle);
+ });
+
+ it('should instantiate a DropLab instance', function () {
+ expect(dropLabSrc.default).toHaveBeenCalled();
+ });
+
+ it('should set .droplab', function () {
+ expect(this.commentTypeToggle.droplab).toBe(this.droplab);
+ });
+
+ it('should call DropLab.prototype.init', function () {
+ expect(this.droplab.init).toHaveBeenCalledWith(
+ this.commentTypeToggle.dropdownTrigger,
+ this.commentTypeToggle.dropdownList,
+ [InputSetter],
+ {
+ InputSetter: [{
+ input: this.commentTypeToggle.noteTypeInput,
+ valueAttribute: 'data-value',
+ }, {
+ input: this.commentTypeToggle.submitButton,
+ valueAttribute: 'data-button-text',
+ },
+ {
+ input: this.commentTypeToggle.closeButton,
+ valueAttribute: 'data-secondary-button-text',
+ }, {
+ input: this.commentTypeToggle.closeButton,
+ valueAttribute: 'data-secondary-button-text',
+ inputAttribute: 'data-alternative-text',
+ }],
+ },
+ );
+ });
+
+ describe('if no .closeButton is provided', function () {
+ beforeEach(function () {
+ this.commentTypeToggle = {
+ dropdownTrigger: {},
+ dropdownList: {},
+ noteTypeInput: {},
+ submitButton: {},
+ };
+
+ this.initDroplab = CommentTypeToggle.prototype.initDroplab.call(this.commentTypeToggle);
+ });
+
+ it('should not add .closeButton related InputSetter config', function () {
+ expect(this.droplab.init).toHaveBeenCalledWith(
+ this.commentTypeToggle.dropdownTrigger,
+ this.commentTypeToggle.dropdownList,
+ [InputSetter],
+ {
+ InputSetter: [{
+ input: this.commentTypeToggle.noteTypeInput,
+ valueAttribute: 'data-value',
+ }, {
+ input: this.commentTypeToggle.submitButton,
+ valueAttribute: 'data-button-text',
+ }],
+ },
+ );
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/droplab/constants.js b/spec/javascripts/droplab/constants.js
new file mode 100644
index 00000000000..35239e4fb8e
--- /dev/null
+++ b/spec/javascripts/droplab/constants.js
@@ -0,0 +1,29 @@
+/* eslint-disable */
+
+import * as constants from '~/droplab/constants';
+
+describe('constants', function () {
+ describe('DATA_TRIGGER', function () {
+ it('should be `data-dropdown-trigger`', function() {
+ expect(constants.DATA_TRIGGER).toBe('data-dropdown-trigger');
+ });
+ });
+
+ describe('DATA_DROPDOWN', function () {
+ it('should be `data-dropdown`', function() {
+ expect(constants.DATA_DROPDOWN).toBe('data-dropdown');
+ });
+ });
+
+ describe('SELECTED_CLASS', function () {
+ it('should be `droplab-item-selected`', function() {
+ expect(constants.SELECTED_CLASS).toBe('droplab-item-selected');
+ });
+ });
+
+ describe('ACTIVE_CLASS', function () {
+ it('should be `droplab-item-active`', function() {
+ expect(constants.ACTIVE_CLASS).toBe('droplab-item-active');
+ });
+ });
+});
diff --git a/spec/javascripts/droplab/drop_down.js b/spec/javascripts/droplab/drop_down.js
new file mode 100644
index 00000000000..bbf953658c8
--- /dev/null
+++ b/spec/javascripts/droplab/drop_down.js
@@ -0,0 +1,578 @@
+/* eslint-disable */
+
+import DropDown from '~/droplab/drop_down';
+import utils from '~/droplab/utils';
+import { SELECTED_CLASS } from '~/droplab/constants';
+
+describe('DropDown', function () {
+ describe('class constructor', function () {
+ beforeEach(function () {
+ spyOn(DropDown.prototype, 'getItems');
+ spyOn(DropDown.prototype, 'initTemplateString');
+ spyOn(DropDown.prototype, 'addEvents');
+
+ this.list = { innerHTML: 'innerHTML' };
+ this.dropdown = new DropDown(this.list);
+ });
+
+ it('sets the .hidden property to true', function () {
+ expect(this.dropdown.hidden).toBe(true);
+ })
+
+ it('sets the .list property', function () {
+ expect(this.dropdown.list).toBe(this.list);
+ });
+
+ it('calls .getItems', function () {
+ expect(DropDown.prototype.getItems).toHaveBeenCalled();
+ });
+
+ it('calls .initTemplateString', function () {
+ expect(DropDown.prototype.initTemplateString).toHaveBeenCalled();
+ });
+
+ it('calls .addEvents', function () {
+ expect(DropDown.prototype.addEvents).toHaveBeenCalled();
+ });
+
+ it('sets the .initialState property to the .list.innerHTML', function () {
+ expect(this.dropdown.initialState).toBe(this.list.innerHTML);
+ });
+
+ describe('if the list argument is a string', function () {
+ beforeEach(function () {
+ this.element = {};
+ this.selector = '.selector';
+
+ spyOn(Document.prototype, 'querySelector').and.returnValue(this.element);
+
+ this.dropdown = new DropDown(this.selector);
+ });
+
+ it('calls .querySelector with the selector string', function () {
+ expect(Document.prototype.querySelector).toHaveBeenCalledWith(this.selector);
+ });
+
+ it('sets the .list property element', function () {
+ expect(this.dropdown.list).toBe(this.element);
+ });
+ });
+ });
+
+ describe('getItems', function () {
+ beforeEach(function () {
+ this.list = { querySelectorAll: () => {} };
+ this.dropdown = { list: this.list };
+ this.nodeList = [];
+
+ spyOn(this.list, 'querySelectorAll').and.returnValue(this.nodeList);
+
+ this.getItems = DropDown.prototype.getItems.call(this.dropdown);
+ });
+
+ it('calls .querySelectorAll with a list item query', function () {
+ expect(this.list.querySelectorAll).toHaveBeenCalledWith('li');
+ });
+
+ it('sets the .items property to the returned list items', function () {
+ expect(this.dropdown.items).toEqual(jasmine.any(Array));
+ });
+
+ it('returns the .items', function () {
+ expect(this.getItems).toEqual(jasmine.any(Array));
+ });
+ });
+
+ describe('initTemplateString', function () {
+ beforeEach(function () {
+ this.items = [{ outerHTML: '<a></a>' }, { outerHTML: '<img>' }];
+ this.dropdown = { items: this.items };
+
+ DropDown.prototype.initTemplateString.call(this.dropdown);
+ });
+
+ it('should set .templateString to the last items .outerHTML', function () {
+ expect(this.dropdown.templateString).toBe(this.items[1].outerHTML);
+ });
+
+ it('should not set .templateString to a non-last items .outerHTML', function () {
+ expect(this.dropdown.templateString).not.toBe(this.items[0].outerHTML);
+ });
+
+ describe('if .items is not set', function () {
+ beforeEach(function () {
+ this.dropdown = { getItems: () => {} };
+
+ spyOn(this.dropdown, 'getItems').and.returnValue([]);
+
+ DropDown.prototype.initTemplateString.call(this.dropdown);
+ });
+
+ it('should call .getItems', function () {
+ expect(this.dropdown.getItems).toHaveBeenCalled();
+ });
+ });
+
+ describe('if items array is empty', function () {
+ beforeEach(function () {
+ this.dropdown = { items: [] };
+
+ DropDown.prototype.initTemplateString.call(this.dropdown);
+ });
+
+ it('should set .templateString to an empty string', function () {
+ expect(this.dropdown.templateString).toBe('');
+ });
+ });
+ });
+
+ describe('clickEvent', function () {
+ beforeEach(function () {
+ this.list = { dispatchEvent: () => {} };
+ this.dropdown = { hide: () => {}, list: this.list, addSelectedClass: () => {} };
+ this.event = { preventDefault: () => {}, target: 'target' };
+ this.customEvent = {};
+ this.closestElement = {};
+
+ spyOn(this.dropdown, 'hide');
+ spyOn(this.dropdown, 'addSelectedClass');
+ spyOn(this.list, 'dispatchEvent');
+ spyOn(this.event, 'preventDefault');
+ spyOn(window, 'CustomEvent').and.returnValue(this.customEvent);
+ spyOn(utils, 'closest').and.returnValues(this.closestElement, undefined);
+
+ DropDown.prototype.clickEvent.call(this.dropdown, this.event);
+ });
+
+ it('should call utils.closest', function () {
+ expect(utils.closest).toHaveBeenCalledWith(this.event.target, 'LI');
+ });
+
+ it('should call addSelectedClass', function () {
+ expect(this.dropdown.addSelectedClass).toHaveBeenCalledWith(this.closestElement);
+ })
+
+ it('should call .preventDefault', function () {
+ expect(this.event.preventDefault).toHaveBeenCalled();
+ });
+
+ it('should call .hide', function () {
+ expect(this.dropdown.hide).toHaveBeenCalled();
+ });
+
+ it('should construct CustomEvent', function () {
+ expect(window.CustomEvent).toHaveBeenCalledWith('click.dl', jasmine.any(Object));
+ });
+
+ it('should call .dispatchEvent with the customEvent', function () {
+ expect(this.list.dispatchEvent).toHaveBeenCalledWith(this.customEvent);
+ });
+
+ describe('if no selected element exists', function () {
+ beforeEach(function () {
+ this.event.preventDefault.calls.reset();
+ this.clickEvent = DropDown.prototype.clickEvent.call(this.dropdown, this.event);
+ });
+
+ it('should return undefined', function () {
+ expect(this.clickEvent).toBe(undefined);
+ });
+
+ it('should return before .preventDefault is called', function () {
+ expect(this.event.preventDefault).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('addSelectedClass', function () {
+ beforeEach(function () {
+ this.items = Array(4).forEach((item, i) => {
+ this.items[i] = { classList: { add: () => {} } };
+ spyOn(this.items[i].classList, 'add');
+ });
+ this.selected = { classList: { add: () => {} } };
+ this.dropdown = { removeSelectedClasses: () => {} };
+
+ spyOn(this.dropdown, 'removeSelectedClasses');
+ spyOn(this.selected.classList, 'add');
+
+ DropDown.prototype.addSelectedClass.call(this.dropdown, this.selected);
+ });
+
+ it('should call .removeSelectedClasses', function () {
+ expect(this.dropdown.removeSelectedClasses).toHaveBeenCalled();
+ });
+
+ it('should call .classList.add', function () {
+ expect(this.selected.classList.add).toHaveBeenCalledWith(SELECTED_CLASS);
+ });
+ });
+
+ describe('removeSelectedClasses', function () {
+ beforeEach(function () {
+ this.items = Array(4);
+ this.items.forEach((item, i) => {
+ this.items[i] = { classList: { add: () => {} } };
+ spyOn(this.items[i].classList, 'add');
+ });
+ this.dropdown = { items: this.items };
+
+ DropDown.prototype.removeSelectedClasses.call(this.dropdown);
+ });
+
+ it('should call .classList.remove for all items', function () {
+ this.items.forEach((item, i) => {
+ expect(this.items[i].classList.add).toHaveBeenCalledWith(SELECTED_CLASS);
+ });
+ });
+
+ describe('if .items is not set', function () {
+ beforeEach(function () {
+ this.dropdown = { getItems: () => {} };
+
+ spyOn(this.dropdown, 'getItems').and.returnValue([]);
+
+ DropDown.prototype.removeSelectedClasses.call(this.dropdown);
+ });
+
+ it('should call .getItems', function () {
+ expect(this.dropdown.getItems).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('addEvents', function () {
+ beforeEach(function () {
+ this.list = { addEventListener: () => {} };
+ this.dropdown = { list: this.list, clickEvent: () => {}, eventWrapper: {} };
+
+ spyOn(this.list, 'addEventListener');
+
+ DropDown.prototype.addEvents.call(this.dropdown);
+ });
+
+ it('should call .addEventListener', function () {
+ expect(this.list.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function));
+ });
+ });
+
+ describe('toggle', function () {
+ beforeEach(function () {
+ this.dropdown = { hidden: true, show: () => {}, hide: () => {} };
+
+ spyOn(this.dropdown, 'show');
+ spyOn(this.dropdown, 'hide');
+
+ DropDown.prototype.toggle.call(this.dropdown);
+ });
+
+ it('should call .show if hidden is true', function () {
+ expect(this.dropdown.show).toHaveBeenCalled();
+ });
+
+ describe('if hidden is false', function () {
+ beforeEach(function () {
+ this.dropdown = { hidden: false, show: () => {}, hide: () => {} };
+
+ spyOn(this.dropdown, 'show');
+ spyOn(this.dropdown, 'hide');
+
+ DropDown.prototype.toggle.call(this.dropdown);
+ });
+
+ it('should call .show if hidden is true', function () {
+ expect(this.dropdown.hide).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('setData', function () {
+ beforeEach(function () {
+ this.dropdown = { render: () => {} };
+ this.data = ['data'];
+
+ spyOn(this.dropdown, 'render');
+
+ DropDown.prototype.setData.call(this.dropdown, this.data);
+ });
+
+ it('should set .data', function () {
+ expect(this.dropdown.data).toBe(this.data);
+ });
+
+ it('should call .render with the .data', function () {
+ expect(this.dropdown.render).toHaveBeenCalledWith(this.data);
+ });
+ });
+
+ describe('addData', function () {
+ beforeEach(function () {
+ this.dropdown = { render: () => {}, data: ['data1'] };
+ this.data = ['data2'];
+
+ spyOn(this.dropdown, 'render');
+ spyOn(Array.prototype, 'concat').and.callThrough();
+
+ DropDown.prototype.addData.call(this.dropdown, this.data);
+ });
+
+ it('should call .concat with data', function () {
+ expect(Array.prototype.concat).toHaveBeenCalledWith(this.data);
+ });
+
+ it('should set .data with concatination', function () {
+ expect(this.dropdown.data).toEqual(['data1', 'data2']);
+ });
+
+ it('should call .render with the .data', function () {
+ expect(this.dropdown.render).toHaveBeenCalledWith(['data1', 'data2']);
+ });
+
+ describe('if .data is undefined', function () {
+ beforeEach(function () {
+ this.dropdown = { render: () => {}, data: undefined };
+ this.data = ['data2'];
+
+ spyOn(this.dropdown, 'render');
+
+ DropDown.prototype.addData.call(this.dropdown, this.data);
+ });
+
+ it('should set .data with concatination', function () {
+ expect(this.dropdown.data).toEqual(['data2']);
+ });
+ });
+ });
+
+ describe('render', function () {
+ beforeEach(function () {
+ this.list = { querySelector: () => {} };
+ this.dropdown = { renderChildren: () => {}, list: this.list };
+ this.renderableList = {};
+ this.data = [0, 1];
+
+ spyOn(this.dropdown, 'renderChildren').and.callFake(data => data);
+ spyOn(this.list, 'querySelector').and.returnValue(this.renderableList);
+ spyOn(this.data, 'map').and.callThrough();
+
+ DropDown.prototype.render.call(this.dropdown, this.data);
+ });
+
+ it('should call .map', function () {
+ expect(this.data.map).toHaveBeenCalledWith(jasmine.any(Function));
+ });
+
+ it('should call .renderChildren for each data item', function() {
+ expect(this.dropdown.renderChildren.calls.count()).toBe(this.data.length);
+ });
+
+ it('sets the renderableList .innerHTML', function () {
+ expect(this.renderableList.innerHTML).toBe('01');
+ });
+
+ describe('if no data argument is passed' , function () {
+ beforeEach(function () {
+ this.data.map.calls.reset();
+ this.dropdown.renderChildren.calls.reset();
+
+ DropDown.prototype.render.call(this.dropdown, undefined);
+ });
+
+ it('should not call .map', function () {
+ expect(this.data.map).not.toHaveBeenCalled();
+ });
+
+ it('should not call .renderChildren', function () {
+ expect(this.dropdown.renderChildren).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('if no dynamic list is present', function () {
+ beforeEach(function () {
+ this.list = { querySelector: () => {} };
+ this.dropdown = { renderChildren: () => {}, list: this.list };
+ this.data = [0, 1];
+
+ spyOn(this.dropdown, 'renderChildren').and.callFake(data => data);
+ spyOn(this.list, 'querySelector');
+ spyOn(this.data, 'map').and.callThrough();
+
+ DropDown.prototype.render.call(this.dropdown, this.data);
+ });
+
+ it('sets the .list .innerHTML', function () {
+ expect(this.list.innerHTML).toBe('01');
+ });
+ });
+ });
+
+ describe('renderChildren', function () {
+ beforeEach(function () {
+ this.templateString = 'templateString';
+ this.dropdown = { setImagesSrc: () => {}, templateString: this.templateString };
+ this.data = { droplab_hidden: true };
+ this.html = 'html';
+ this.template = { firstChild: { outerHTML: 'outerHTML', style: {} } };
+
+ spyOn(utils, 't').and.returnValue(this.html);
+ spyOn(document, 'createElement').and.returnValue(this.template);
+ spyOn(this.dropdown, 'setImagesSrc');
+
+ this.renderChildren = DropDown.prototype.renderChildren.call(this.dropdown, this.data);
+ });
+
+ it('should call utils.t with .templateString and data', function () {
+ expect(utils.t).toHaveBeenCalledWith(this.templateString, this.data);
+ });
+
+ it('should call document.createElement', function () {
+ expect(document.createElement).toHaveBeenCalledWith('div');
+ });
+
+ it('should set the templates .innerHTML to the HTML', function () {
+ expect(this.template.innerHTML).toBe(this.html);
+ });
+
+ it('should call .setImagesSrc with the template', function () {
+ expect(this.dropdown.setImagesSrc).toHaveBeenCalledWith(this.template);
+ });
+
+ it('should set the template display to none', function () {
+ expect(this.template.firstChild.style.display).toBe('none');
+ });
+
+ it('should return the templates .firstChild.outerHTML', function () {
+ expect(this.renderChildren).toBe(this.template.firstChild.outerHTML);
+ });
+
+ describe('if droplab_hidden is false', function () {
+ beforeEach(function () {
+ this.data = { droplab_hidden: false };
+ this.renderChildren = DropDown.prototype.renderChildren.call(this.dropdown, this.data);
+ });
+
+ it('should set the template display to block', function () {
+ expect(this.template.firstChild.style.display).toBe('block');
+ });
+ });
+ });
+
+ describe('setImagesSrc', function () {
+ beforeEach(function () {
+ this.dropdown = {};
+ this.template = { querySelectorAll: () => {} };
+
+ spyOn(this.template, 'querySelectorAll').and.returnValue([]);
+
+ DropDown.prototype.setImagesSrc.call(this.dropdown, this.template);
+ });
+
+ it('should call .querySelectorAll', function () {
+ expect(this.template.querySelectorAll).toHaveBeenCalledWith('img[data-src]');
+ });
+ });
+
+ describe('show', function () {
+ beforeEach(function () {
+ this.list = { style: {} };
+ this.dropdown = { list: this.list, hidden: true };
+
+ DropDown.prototype.show.call(this.dropdown);
+ });
+
+ it('it should set .list display to block', function () {
+ expect(this.list.style.display).toBe('block');
+ });
+
+ it('it should set .hidden to false', function () {
+ expect(this.dropdown.hidden).toBe(false);
+ });
+
+ describe('if .hidden is false', function () {
+ beforeEach(function () {
+ this.list = { style: {} };
+ this.dropdown = { list: this.list, hidden: false };
+
+ this.show = DropDown.prototype.show.call(this.dropdown);
+ });
+
+ it('should return undefined', function () {
+ expect(this.show).toEqual(undefined);
+ });
+
+ it('should not set .list display to block', function () {
+ expect(this.list.style.display).not.toEqual('block');
+ });
+ });
+ });
+
+ describe('hide', function () {
+ beforeEach(function () {
+ this.list = { style: {} };
+ this.dropdown = { list: this.list };
+
+ DropDown.prototype.hide.call(this.dropdown);
+ });
+
+ it('it should set .list display to none', function () {
+ expect(this.list.style.display).toBe('none');
+ });
+
+ it('it should set .hidden to true', function () {
+ expect(this.dropdown.hidden).toBe(true);
+ });
+ });
+
+ describe('toggle', function () {
+ beforeEach(function () {
+ this.hidden = true
+ this.dropdown = { hidden: this.hidden, show: () => {}, hide: () => {} };
+
+ spyOn(this.dropdown, 'show');
+ spyOn(this.dropdown, 'hide');
+
+ DropDown.prototype.toggle.call(this.dropdown);
+ });
+
+ it('should call .show', function () {
+ expect(this.dropdown.show).toHaveBeenCalled();
+ });
+
+ describe('if .hidden is false', function () {
+ beforeEach(function () {
+ this.hidden = false
+ this.dropdown = { hidden: this.hidden, show: () => {}, hide: () => {} };
+
+ spyOn(this.dropdown, 'show');
+ spyOn(this.dropdown, 'hide');
+
+ DropDown.prototype.toggle.call(this.dropdown);
+ });
+
+ it('should call .hide', function () {
+ expect(this.dropdown.hide).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('destroy', function () {
+ beforeEach(function () {
+ this.list = { removeEventListener: () => {} };
+ this.eventWrapper = { clickEvent: 'clickEvent' };
+ this.dropdown = { list: this.list, hide: () => {}, eventWrapper: this.eventWrapper };
+
+ spyOn(this.list, 'removeEventListener');
+ spyOn(this.dropdown, 'hide');
+
+ DropDown.prototype.destroy.call(this.dropdown);
+ });
+
+ it('it should call .hide', function () {
+ expect(this.dropdown.hide).toHaveBeenCalled();
+ });
+
+ it('it should call .removeEventListener', function () {
+ expect(this.list.removeEventListener).toHaveBeenCalledWith('click', this.eventWrapper.clickEvent);
+ });
+ });
+});
diff --git a/spec/javascripts/droplab/hook.js b/spec/javascripts/droplab/hook.js
new file mode 100644
index 00000000000..8ebdcdd1404
--- /dev/null
+++ b/spec/javascripts/droplab/hook.js
@@ -0,0 +1,82 @@
+/* eslint-disable */
+
+import Hook from '~/droplab/hook';
+import * as dropdownSrc from '~/droplab/drop_down';
+
+describe('Hook', function () {
+ describe('class constructor', function () {
+ beforeEach(function () {
+ this.trigger = { id: 'id' };
+ this.list = {};
+ this.plugins = {};
+ this.config = {};
+ this.dropdown = {};
+
+ spyOn(dropdownSrc, 'default').and.returnValue(this.dropdown);
+
+ this.hook = new Hook(this.trigger, this.list, this.plugins, this.config);
+ });
+
+ it('should set .trigger', function () {
+ expect(this.hook.trigger).toBe(this.trigger);
+ });
+
+ it('should set .list', function () {
+ expect(this.hook.list).toBe(this.dropdown);
+ });
+
+ it('should call DropDown constructor', function () {
+ expect(dropdownSrc.default).toHaveBeenCalledWith(this.list);
+ });
+
+ it('should set .type', function () {
+ expect(this.hook.type).toBe('Hook');
+ });
+
+ it('should set .event', function () {
+ expect(this.hook.event).toBe('click');
+ });
+
+ it('should set .plugins', function () {
+ expect(this.hook.plugins).toBe(this.plugins);
+ });
+
+ it('should set .config', function () {
+ expect(this.hook.config).toBe(this.config);
+ });
+
+ it('should set .id', function () {
+ expect(this.hook.id).toBe(this.trigger.id);
+ });
+
+ describe('if config argument is undefined', function () {
+ beforeEach(function () {
+ this.config = undefined;
+
+ this.hook = new Hook(this.trigger, this.list, this.plugins, this.config);
+ });
+
+ it('should set .config to an empty object', function () {
+ expect(this.hook.config).toEqual({});
+ });
+ });
+
+ describe('if plugins argument is undefined', function () {
+ beforeEach(function () {
+ this.plugins = undefined;
+
+ this.hook = new Hook(this.trigger, this.list, this.plugins, this.config);
+ });
+
+ it('should set .plugins to an empty array', function () {
+ expect(this.hook.plugins).toEqual([]);
+ });
+ });
+ });
+
+ describe('addEvents', function () {
+ it('should exist', function () {
+ expect(Hook.prototype.hasOwnProperty('addEvents')).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/droplab/plugins/input_setter.js b/spec/javascripts/droplab/plugins/input_setter.js
new file mode 100644
index 00000000000..81bb70fa6e5
--- /dev/null
+++ b/spec/javascripts/droplab/plugins/input_setter.js
@@ -0,0 +1,177 @@
+/* eslint-disable */
+
+import InputSetter from '~/droplab/plugins/input_setter';
+
+describe('InputSetter', function () {
+ describe('init', function () {
+ beforeEach(function () {
+ this.config = { InputSetter: {} };
+ this.hook = { config: this.config };
+ this.inputSetter = jasmine.createSpyObj('inputSetter', ['addEvents']);
+
+ InputSetter.init.call(this.inputSetter, this.hook);
+ });
+
+ it('should set .hook', function () {
+ expect(this.inputSetter.hook).toBe(this.hook);
+ });
+
+ it('should set .config', function () {
+ expect(this.inputSetter.config).toBe(this.config.InputSetter);
+ });
+
+ it('should set .eventWrapper', function () {
+ expect(this.inputSetter.eventWrapper).toEqual({});
+ });
+
+ it('should call .addEvents', function () {
+ expect(this.inputSetter.addEvents).toHaveBeenCalled();
+ });
+
+ describe('if config.InputSetter is not set', function () {
+ beforeEach(function () {
+ this.config = { InputSetter: undefined };
+ this.hook = { config: this.config };
+
+ InputSetter.init.call(this.inputSetter, this.hook);
+ });
+
+ it('should set .config to an empty object', function () {
+ expect(this.inputSetter.config).toEqual({});
+ });
+
+ it('should set hook.config to an empty object', function () {
+ expect(this.hook.config.InputSetter).toEqual({});
+ });
+ })
+ });
+
+ describe('addEvents', function () {
+ beforeEach(function () {
+ this.hook = { list: { list: jasmine.createSpyObj('list', ['addEventListener']) } };
+ this.inputSetter = { eventWrapper: {}, hook: this.hook, setInputs: () => {} };
+
+ InputSetter.addEvents.call(this.inputSetter);
+ });
+
+ it('should set .eventWrapper.setInputs', function () {
+ expect(this.inputSetter.eventWrapper.setInputs).toEqual(jasmine.any(Function));
+ });
+
+ it('should call .addEventListener', function () {
+ expect(this.hook.list.list.addEventListener)
+ .toHaveBeenCalledWith('click.dl', this.inputSetter.eventWrapper.setInputs);
+ });
+ });
+
+ describe('removeEvents', function () {
+ beforeEach(function () {
+ this.hook = { list: { list: jasmine.createSpyObj('list', ['removeEventListener']) } };
+ this.eventWrapper = jasmine.createSpyObj('eventWrapper', ['setInputs']);
+ this.inputSetter = { eventWrapper: this.eventWrapper, hook: this.hook };
+
+ InputSetter.removeEvents.call(this.inputSetter);
+ });
+
+ it('should call .removeEventListener', function () {
+ expect(this.hook.list.list.removeEventListener)
+ .toHaveBeenCalledWith('click.dl', this.eventWrapper.setInputs);
+ });
+ });
+
+ describe('setInputs', function () {
+ beforeEach(function () {
+ this.event = { detail: { selected: {} } };
+ this.config = [0, 1];
+ this.inputSetter = { config: this.config, setInput: () => {} };
+
+ spyOn(this.inputSetter, 'setInput');
+
+ InputSetter.setInputs.call(this.inputSetter, this.event);
+ });
+
+ it('should call .setInput for each config element', function () {
+ const allArgs = this.inputSetter.setInput.calls.allArgs();
+
+ expect(allArgs.length).toEqual(2);
+
+ allArgs.forEach((args, i) => {
+ expect(args[0]).toBe(this.config[i]);
+ expect(args[1]).toBe(this.event.detail.selected);
+ });
+ });
+
+ describe('if config isnt an array', function () {
+ beforeEach(function () {
+ this.inputSetter = { config: {}, setInput: () => {} };
+
+ InputSetter.setInputs.call(this.inputSetter, this.event);
+ });
+
+ it('should set .config to an array with .config as the first element', function () {
+ expect(this.inputSetter.config).toEqual([{}]);
+ });
+ });
+ });
+
+ describe('setInput', function () {
+ beforeEach(function () {
+ this.selectedItem = { getAttribute: () => {} };
+ this.input = { value: 'oldValue', tagName: 'INPUT' };
+ this.config = { valueAttribute: {}, input: this.input };
+ this.inputSetter = { hook: { trigger: {} } };
+ this.newValue = 'newValue';
+
+ spyOn(this.selectedItem, 'getAttribute').and.returnValue(this.newValue);
+
+ InputSetter.setInput.call(this.inputSetter, this.config, this.selectedItem);
+ });
+
+ it('should call .getAttribute', function () {
+ expect(this.selectedItem.getAttribute).toHaveBeenCalledWith(this.config.valueAttribute);
+ });
+
+ it('should set the value of the input', function () {
+ expect(this.input.value).toBe(this.newValue);
+ })
+
+ describe('if no config.input is provided', function () {
+ beforeEach(function () {
+ this.config = { valueAttribute: {} };
+ this.trigger = { value: 'oldValue', tagName: 'INPUT' };
+ this.inputSetter = { hook: { trigger: this.trigger } };
+
+ InputSetter.setInput.call(this.inputSetter, this.config, this.selectedItem);
+ });
+
+ it('should set the value of the hook.trigger', function () {
+ expect(this.trigger.value).toBe(this.newValue);
+ });
+ });
+
+ describe('if the input tag is not INPUT', function () {
+ beforeEach(function () {
+ this.input = { textContent: 'oldValue', tagName: 'SPAN' };
+ this.config = { valueAttribute: {}, input: this.input };
+
+ InputSetter.setInput.call(this.inputSetter, this.config, this.selectedItem);
+ });
+
+ it('should set the textContent of the input', function () {
+ expect(this.config.input.textContent).toBe(this.newValue);
+ });
+ });
+ });
+
+ describe('destroy', function () {
+ beforeEach(function () {
+ this.inputSetter = jasmine.createSpyObj('inputSetter', ['removeEvents']);
+
+ InputSetter.destroy.call(this.inputSetter);
+ });
+
+ it('should call .removeEvents', function () {
+ expect(this.inputSetter.removeEvents).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js
index c16f77c53a2..2b1fe5e3eef 100644
--- a/spec/javascripts/filtered_search/dropdown_user_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_user_spec.js
@@ -33,7 +33,7 @@ require('~/filtered_search/dropdown_user');
});
});
- describe('config droplabAjaxFilter\'s endpoint', () => {
+ describe('config AjaxFilter\'s endpoint', () => {
beforeEach(() => {
spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
@@ -45,13 +45,13 @@ require('~/filtered_search/dropdown_user');
};
const dropdown = new gl.DropdownUser();
- expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json');
+ expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json');
});
it('should return endpoint when relative_url_root is undefined', () => {
const dropdown = new gl.DropdownUser();
- expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json');
+ expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json');
});
it('should return endpoint with relative url when available', () => {
@@ -60,7 +60,7 @@ require('~/filtered_search/dropdown_user');
};
const dropdown = new gl.DropdownUser();
- expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json');
+ expect(dropdown.config.AjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json');
});
afterEach(() => {
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index b300feaabe1..3f79eaf7afb 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -143,6 +143,7 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
expect(new_note.author).to eq(sent_notification.recipient)
expect(new_note.position).to eq(note.position)
expect(new_note.note).to include("I could not disagree more.")
+ expect(new_note.in_reply_to?(note)).to be_truthy
end
it "adds all attachments" do
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 6a89b007f96..e6f0a3b5920 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -63,7 +63,7 @@ describe Notify do
it 'contains a link to note author' do
is_expected.to have_html_escaped_body_text(issue.author_name)
- is_expected.to have_body_text 'wrote:'
+ is_expected.to have_body_text 'created an issue:'
end
end
end
@@ -215,7 +215,7 @@ describe Notify do
it 'contains a link to note author' do
is_expected.to have_html_escaped_body_text merge_request.author_name
- is_expected.to have_body_text 'wrote:'
+ is_expected.to have_body_text 'created a merge request:'
end
end
end
@@ -554,7 +554,7 @@ describe Notify do
end
it 'does not contain note author' do
- is_expected.not_to have_body_text 'wrote:'
+ is_expected.not_to have_body_text note.author_name
end
context 'when enabled email_author_in_body' do
@@ -564,7 +564,6 @@ describe Notify do
it 'contains a link to note author' do
is_expected.to have_html_escaped_body_text note.author_name
- is_expected.to have_body_text 'wrote:'
end
end
end
@@ -637,7 +636,7 @@ describe Notify do
end
end
- context 'items that are noteable, emails for a note on a diff' do
+ context 'items that are noteable, the email for a discussion note' do
let(:project) { create(:project, :repository) }
let(:note_author) { create(:user, name: 'author_name') }
@@ -645,8 +644,118 @@ describe Notify do
allow(Note).to receive(:find).with(note.id).and_return(note)
end
- shared_examples 'a note email on a diff' do |model|
- let(:note) { create(model, project: project, author: note_author) }
+ shared_examples 'a discussion note email' do |model|
+ it_behaves_like 'it should have Gmail Actions links'
+
+ it 'is sent to the given recipient as the author' do
+ sender = subject.header[:from].addrs[0]
+
+ aggregate_failures do
+ expect(sender.display_name).to eq(note_author.name)
+ expect(sender.address).to eq(gitlab_sender)
+ expect(subject).to deliver_to(recipient.notification_email)
+ end
+ end
+
+ it 'contains the message from the note' do
+ is_expected.to have_body_text note.note
+ end
+
+ it 'contains an introduction' do
+ is_expected.to have_body_text 'started a new discussion'
+ end
+
+ context 'when a comment on an existing discussion' do
+ let!(:second_note) { create(model, author: note_author, noteable: nil, in_reply_to: note) }
+
+ it 'contains an introduction' do
+ is_expected.to have_body_text 'commented on a'
+ end
+ end
+ end
+
+ describe 'on a commit' do
+ let(:commit) { project.commit }
+ let(:note) { create(:discussion_note_on_commit, commit_id: commit.id, project: project, author: note_author) }
+
+ before(:each) { allow(note).to receive(:noteable).and_return(commit) }
+
+ subject { Notify.note_commit_email(recipient.id, note.id) }
+
+ it_behaves_like 'a discussion note email', :discussion_note_on_commit
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { commit }
+ end
+ it_behaves_like 'it should show Gmail Actions View Commit link'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+
+ it 'has the correct subject' do
+ is_expected.to have_subject "Re: #{project.name} | #{commit.title.strip} (#{commit.short_id})"
+ end
+
+ it 'contains a link to the commit' do
+ is_expected.to have_body_text commit.short_id
+ end
+ end
+
+ describe 'on a merge request' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:note) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project, author: note_author) }
+ let(:note_on_merge_request_path) { namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: "note_#{note.id}") }
+ before(:each) { allow(note).to receive(:noteable).and_return(merge_request) }
+
+ subject { Notify.note_merge_request_email(recipient.id, note.id) }
+
+ it_behaves_like 'a discussion note email', :discussion_note_on_merge_request
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'an unsubscribeable thread'
+
+ it 'has the correct subject' do
+ is_expected.to have_referable_subject(merge_request, reply: true)
+ end
+
+ it 'contains a link to the merge request note' do
+ is_expected.to have_body_text note_on_merge_request_path
+ end
+ end
+
+ describe 'on an issue' do
+ let(:issue) { create(:issue, project: project) }
+ let(:note) { create(:discussion_note_on_issue, noteable: issue, project: project, author: note_author) }
+ let(:note_on_issue_path) { namespace_project_issue_path(project.namespace, project, issue, anchor: "note_#{note.id}") }
+ before(:each) { allow(note).to receive(:noteable).and_return(issue) }
+
+ subject { Notify.note_issue_email(recipient.id, note.id) }
+
+ it_behaves_like 'a discussion note email', :discussion_note_on_issue
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
+
+ it 'has the correct subject' do
+ is_expected.to have_referable_subject(issue, reply: true)
+ end
+
+ it 'contains a link to the issue note' do
+ is_expected.to have_body_text note_on_issue_path
+ end
+ end
+ end
+
+ context 'items that are noteable, the email for a diff discussion note' do
+ let(:note_author) { create(:user, name: 'author_name') }
+
+ before :each do
+ allow(Note).to receive(:find).with(note.id).and_return(note)
+ end
+
+ shared_examples 'an email for a note on a diff discussion' do |model|
+ let(:note) { create(model, author: note_author) }
it "includes diffs with character-level highlighting" do
is_expected.to have_body_text '<span class="p">}</span></span>'
@@ -672,18 +781,15 @@ describe Notify do
is_expected.to have_html_escaped_body_text note.note
end
- it 'does not contain note author' do
- is_expected.not_to have_body_text 'wrote:'
+ it 'contains an introduction' do
+ is_expected.to have_body_text 'started a new discussion on'
end
- context 'when enabled email_author_in_body' do
- before do
- stub_application_setting(email_author_in_body: true)
- end
+ context 'when a comment on an existing discussion' do
+ let!(:second_note) { create(model, author: note_author, noteable: nil, in_reply_to: note) }
- it 'contains a link to note author' do
- is_expected.to have_html_escaped_body_text note.author_name
- is_expected.to have_body_text 'wrote:'
+ it 'contains an introduction' do
+ is_expected.to have_body_text 'commented on a discussion on'
end
end
end
@@ -694,7 +800,7 @@ describe Notify do
subject { Notify.note_commit_email(recipient.id, note.id) }
- it_behaves_like 'a note email on a diff', :diff_note_on_commit
+ it_behaves_like 'an email for a note on a diff discussion', :diff_note_on_commit
it_behaves_like 'it should show Gmail Actions View Commit link'
it_behaves_like 'a user cannot unsubscribe through footer link'
end
@@ -705,7 +811,7 @@ describe Notify do
subject { Notify.note_merge_request_email(recipient.id, note.id) }
- it_behaves_like 'a note email on a diff', :diff_note_on_merge_request
+ it_behaves_like 'an email for a note on a diff discussion', :diff_note_on_merge_request
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
end
diff --git a/spec/mailers/previews/notify_preview.rb b/spec/mailers/previews/notify_preview.rb
index 0e1ccb5b847..580f0d56a92 100644
--- a/spec/mailers/previews/notify_preview.rb
+++ b/spec/mailers/previews/notify_preview.rb
@@ -1,4 +1,100 @@
class NotifyPreview < ActionMailer::Preview
+ def note_merge_request_email_for_individual_note
+ note_email(:note_merge_request_email) do
+ note = <<-MD.strip_heredoc
+ This is an individual note on a merge request :smiley:
+
+ In this notification email, we expect to see:
+
+ - The note contents (that's what you're looking at)
+ - A link to view this note on Gitlab
+ - An explanation for why the user is receiving this notification
+ MD
+
+ create_note(noteable_type: 'merge_request', noteable_id: merge_request.id, note: note)
+ end
+ end
+
+ def note_merge_request_email_for_discussion
+ note_email(:note_merge_request_email) do
+ note = <<-MD.strip_heredoc
+ This is a new discussion on a merge request :smiley:
+
+ In this notification email, we expect to see:
+
+ - A line saying who started this discussion
+ - The note contents (that's what you're looking at)
+ - A link to view this discussion on Gitlab
+ - An explanation for why the user is receiving this notification
+ MD
+
+ create_note(noteable_type: 'merge_request', noteable_id: merge_request.id, type: 'DiscussionNote', note: note)
+ end
+ end
+
+ def note_merge_request_email_for_diff_discussion
+ note_email(:note_merge_request_email) do
+ note = <<-MD.strip_heredoc
+ This is a new discussion on a merge request :smiley:
+
+ In this notification email, we expect to see:
+
+ - A line saying who started this discussion and on what file
+ - The diff
+ - The note contents (that's what you're looking at)
+ - A link to view this discussion on Gitlab
+ - An explanation for why the user is receiving this notification
+ MD
+
+ position = Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ diff_refs: merge_request.diff_refs
+ )
+
+ create_note(noteable_type: 'merge_request', noteable_id: merge_request.id, type: 'DiffNote', position: position, note: note)
+ end
+ end
+
+ private
+
+ def project
+ @project ||= Project.find_by_full_path('gitlab-org/gitlab-test')
+ end
+
+ def merge_request
+ @merge_request ||= project.merge_requests.find_by(source_branch: 'master', target_branch: 'feature')
+ end
+
+ def user
+ @user ||= User.last
+ end
+
+ def create_note(params)
+ Notes::CreateService.new(project, user, params).execute
+ end
+
+ def note_email(method)
+ cleanup do
+ note = yield
+
+ Notify.public_send(method, user.id, note)
+ end
+ end
+
+ def cleanup
+ email = nil
+
+ ActiveRecord::Base.transaction do
+ email = yield
+ raise ActiveRecord::Rollback
+ end
+
+ email
+ end
+
def pipeline_success_email
pipeline = Ci::Pipeline.last
Notify.pipeline_success_email(pipeline, pipeline.user.try(:email))
diff --git a/spec/models/concerns/discussion_on_diff_spec.rb b/spec/models/concerns/discussion_on_diff_spec.rb
new file mode 100644
index 00000000000..0002a00770f
--- /dev/null
+++ b/spec/models/concerns/discussion_on_diff_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe DiffDiscussion, DiscussionOnDiff, model: true do
+ subject { create(:diff_note_on_merge_request).to_discussion }
+
+ describe "#truncated_diff_lines" do
+ let(:truncated_lines) { subject.truncated_diff_lines }
+
+ context "when diff is greater than allowed number of truncated diff lines " do
+ it "returns fewer lines" do
+ expect(subject.diff_lines.count).to be > described_class::NUMBER_OF_TRUNCATED_DIFF_LINES
+
+ expect(truncated_lines.count).to be <= described_class::NUMBER_OF_TRUNCATED_DIFF_LINES
+ end
+ end
+
+ context "when some diff lines are meta" do
+ it "returns no meta lines" do
+ expect(subject.diff_lines).to include(be_meta)
+ expect(truncated_lines).not_to include(be_meta)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
new file mode 100644
index 00000000000..92cc8859a8c
--- /dev/null
+++ b/spec/models/concerns/noteable_spec.rb
@@ -0,0 +1,261 @@
+require 'spec_helper'
+
+describe MergeRequest, Noteable, model: true do
+ let!(:active_diff_note1) { create(:diff_note_on_merge_request) }
+ let(:project) { active_diff_note1.project }
+ subject { active_diff_note1.noteable }
+ let!(:active_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: subject, in_reply_to: active_diff_note1) }
+ let!(:active_diff_note3) { create(:diff_note_on_merge_request, project: project, noteable: subject, position: active_position2) }
+ let!(:outdated_diff_note1) { create(:diff_note_on_merge_request, project: project, noteable: subject, position: outdated_position) }
+ let!(:outdated_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: subject, in_reply_to: outdated_diff_note1) }
+ let!(:discussion_note1) { create(:discussion_note_on_merge_request, project: project, noteable: subject) }
+ let!(:discussion_note2) { create(:discussion_note_on_merge_request, in_reply_to: discussion_note1) }
+ let!(:commit_diff_note1) { create(:diff_note_on_commit, project: project) }
+ let!(:commit_diff_note2) { create(:diff_note_on_commit, project: project, in_reply_to: commit_diff_note1) }
+ let!(:commit_note1) { create(:note_on_commit, project: project) }
+ let!(:commit_note2) { create(:note_on_commit, project: project) }
+ let!(:commit_discussion_note1) { create(:discussion_note_on_commit, project: project) }
+ let!(:commit_discussion_note2) { create(:discussion_note_on_commit, in_reply_to: commit_discussion_note1) }
+ let!(:commit_discussion_note3) { create(:discussion_note_on_commit, project: project) }
+ let!(:note1) { create(:note, project: project, noteable: subject) }
+ let!(:note2) { create(:note, project: project, noteable: subject) }
+
+ let(:active_position2) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: 16,
+ new_line: 22,
+ diff_refs: subject.diff_refs
+ )
+ end
+
+ let(:outdated_position) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 9,
+ diff_refs: project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e").diff_refs
+ )
+ end
+
+ describe '#discussions' do
+ let(:discussions) { subject.discussions }
+
+ it 'includes discussions for diff notes, commit diff notes, commit notes, and regular notes' do
+ expect(discussions).to eq([
+ DiffDiscussion.new([active_diff_note1, active_diff_note2], subject),
+ DiffDiscussion.new([active_diff_note3], subject),
+ DiffDiscussion.new([outdated_diff_note1, outdated_diff_note2], subject),
+ Discussion.new([discussion_note1, discussion_note2], subject),
+ DiffDiscussion.new([commit_diff_note1, commit_diff_note2], subject),
+ OutOfContextDiscussion.new([commit_note1, commit_note2], subject),
+ Discussion.new([commit_discussion_note1, commit_discussion_note2], subject),
+ Discussion.new([commit_discussion_note3], subject),
+ IndividualNoteDiscussion.new([note1], subject),
+ IndividualNoteDiscussion.new([note2], subject)
+ ])
+ end
+ end
+
+ describe '#grouped_diff_discussions' do
+ let(:grouped_diff_discussions) { subject.grouped_diff_discussions }
+
+ it "includes active discussions" do
+ discussions = grouped_diff_discussions.values.flatten
+
+ expect(discussions.count).to eq(2)
+ expect(discussions.map(&:id)).to eq([active_diff_note1.discussion_id, active_diff_note3.discussion_id])
+ expect(discussions.all?(&:active?)).to be true
+
+ expect(discussions.first.notes).to eq([active_diff_note1, active_diff_note2])
+ expect(discussions.last.notes).to eq([active_diff_note3])
+ end
+
+ it "doesn't include outdated discussions" do
+ expect(grouped_diff_discussions.values.flatten.map(&:id)).not_to include(outdated_diff_note1.discussion_id)
+ end
+
+ it "groups the discussions by line code" do
+ expect(grouped_diff_discussions[active_diff_note1.line_code].first.id).to eq(active_diff_note1.discussion_id)
+ expect(grouped_diff_discussions[active_diff_note3.line_code].first.id).to eq(active_diff_note3.discussion_id)
+ end
+ end
+
+ context "discussion status" do
+ let(:first_discussion) { build_stubbed(:discussion_note_on_merge_request, noteable: subject, project: project).to_discussion }
+ let(:second_discussion) { build_stubbed(:discussion_note_on_merge_request, noteable: subject, project: project).to_discussion }
+ let(:third_discussion) { build_stubbed(:discussion_note_on_merge_request, noteable: subject, project: project).to_discussion }
+
+ before do
+ allow(subject).to receive(:resolvable_discussions).and_return([first_discussion, second_discussion, third_discussion])
+ end
+
+ describe "#discussions_resolvable?" do
+ context "when all discussions are unresolvable" do
+ before do
+ allow(first_discussion).to receive(:resolvable?).and_return(false)
+ allow(second_discussion).to receive(:resolvable?).and_return(false)
+ allow(third_discussion).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.discussions_resolvable?).to be false
+ end
+ end
+
+ context "when some discussions are unresolvable and some discussions are resolvable" do
+ before do
+ allow(first_discussion).to receive(:resolvable?).and_return(true)
+ allow(second_discussion).to receive(:resolvable?).and_return(false)
+ allow(third_discussion).to receive(:resolvable?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.discussions_resolvable?).to be true
+ end
+ end
+
+ context "when all discussions are resolvable" do
+ before do
+ allow(first_discussion).to receive(:resolvable?).and_return(true)
+ allow(second_discussion).to receive(:resolvable?).and_return(true)
+ allow(third_discussion).to receive(:resolvable?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.discussions_resolvable?).to be true
+ end
+ end
+ end
+
+ describe "#discussions_resolved?" do
+ context "when discussions are not resolvable" do
+ before do
+ allow(subject).to receive(:discussions_resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.discussions_resolved?).to be false
+ end
+ end
+
+ context "when discussions are resolvable" do
+ before do
+ allow(subject).to receive(:discussions_resolvable?).and_return(true)
+
+ allow(first_discussion).to receive(:resolvable?).and_return(true)
+ allow(second_discussion).to receive(:resolvable?).and_return(false)
+ allow(third_discussion).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable discussions are resolved" do
+ before do
+ allow(first_discussion).to receive(:resolved?).and_return(true)
+ allow(third_discussion).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.discussions_resolved?).to be true
+ end
+ end
+
+ context "when some resolvable discussions are not resolved" do
+ before do
+ allow(first_discussion).to receive(:resolved?).and_return(true)
+ allow(third_discussion).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.discussions_resolved?).to be false
+ end
+ end
+ end
+ end
+
+ describe "#discussions_to_be_resolved?" do
+ context "when discussions are not resolvable" do
+ before do
+ allow(subject).to receive(:discussions_resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.discussions_to_be_resolved?).to be false
+ end
+ end
+
+ context "when discussions are resolvable" do
+ before do
+ allow(subject).to receive(:discussions_resolvable?).and_return(true)
+
+ allow(first_discussion).to receive(:resolvable?).and_return(true)
+ allow(second_discussion).to receive(:resolvable?).and_return(false)
+ allow(third_discussion).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable discussions are resolved" do
+ before do
+ allow(first_discussion).to receive(:resolved?).and_return(true)
+ allow(third_discussion).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns false" do
+ expect(subject.discussions_to_be_resolved?).to be false
+ end
+ end
+
+ context "when some resolvable discussions are not resolved" do
+ before do
+ allow(first_discussion).to receive(:resolved?).and_return(true)
+ allow(third_discussion).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns true" do
+ expect(subject.discussions_to_be_resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#discussions_to_be_resolved" do
+ before do
+ allow(first_discussion).to receive(:to_be_resolved?).and_return(true)
+ allow(second_discussion).to receive(:to_be_resolved?).and_return(false)
+ allow(third_discussion).to receive(:to_be_resolved?).and_return(false)
+ end
+
+ it 'includes only discussions that need to be resolved' do
+ expect(subject.discussions_to_be_resolved).to eq([first_discussion])
+ end
+ end
+
+ describe '#discussions_can_be_resolved_by?' do
+ let(:user) { build(:user) }
+
+ context 'all discussions can be resolved by the user' do
+ before do
+ allow(first_discussion).to receive(:can_resolve?).with(user).and_return(true)
+ allow(second_discussion).to receive(:can_resolve?).with(user).and_return(true)
+ allow(third_discussion).to receive(:can_resolve?).with(user).and_return(true)
+ end
+
+ it 'allows a user to resolve the discussions' do
+ expect(subject.discussions_can_be_resolved_by?(user)).to be(true)
+ end
+ end
+
+ context 'one discussion cannot be resolved by the user' do
+ before do
+ allow(first_discussion).to receive(:can_resolve?).with(user).and_return(true)
+ allow(second_discussion).to receive(:can_resolve?).with(user).and_return(true)
+ allow(third_discussion).to receive(:can_resolve?).with(user).and_return(false)
+ end
+
+ it 'allows a user to resolve the discussions' do
+ expect(subject.discussions_can_be_resolved_by?(user)).to be(false)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/resolvable_discussion_spec.rb b/spec/models/concerns/resolvable_discussion_spec.rb
new file mode 100644
index 00000000000..18327fe262d
--- /dev/null
+++ b/spec/models/concerns/resolvable_discussion_spec.rb
@@ -0,0 +1,548 @@
+require 'spec_helper'
+
+describe Discussion, ResolvableDiscussion, models: true do
+ subject { described_class.new([first_note, second_note, third_note]) }
+
+ let(:first_note) { create(:discussion_note_on_merge_request) }
+ let(:merge_request) { first_note.noteable }
+ let(:project) { first_note.project }
+ let(:second_note) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project, in_reply_to: first_note) }
+ let(:third_note) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
+
+ describe "#resolvable?" do
+ context "when potentially resolvable" do
+ before do
+ allow(subject).to receive(:potentially_resolvable?).and_return(true)
+ end
+
+ context "when all notes are unresolvable" do
+ before do
+ allow(first_note).to receive(:resolvable?).and_return(false)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+
+ context "when some notes are unresolvable and some notes are resolvable" do
+ before do
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.resolvable?).to be true
+ end
+ end
+
+ context "when all notes are resolvable" do
+ before do
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(true)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.resolvable?).to be true
+ end
+ end
+ end
+
+ context "when not potentially resolvable" do
+ before do
+ allow(subject).to receive(:potentially_resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+ end
+
+ describe "#resolved?" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable notes are resolved" do
+ before do
+ allow(first_note).to receive(:resolved?).and_return(true)
+ allow(third_note).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.resolved?).to be true
+ end
+ end
+
+ context "when some resolvable notes are not resolved" do
+ before do
+ allow(first_note).to receive(:resolved?).and_return(true)
+ allow(third_note).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolved?).to be false
+ end
+ end
+ end
+ end
+
+ describe "#to_be_resolved?" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable notes are resolved" do
+ before do
+ allow(first_note).to receive(:resolved?).and_return(true)
+ allow(third_note).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when some resolvable notes are not resolved" do
+ before do
+ allow(first_note).to receive(:resolved?).and_return(true)
+ allow(third_note).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns true" do
+ expect(subject.to_be_resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#can_resolve?" do
+ let(:current_user) { create(:user) }
+
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.can_resolve?(current_user)).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when not signed in" do
+ let(:current_user) { nil }
+
+ it "returns false" do
+ expect(subject.can_resolve?(current_user)).to be false
+ end
+ end
+
+ context "when signed in" do
+ context "when the signed in user is the noteable author" do
+ before do
+ subject.noteable.author = current_user
+ end
+
+ it "returns true" do
+ expect(subject.can_resolve?(current_user)).to be true
+ end
+ end
+
+ context "when the signed in user can push to the project" do
+ before do
+ subject.project.team << [current_user, :master]
+ end
+
+ it "returns true" do
+ expect(subject.can_resolve?(current_user)).to be true
+ end
+ end
+
+ context "when the signed in user is a random user" do
+ it "returns false" do
+ expect(subject.can_resolve?(current_user)).to be false
+ end
+ end
+ end
+ end
+ end
+
+ describe "#resolve!" do
+ let(:current_user) { create(:user) }
+
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.resolve!(current_user)).to be_nil
+ end
+
+ it "doesn't set resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "doesn't set resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "doesn't mark as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ let(:user) { create(:user) }
+ let(:second_note) { create(:diff_note_on_commit) } # unresolvable
+
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable notes are resolved" do
+ before do
+ first_note.resolve!(user)
+ third_note.resolve!(user)
+
+ first_note.reload
+ third_note.reload
+ end
+
+ it "doesn't change resolved_at on the resolved notes" do
+ expect(first_note.resolved_at).not_to be_nil
+ expect(third_note.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_at }
+ expect { subject.resolve!(current_user) }.not_to change { third_note.resolved_at }
+ end
+
+ it "doesn't change resolved_by on the resolved notes" do
+ expect(first_note.resolved_by).to eq(user)
+ expect(third_note.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_by }
+ expect { subject.resolve!(current_user) }.not_to change { third_note.resolved_by }
+ end
+
+ it "doesn't change the resolved state on the resolved notes" do
+ expect(first_note.resolved?).to be true
+ expect(third_note.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved? }
+ expect { subject.resolve!(current_user) }.not_to change { third_note.resolved? }
+ end
+
+ it "doesn't change resolved_at" do
+ expect(subject.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_at }
+ end
+
+ it "doesn't change resolved_by" do
+ expect(subject.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_by }
+ end
+
+ it "doesn't change resolved state" do
+ expect(subject.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved? }
+ end
+ end
+
+ context "when some resolvable notes are resolved" do
+ before do
+ first_note.resolve!(user)
+ end
+
+ it "doesn't change resolved_at on the resolved note" do
+ expect(first_note.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.
+ not_to change { first_note.reload.resolved_at }
+ end
+
+ it "doesn't change resolved_by on the resolved note" do
+ expect(first_note.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.
+ not_to change { first_note.reload && first_note.resolved_by }
+ end
+
+ it "doesn't change the resolved state on the resolved note" do
+ expect(first_note.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.
+ not_to change { first_note.reload && first_note.resolved? }
+ end
+
+ it "sets resolved_at on the unresolved note" do
+ subject.resolve!(current_user)
+ third_note.reload
+
+ expect(third_note.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by on the unresolved note" do
+ subject.resolve!(current_user)
+ third_note.reload
+
+ expect(third_note.resolved_by).to eq(current_user)
+ end
+
+ it "marks the unresolved note as resolved" do
+ subject.resolve!(current_user)
+ third_note.reload
+
+ expect(third_note.resolved?).to be true
+ end
+
+ it "sets resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to eq(current_user)
+ end
+
+ it "marks as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be true
+ end
+ end
+
+ context "when no resolvable notes are resolved" do
+ it "sets resolved_at on the unresolved notes" do
+ subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
+
+ expect(first_note.resolved_at).not_to be_nil
+ expect(third_note.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by on the unresolved notes" do
+ subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
+
+ expect(first_note.resolved_by).to eq(current_user)
+ expect(third_note.resolved_by).to eq(current_user)
+ end
+
+ it "marks the unresolved notes as resolved" do
+ subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
+
+ expect(first_note.resolved?).to be true
+ expect(third_note.resolved?).to be true
+ end
+
+ it "sets resolved_at" do
+ subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
+
+ expect(subject.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by" do
+ subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
+
+ expect(subject.resolved_by).to eq(current_user)
+ end
+
+ it "marks as resolved" do
+ subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
+
+ expect(subject.resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#unresolve!" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.unresolve!).to be_nil
+ end
+ end
+
+ context "when resolvable" do
+ let(:user) { create(:user) }
+
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable notes are resolved" do
+ before do
+ first_note.resolve!(user)
+ third_note.resolve!(user)
+ end
+
+ it "unsets resolved_at on the resolved notes" do
+ subject.unresolve!
+ first_note.reload
+ third_note.reload
+
+ expect(first_note.resolved_at).to be_nil
+ expect(third_note.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by on the resolved notes" do
+ subject.unresolve!
+ first_note.reload
+ third_note.reload
+
+ expect(first_note.resolved_by).to be_nil
+ expect(third_note.resolved_by).to be_nil
+ end
+
+ it "unmarks the resolved notes as resolved" do
+ subject.unresolve!
+ first_note.reload
+ third_note.reload
+
+ expect(first_note.resolved?).to be false
+ expect(third_note.resolved?).to be false
+ end
+
+ it "unsets resolved_at" do
+ subject.unresolve!
+ first_note.reload
+ third_note.reload
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by" do
+ subject.unresolve!
+ first_note.reload
+ third_note.reload
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "unmarks as resolved" do
+ subject.unresolve!
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when some resolvable notes are resolved" do
+ before do
+ first_note.resolve!(user)
+ end
+
+ it "unsets resolved_at on the resolved note" do
+ subject.unresolve!
+
+ expect(subject.first_note.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by on the resolved note" do
+ subject.unresolve!
+
+ expect(subject.first_note.resolved_by).to be_nil
+ end
+
+ it "unmarks the resolved note as resolved" do
+ subject.unresolve!
+
+ expect(subject.first_note.resolved?).to be false
+ end
+ end
+ end
+ end
+
+ describe "#first_note_to_resolve" do
+ it "returns the first note that still needs to be resolved" do
+ allow(first_note).to receive(:to_be_resolved?).and_return(false)
+ allow(second_note).to receive(:to_be_resolved?).and_return(true)
+
+ expect(subject.first_note_to_resolve).to eq(second_note)
+ end
+ end
+
+ describe "#last_resolved_note" do
+ let(:current_user) { create(:user) }
+
+ before do
+ first_note.resolve!(current_user)
+ third_note.resolve!(current_user)
+ second_note.resolve!(current_user)
+ end
+
+ it "returns the last note that was resolved" do
+ expect(subject.last_resolved_note).to eq(second_note)
+ end
+ end
+end
diff --git a/spec/models/concerns/resolvable_note_spec.rb b/spec/models/concerns/resolvable_note_spec.rb
new file mode 100644
index 00000000000..1503ccdff11
--- /dev/null
+++ b/spec/models/concerns/resolvable_note_spec.rb
@@ -0,0 +1,329 @@
+require 'spec_helper'
+
+describe Note, ResolvableNote, models: true do
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ subject { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
+
+ context 'resolvability scopes' do
+ let!(:note1) { create(:note, project: project) }
+ let!(:note2) { create(:diff_note_on_commit, project: project) }
+ let!(:note3) { create(:diff_note_on_merge_request, :resolved, noteable: merge_request, project: project) }
+ let!(:note4) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
+ let!(:note5) { create(:discussion_note_on_issue, project: project) }
+ let!(:note6) { create(:discussion_note_on_merge_request, :system, noteable: merge_request, project: project) }
+
+ describe '.potentially_resolvable' do
+ it 'includes diff and discussion notes on merge requests' do
+ expect(Note.potentially_resolvable).to match_array([note3, note4, note6])
+ end
+ end
+
+ describe '.resolvable' do
+ it 'includes non-system diff and discussion notes on merge requests' do
+ expect(Note.resolvable).to match_array([note3, note4])
+ end
+ end
+
+ describe '.resolved' do
+ it 'includes resolved non-system diff and discussion notes on merge requests' do
+ expect(Note.resolved).to match_array([note3])
+ end
+ end
+
+ describe '.unresolved' do
+ it 'includes non-resolved non-system diff and discussion notes on merge requests' do
+ expect(Note.unresolved).to match_array([note4])
+ end
+ end
+ end
+
+ describe ".resolve!" do
+ let(:current_user) { create(:user) }
+ let!(:commit_note) { create(:diff_note_on_commit, project: project) }
+ let!(:resolved_note) { create(:discussion_note_on_merge_request, :resolved, noteable: merge_request, project: project) }
+ let!(:unresolved_note) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
+
+ before do
+ described_class.resolve!(current_user)
+
+ commit_note.reload
+ resolved_note.reload
+ unresolved_note.reload
+ end
+
+ it 'resolves only the resolvable, not yet resolved notes' do
+ expect(commit_note.resolved_at).to be_nil
+ expect(resolved_note.resolved_by).not_to eq(current_user)
+ expect(unresolved_note.resolved_at).not_to be_nil
+ expect(unresolved_note.resolved_by).to eq(current_user)
+ end
+ end
+
+ describe ".unresolve!" do
+ let!(:resolved_note) { create(:discussion_note_on_merge_request, :resolved, noteable: merge_request, project: project) }
+
+ before do
+ described_class.unresolve!
+
+ resolved_note.reload
+ end
+
+ it 'unresolves the resolved notes' do
+ expect(resolved_note.resolved_by).to be_nil
+ expect(resolved_note.resolved_at).to be_nil
+ end
+ end
+
+ describe '#resolvable?' do
+ context "when potentially resolvable" do
+ before do
+ allow(subject).to receive(:potentially_resolvable?).and_return(true)
+ end
+
+ context "when a system note" do
+ before do
+ subject.system = true
+ end
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+
+ context "when a regular note" do
+ it "returns true" do
+ expect(subject.resolvable?).to be true
+ end
+ end
+ end
+
+ context "when not potentially resolvable" do
+ before do
+ allow(subject).to receive(:potentially_resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+ end
+
+ describe "#to_be_resolved?" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when resolved" do
+ before do
+ allow(subject).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when not resolved" do
+ before do
+ allow(subject).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns true" do
+ expect(subject.to_be_resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#resolved?" do
+ let(:current_user) { create(:user) }
+
+ context 'when not resolvable' do
+ before do
+ subject.resolve!(current_user)
+
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(subject.resolved?).to be_falsey
+ end
+ end
+
+ context 'when resolvable' do
+ context 'when the note has been resolved' do
+ before do
+ subject.resolve!(current_user)
+ end
+
+ it 'returns true' do
+ expect(subject.resolved?).to be_truthy
+ end
+ end
+
+ context 'when the note has not been resolved' do
+ it 'returns false' do
+ expect(subject.resolved?).to be_falsey
+ end
+ end
+ end
+ end
+
+ describe "#resolve!" do
+ let(:current_user) { create(:user) }
+
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.resolve!(current_user)).to be_nil
+ end
+
+ it "doesn't set resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "doesn't set resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "doesn't mark as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when already resolved" do
+ let(:user) { create(:user) }
+
+ before do
+ subject.resolve!(user)
+ end
+
+ it "returns nil" do
+ expect(subject.resolve!(current_user)).to be_nil
+ end
+
+ it "doesn't change resolved_at" do
+ expect(subject.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_at }
+ end
+
+ it "doesn't change resolved_by" do
+ expect(subject.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_by }
+ end
+
+ it "doesn't change resolved status" do
+ expect(subject.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved? }
+ end
+ end
+
+ context "when not yet resolved" do
+ it "returns true" do
+ expect(subject.resolve!(current_user)).to be true
+ end
+
+ it "sets resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to eq(current_user)
+ end
+
+ it "marks as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#unresolve!" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.unresolve!).to be_nil
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when resolved" do
+ let(:user) { create(:user) }
+
+ before do
+ subject.resolve!(user)
+ end
+
+ it "returns true" do
+ expect(subject.unresolve!).to be true
+ end
+
+ it "unsets resolved_at" do
+ subject.unresolve!
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by" do
+ subject.unresolve!
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "unmarks as resolved" do
+ subject.unresolve!
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when not resolved" do
+ it "returns nil" do
+ expect(subject.unresolve!).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/diff_discussion_spec.rb b/spec/models/diff_discussion_spec.rb
new file mode 100644
index 00000000000..48e7c0a822c
--- /dev/null
+++ b/spec/models/diff_discussion_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe DiffDiscussion, model: true do
+ subject { described_class.new([first_note, second_note, third_note]) }
+
+ let(:first_note) { create(:diff_note_on_merge_request) }
+ let(:merge_request) { first_note.noteable }
+ let(:project) { first_note.project }
+ let(:second_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, in_reply_to: first_note) }
+ let(:third_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, in_reply_to: first_note) }
+
+ describe '#reply_attributes' do
+ it 'includes position and original_position' do
+ attributes = subject.reply_attributes
+ expect(attributes[:position]).to eq(first_note.position.to_json)
+ expect(attributes[:original_position]).to eq(first_note.original_position.to_json)
+ end
+ end
+end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 9ea3a4b7020..fb80b74b226 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -31,43 +31,6 @@ describe DiffNote, models: true do
subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
- describe ".resolve!" do
- let(:current_user) { create(:user) }
- let!(:commit_note) { create(:diff_note_on_commit) }
- let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) }
- let!(:unresolved_note) { create(:diff_note_on_merge_request) }
-
- before do
- described_class.resolve!(current_user)
-
- commit_note.reload
- resolved_note.reload
- unresolved_note.reload
- end
-
- it 'resolves only the resolvable, not yet resolved notes' do
- expect(commit_note.resolved_at).to be_nil
- expect(resolved_note.resolved_by).not_to eq(current_user)
- expect(unresolved_note.resolved_at).not_to be_nil
- expect(unresolved_note.resolved_by).to eq(current_user)
- end
- end
-
- describe ".unresolve!" do
- let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) }
-
- before do
- described_class.unresolve!
-
- resolved_note.reload
- end
-
- it 'unresolves the resolved notes' do
- expect(resolved_note.resolved_by).to be_nil
- expect(resolved_note.resolved_at).to be_nil
- end
- end
-
describe "#position=" do
context "when provided a string" do
it "sets the position" do
@@ -94,6 +57,32 @@ describe DiffNote, models: true do
end
end
+ describe "#original_position=" do
+ context "when provided a string" do
+ it "sets the original position" do
+ subject.original_position = new_position.to_json
+
+ expect(subject.original_position).to eq(new_position)
+ end
+ end
+
+ context "when provided a hash" do
+ it "sets the original position" do
+ subject.original_position = new_position.to_h
+
+ expect(subject.original_position).to eq(new_position)
+ end
+ end
+
+ context "when provided a position object" do
+ it "sets the original position" do
+ subject.original_position = new_position
+
+ expect(subject.original_position).to eq(new_position)
+ end
+ end
+ end
+
describe "#diff_file" do
it "returns the correct diff file" do
diff_file = subject.diff_file
@@ -226,252 +215,6 @@ describe DiffNote, models: true do
end
end
- describe "#resolvable?" do
- context "when noteable is a commit" do
- subject { create(:diff_note_on_commit, project: project, position: position) }
-
- it "returns false" do
- expect(subject.resolvable?).to be false
- end
- end
-
- context "when noteable is a merge request" do
- context "when a system note" do
- before do
- subject.system = true
- end
-
- it "returns false" do
- expect(subject.resolvable?).to be false
- end
- end
-
- context "when a regular note" do
- it "returns true" do
- expect(subject.resolvable?).to be true
- end
- end
- end
- end
-
- describe "#to_be_resolved?" do
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.to_be_resolved?).to be false
- end
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
- end
-
- context "when resolved" do
- before do
- allow(subject).to receive(:resolved?).and_return(true)
- end
-
- it "returns false" do
- expect(subject.to_be_resolved?).to be false
- end
- end
-
- context "when not resolved" do
- before do
- allow(subject).to receive(:resolved?).and_return(false)
- end
-
- it "returns true" do
- expect(subject.to_be_resolved?).to be true
- end
- end
- end
- end
-
- describe "#resolve!" do
- let(:current_user) { create(:user) }
-
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns nil" do
- expect(subject.resolve!(current_user)).to be_nil
- end
-
- it "doesn't set resolved_at" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_at).to be_nil
- end
-
- it "doesn't set resolved_by" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_by).to be_nil
- end
-
- it "doesn't mark as resolved" do
- subject.resolve!(current_user)
-
- expect(subject.resolved?).to be false
- end
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
- end
-
- context "when already resolved" do
- let(:user) { create(:user) }
-
- before do
- subject.resolve!(user)
- end
-
- it "returns nil" do
- expect(subject.resolve!(current_user)).to be_nil
- end
-
- it "doesn't change resolved_at" do
- expect(subject.resolved_at).not_to be_nil
-
- expect { subject.resolve!(current_user) }.not_to change { subject.resolved_at }
- end
-
- it "doesn't change resolved_by" do
- expect(subject.resolved_by).to eq(user)
-
- expect { subject.resolve!(current_user) }.not_to change { subject.resolved_by }
- end
-
- it "doesn't change resolved status" do
- expect(subject.resolved?).to be true
-
- expect { subject.resolve!(current_user) }.not_to change { subject.resolved? }
- end
- end
-
- context "when not yet resolved" do
- it "returns true" do
- expect(subject.resolve!(current_user)).to be true
- end
-
- it "sets resolved_at" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_at).not_to be_nil
- end
-
- it "sets resolved_by" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_by).to eq(current_user)
- end
-
- it "marks as resolved" do
- subject.resolve!(current_user)
-
- expect(subject.resolved?).to be true
- end
- end
- end
- end
-
- describe "#unresolve!" do
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns nil" do
- expect(subject.unresolve!).to be_nil
- end
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
- end
-
- context "when resolved" do
- let(:user) { create(:user) }
-
- before do
- subject.resolve!(user)
- end
-
- it "returns true" do
- expect(subject.unresolve!).to be true
- end
-
- it "unsets resolved_at" do
- subject.unresolve!
-
- expect(subject.resolved_at).to be_nil
- end
-
- it "unsets resolved_by" do
- subject.unresolve!
-
- expect(subject.resolved_by).to be_nil
- end
-
- it "unmarks as resolved" do
- subject.unresolve!
-
- expect(subject.resolved?).to be false
- end
- end
-
- context "when not resolved" do
- it "returns nil" do
- expect(subject.unresolve!).to be_nil
- end
- end
- end
- end
-
- describe "#discussion" do
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns nil" do
- expect(subject.discussion).to be_nil
- end
- end
-
- context "when resolvable" do
- let!(:diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: subject.position) }
- let!(:diff_note3) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: active_position2) }
-
- let(:active_position2) do
- Gitlab::Diff::Position.new(
- old_path: "files/ruby/popen.rb",
- new_path: "files/ruby/popen.rb",
- old_line: 16,
- new_line: 22,
- diff_refs: merge_request.diff_refs
- )
- end
-
- it "returns the discussion this note is in" do
- discussion = subject.discussion
-
- expect(discussion.id).to eq(subject.discussion_id)
- expect(discussion.notes).to eq([subject, diff_note2])
- end
- end
- end
-
describe "#discussion_id" do
let(:note) { create(:diff_note_on_merge_request) }
@@ -496,29 +239,4 @@ describe DiffNote, models: true do
end
end
end
-
- describe "#original_discussion_id" do
- let(:note) { create(:diff_note_on_merge_request) }
-
- context "when it is newly created" do
- it "has a discussion id" do
- expect(note.original_discussion_id).not_to be_nil
- expect(note.original_discussion_id).to match(/\A\h{40}\z/)
- end
- end
-
- context "when it didn't store a discussion id before" do
- before do
- note.update_column(:original_discussion_id, nil)
- end
-
- it "has a discussion id" do
- # The original_discussion_id is set in `after_initialize`, so `reload` won't work
- reloaded_note = Note.find(note.id)
-
- expect(reloaded_note.original_discussion_id).not_to be_nil
- expect(reloaded_note.original_discussion_id).to match(/\A\h{40}\z/)
- end
- end
- end
end
diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb
index bc32fadd391..0221e23ced8 100644
--- a/spec/models/discussion_spec.rb
+++ b/spec/models/discussion_spec.rb
@@ -4,618 +4,27 @@ describe Discussion, model: true do
subject { described_class.new([first_note, second_note, third_note]) }
let(:first_note) { create(:diff_note_on_merge_request) }
- let(:second_note) { create(:diff_note_on_merge_request) }
+ let(:merge_request) { first_note.noteable }
+ let(:second_note) { create(:diff_note_on_merge_request, in_reply_to: first_note) }
let(:third_note) { create(:diff_note_on_merge_request) }
- describe "#resolvable?" do
- context "when a diff discussion" do
- before do
- allow(subject).to receive(:diff_discussion?).and_return(true)
- end
-
- context "when all notes are unresolvable" do
- before do
- allow(first_note).to receive(:resolvable?).and_return(false)
- allow(second_note).to receive(:resolvable?).and_return(false)
- allow(third_note).to receive(:resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.resolvable?).to be false
- end
- end
-
- context "when some notes are unresolvable and some notes are resolvable" do
- before do
- allow(first_note).to receive(:resolvable?).and_return(true)
- allow(second_note).to receive(:resolvable?).and_return(false)
- allow(third_note).to receive(:resolvable?).and_return(true)
- end
-
- it "returns true" do
- expect(subject.resolvable?).to be true
- end
- end
-
- context "when all notes are resolvable" do
- before do
- allow(first_note).to receive(:resolvable?).and_return(true)
- allow(second_note).to receive(:resolvable?).and_return(true)
- allow(third_note).to receive(:resolvable?).and_return(true)
- end
-
- it "returns true" do
- expect(subject.resolvable?).to be true
- end
- end
- end
-
- context "when not a diff discussion" do
- before do
- allow(subject).to receive(:diff_discussion?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.resolvable?).to be false
- end
- end
- end
-
- describe "#resolved?" do
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.resolved?).to be false
- end
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
-
- allow(first_note).to receive(:resolvable?).and_return(true)
- allow(second_note).to receive(:resolvable?).and_return(false)
- allow(third_note).to receive(:resolvable?).and_return(true)
- end
-
- context "when all resolvable notes are resolved" do
- before do
- allow(first_note).to receive(:resolved?).and_return(true)
- allow(third_note).to receive(:resolved?).and_return(true)
- end
-
- it "returns true" do
- expect(subject.resolved?).to be true
- end
- end
-
- context "when some resolvable notes are not resolved" do
- before do
- allow(first_note).to receive(:resolved?).and_return(true)
- allow(third_note).to receive(:resolved?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.resolved?).to be false
- end
- end
- end
- end
-
- describe "#to_be_resolved?" do
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.to_be_resolved?).to be false
- end
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
-
- allow(first_note).to receive(:resolvable?).and_return(true)
- allow(second_note).to receive(:resolvable?).and_return(false)
- allow(third_note).to receive(:resolvable?).and_return(true)
- end
-
- context "when all resolvable notes are resolved" do
- before do
- allow(first_note).to receive(:resolved?).and_return(true)
- allow(third_note).to receive(:resolved?).and_return(true)
- end
-
- it "returns false" do
- expect(subject.to_be_resolved?).to be false
- end
- end
-
- context "when some resolvable notes are not resolved" do
- before do
- allow(first_note).to receive(:resolved?).and_return(true)
- allow(third_note).to receive(:resolved?).and_return(false)
- end
-
- it "returns true" do
- expect(subject.to_be_resolved?).to be true
- end
- end
- end
- end
-
- describe "#can_resolve?" do
- let(:current_user) { create(:user) }
-
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.can_resolve?(current_user)).to be false
- end
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
- end
-
- context "when not signed in" do
- let(:current_user) { nil }
-
- it "returns false" do
- expect(subject.can_resolve?(current_user)).to be false
- end
- end
-
- context "when signed in" do
- context "when the signed in user is the noteable author" do
- before do
- subject.noteable.author = current_user
- end
-
- it "returns true" do
- expect(subject.can_resolve?(current_user)).to be true
- end
- end
-
- context "when the signed in user can push to the project" do
- before do
- subject.project.team << [current_user, :master]
- end
-
- it "returns true" do
- expect(subject.can_resolve?(current_user)).to be true
- end
- end
-
- context "when the signed in user is a random user" do
- it "returns false" do
- expect(subject.can_resolve?(current_user)).to be false
- end
- end
- end
- end
- end
-
- describe "#resolve!" do
- let(:current_user) { create(:user) }
-
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns nil" do
- expect(subject.resolve!(current_user)).to be_nil
- end
-
- it "doesn't set resolved_at" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_at).to be_nil
- end
-
- it "doesn't set resolved_by" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_by).to be_nil
- end
-
- it "doesn't mark as resolved" do
- subject.resolve!(current_user)
-
- expect(subject.resolved?).to be false
- end
- end
-
- context "when resolvable" do
- let(:user) { create(:user) }
- let(:second_note) { create(:diff_note_on_commit) } # unresolvable
-
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
- end
-
- context "when all resolvable notes are resolved" do
- before do
- first_note.resolve!(user)
- third_note.resolve!(user)
-
- first_note.reload
- third_note.reload
- end
-
- it "doesn't change resolved_at on the resolved notes" do
- expect(first_note.resolved_at).not_to be_nil
- expect(third_note.resolved_at).not_to be_nil
-
- expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_at }
- expect { subject.resolve!(current_user) }.not_to change { third_note.resolved_at }
- end
-
- it "doesn't change resolved_by on the resolved notes" do
- expect(first_note.resolved_by).to eq(user)
- expect(third_note.resolved_by).to eq(user)
-
- expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_by }
- expect { subject.resolve!(current_user) }.not_to change { third_note.resolved_by }
- end
-
- it "doesn't change the resolved state on the resolved notes" do
- expect(first_note.resolved?).to be true
- expect(third_note.resolved?).to be true
-
- expect { subject.resolve!(current_user) }.not_to change { first_note.resolved? }
- expect { subject.resolve!(current_user) }.not_to change { third_note.resolved? }
- end
-
- it "doesn't change resolved_at" do
- expect(subject.resolved_at).not_to be_nil
-
- expect { subject.resolve!(current_user) }.not_to change { subject.resolved_at }
- end
-
- it "doesn't change resolved_by" do
- expect(subject.resolved_by).to eq(user)
-
- expect { subject.resolve!(current_user) }.not_to change { subject.resolved_by }
- end
-
- it "doesn't change resolved state" do
- expect(subject.resolved?).to be true
-
- expect { subject.resolve!(current_user) }.not_to change { subject.resolved? }
- end
- end
-
- context "when some resolvable notes are resolved" do
- before do
- first_note.resolve!(user)
- end
-
- it "doesn't change resolved_at on the resolved note" do
- expect(first_note.resolved_at).not_to be_nil
-
- expect { subject.resolve!(current_user) }.
- not_to change { first_note.reload.resolved_at }
- end
-
- it "doesn't change resolved_by on the resolved note" do
- expect(first_note.resolved_by).to eq(user)
-
- expect { subject.resolve!(current_user) }.
- not_to change { first_note.reload && first_note.resolved_by }
- end
-
- it "doesn't change the resolved state on the resolved note" do
- expect(first_note.resolved?).to be true
-
- expect { subject.resolve!(current_user) }.
- not_to change { first_note.reload && first_note.resolved? }
- end
-
- it "sets resolved_at on the unresolved note" do
- subject.resolve!(current_user)
- third_note.reload
-
- expect(third_note.resolved_at).not_to be_nil
- end
-
- it "sets resolved_by on the unresolved note" do
- subject.resolve!(current_user)
- third_note.reload
-
- expect(third_note.resolved_by).to eq(current_user)
- end
-
- it "marks the unresolved note as resolved" do
- subject.resolve!(current_user)
- third_note.reload
-
- expect(third_note.resolved?).to be true
- end
-
- it "sets resolved_at" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_at).not_to be_nil
- end
-
- it "sets resolved_by" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_by).to eq(current_user)
- end
-
- it "marks as resolved" do
- subject.resolve!(current_user)
-
- expect(subject.resolved?).to be true
- end
- end
-
- context "when no resolvable notes are resolved" do
- it "sets resolved_at on the unresolved notes" do
- subject.resolve!(current_user)
- first_note.reload
- third_note.reload
-
- expect(first_note.resolved_at).not_to be_nil
- expect(third_note.resolved_at).not_to be_nil
- end
-
- it "sets resolved_by on the unresolved notes" do
- subject.resolve!(current_user)
- first_note.reload
- third_note.reload
-
- expect(first_note.resolved_by).to eq(current_user)
- expect(third_note.resolved_by).to eq(current_user)
- end
-
- it "marks the unresolved notes as resolved" do
- subject.resolve!(current_user)
- first_note.reload
- third_note.reload
-
- expect(first_note.resolved?).to be true
- expect(third_note.resolved?).to be true
- end
-
- it "sets resolved_at" do
- subject.resolve!(current_user)
- first_note.reload
- third_note.reload
-
- expect(subject.resolved_at).not_to be_nil
- end
-
- it "sets resolved_by" do
- subject.resolve!(current_user)
- first_note.reload
- third_note.reload
-
- expect(subject.resolved_by).to eq(current_user)
- end
-
- it "marks as resolved" do
- subject.resolve!(current_user)
- first_note.reload
- third_note.reload
-
- expect(subject.resolved?).to be true
- end
- end
- end
- end
-
- describe "#unresolve!" do
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns nil" do
- expect(subject.unresolve!).to be_nil
- end
- end
-
- context "when resolvable" do
- let(:user) { create(:user) }
-
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
-
- allow(first_note).to receive(:resolvable?).and_return(true)
- allow(second_note).to receive(:resolvable?).and_return(false)
- allow(third_note).to receive(:resolvable?).and_return(true)
- end
-
- context "when all resolvable notes are resolved" do
- before do
- first_note.resolve!(user)
- third_note.resolve!(user)
- end
-
- it "unsets resolved_at on the resolved notes" do
- subject.unresolve!
- first_note.reload
- third_note.reload
-
- expect(first_note.resolved_at).to be_nil
- expect(third_note.resolved_at).to be_nil
- end
-
- it "unsets resolved_by on the resolved notes" do
- subject.unresolve!
- first_note.reload
- third_note.reload
-
- expect(first_note.resolved_by).to be_nil
- expect(third_note.resolved_by).to be_nil
- end
-
- it "unmarks the resolved notes as resolved" do
- subject.unresolve!
- first_note.reload
- third_note.reload
-
- expect(first_note.resolved?).to be false
- expect(third_note.resolved?).to be false
- end
-
- it "unsets resolved_at" do
- subject.unresolve!
- first_note.reload
- third_note.reload
-
- expect(subject.resolved_at).to be_nil
- end
-
- it "unsets resolved_by" do
- subject.unresolve!
- first_note.reload
- third_note.reload
-
- expect(subject.resolved_by).to be_nil
- end
-
- it "unmarks as resolved" do
- subject.unresolve!
-
- expect(subject.resolved?).to be false
- end
- end
-
- context "when some resolvable notes are resolved" do
- before do
- first_note.resolve!(user)
- end
-
- it "unsets resolved_at on the resolved note" do
- subject.unresolve!
-
- expect(subject.first_note.resolved_at).to be_nil
- end
-
- it "unsets resolved_by on the resolved note" do
- subject.unresolve!
-
- expect(subject.first_note.resolved_by).to be_nil
- end
-
- it "unmarks the resolved note as resolved" do
- subject.unresolve!
-
- expect(subject.first_note.resolved?).to be false
- end
- end
+ describe '.build' do
+ it 'returns a discussion of the right type' do
+ discussion = described_class.build([first_note, second_note], merge_request)
+ expect(discussion).to be_a(DiffDiscussion)
+ expect(discussion.notes.count).to be(2)
+ expect(discussion.first_note).to be(first_note)
+ expect(discussion.noteable).to be(merge_request)
end
end
- describe "#first_note_to_resolve" do
- it "returns the first not that still needs to be resolved" do
- allow(first_note).to receive(:to_be_resolved?).and_return(false)
- allow(second_note).to receive(:to_be_resolved?).and_return(true)
-
- expect(subject.first_note_to_resolve).to eq(second_note)
- end
- end
-
- describe "#collapsed?" do
- context "when a diff discussion" do
- before do
- allow(subject).to receive(:diff_discussion?).and_return(true)
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
- end
-
- context "when resolved" do
- before do
- allow(subject).to receive(:resolved?).and_return(true)
- end
-
- it "returns true" do
- expect(subject.collapsed?).to be true
- end
- end
-
- context "when not resolved" do
- before do
- allow(subject).to receive(:resolved?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.collapsed?).to be false
- end
- end
- end
-
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- context "when active" do
- before do
- allow(subject).to receive(:active?).and_return(true)
- end
-
- it "returns false" do
- expect(subject.collapsed?).to be false
- end
- end
-
- context "when outdated" do
- before do
- allow(subject).to receive(:active?).and_return(false)
- end
-
- it "returns true" do
- expect(subject.collapsed?).to be true
- end
- end
- end
- end
-
- context "when not a diff discussion" do
- before do
- allow(subject).to receive(:diff_discussion?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.collapsed?).to be false
- end
- end
- end
-
- describe "#truncated_diff_lines" do
- let(:truncated_lines) { subject.truncated_diff_lines }
-
- context "when diff is greater than allowed number of truncated diff lines " do
- it "returns fewer lines" do
- expect(subject.diff_lines.count).to be > described_class::NUMBER_OF_TRUNCATED_DIFF_LINES
-
- expect(truncated_lines.count).to be <= described_class::NUMBER_OF_TRUNCATED_DIFF_LINES
- end
- end
-
- context "when some diff lines are meta" do
- it "returns no meta lines" do
- expect(subject.diff_lines).to include(be_meta)
- expect(truncated_lines).not_to include(be_meta)
- end
+ describe '.build_collection' do
+ it 'returns an array of discussions of the right type' do
+ discussions = described_class.build_collection([first_note, second_note, third_note], merge_request)
+ expect(discussions).to eq([
+ DiffDiscussion.new([first_note, second_note], merge_request),
+ DiffDiscussion.new([third_note], merge_request)
+ ])
end
end
end
diff --git a/spec/models/legacy_diff_discussion_spec.rb b/spec/models/legacy_diff_discussion_spec.rb
new file mode 100644
index 00000000000..153e757a0ef
--- /dev/null
+++ b/spec/models/legacy_diff_discussion_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe LegacyDiffDiscussion, models: true do
+ subject { create(:legacy_diff_note_on_merge_request).to_discussion }
+
+ describe '#reply_attributes' do
+ it 'includes line_code' do
+ expect(subject.reply_attributes[:line_code]).to eq(subject.line_code)
+ end
+ end
+end
diff --git a/spec/models/legacy_diff_note_spec.rb b/spec/models/legacy_diff_note_spec.rb
deleted file mode 100644
index 81517a18b74..00000000000
--- a/spec/models/legacy_diff_note_spec.rb
+++ /dev/null
@@ -1,101 +0,0 @@
-require 'spec_helper'
-
-describe LegacyDiffNote, models: true do
- describe "Commit diff line notes" do
- let!(:note) { create(:legacy_diff_note_on_commit, note: "+1 from me") }
- let!(:commit) { note.noteable }
-
- it "saves a valid note" do
- expect(note.commit_id).to eq(commit.id)
- expect(note.noteable.id).to eq(commit.id)
- end
-
- it "is recognized by #legacy_diff_note?" do
- expect(note).to be_legacy_diff_note
- end
- end
-
- describe '#active?' do
- it 'is always true when the note has no associated diff line' do
- note = build(:legacy_diff_note_on_merge_request)
-
- expect(note).to receive(:diff_line).and_return(nil)
-
- expect(note).to be_active
- end
-
- it 'is never true when the note has no noteable associated' do
- note = build(:legacy_diff_note_on_merge_request)
-
- expect(note).to receive(:diff_line).and_return(double)
- expect(note).to receive(:noteable).and_return(nil)
-
- expect(note).not_to be_active
- end
-
- it 'returns the memoized value if defined' do
- note = build(:legacy_diff_note_on_merge_request)
-
- note.instance_variable_set(:@active, 'foo')
- expect(note).not_to receive(:find_noteable_diff)
-
- expect(note.active?).to eq 'foo'
- end
-
- context 'for a merge request noteable' do
- it 'is false when noteable has no matching diff' do
- merge = build_stubbed(:merge_request, :simple)
- note = build(:legacy_diff_note_on_merge_request, noteable: merge)
-
- allow(note).to receive(:diff_line).and_return(double)
- expect(note).to receive(:find_noteable_diff).and_return(nil)
-
- expect(note).not_to be_active
- end
-
- it 'is true when noteable has a matching diff' do
- merge = create(:merge_request, :simple)
-
- # Generate a real line_code value so we know it will match. We use a
- # random line from a random diff just for funsies.
- diff = merge.raw_diffs.to_a.sample
- line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample
- code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
-
- # We're persisting in order to trigger the set_diff callback
- note = create(:legacy_diff_note_on_merge_request, noteable: merge,
- line_code: code,
- project: merge.source_project)
-
- # Make sure we don't get a false positive from a guard clause
- expect(note).to receive(:find_noteable_diff).and_call_original
- expect(note).to be_active
- end
- end
- end
-
- describe "#discussion_id" do
- let(:note) { create(:note) }
-
- context "when it is newly created" do
- it "has a discussion id" do
- expect(note.discussion_id).not_to be_nil
- expect(note.discussion_id).to match(/\A\h{40}\z/)
- end
- end
-
- context "when it didn't store a discussion id before" do
- before do
- note.update_column(:discussion_id, nil)
- end
-
- it "has a discussion id" do
- # The discussion_id is set in `after_initialize`, so `reload` won't work
- reloaded_note = Note.find(note.id)
-
- expect(reloaded_note.discussion_id).not_to be_nil
- expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/)
- end
- end
- end
-end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 24e7c1b17d9..b615d956686 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1224,182 +1224,6 @@ describe MergeRequest, models: true do
end
end
- context "discussion status" do
- let(:first_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
- let(:second_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
- let(:third_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
-
- before do
- allow(subject).to receive(:diff_discussions).and_return([first_discussion, second_discussion, third_discussion])
- end
-
- describe '#resolvable_discussions' do
- before do
- allow(first_discussion).to receive(:to_be_resolved?).and_return(true)
- allow(second_discussion).to receive(:to_be_resolved?).and_return(false)
- allow(third_discussion).to receive(:to_be_resolved?).and_return(false)
- end
-
- it 'includes only discussions that need to be resolved' do
- expect(subject.resolvable_discussions).to eq([first_discussion])
- end
- end
-
- describe '#discussions_can_be_resolved_by? user' do
- let(:user) { build(:user) }
-
- context 'all discussions can be resolved by the user' do
- before do
- allow(first_discussion).to receive(:can_resolve?).with(user).and_return(true)
- allow(second_discussion).to receive(:can_resolve?).with(user).and_return(true)
- allow(third_discussion).to receive(:can_resolve?).with(user).and_return(true)
- end
-
- it 'allows a user to resolve the discussions' do
- expect(subject.discussions_can_be_resolved_by?(user)).to be(true)
- end
- end
-
- context 'one discussion cannot be resolved by the user' do
- before do
- allow(first_discussion).to receive(:can_resolve?).with(user).and_return(true)
- allow(second_discussion).to receive(:can_resolve?).with(user).and_return(true)
- allow(third_discussion).to receive(:can_resolve?).with(user).and_return(false)
- end
-
- it 'allows a user to resolve the discussions' do
- expect(subject.discussions_can_be_resolved_by?(user)).to be(false)
- end
- end
- end
-
- describe "#discussions_resolvable?" do
- context "when all discussions are unresolvable" do
- before do
- allow(first_discussion).to receive(:resolvable?).and_return(false)
- allow(second_discussion).to receive(:resolvable?).and_return(false)
- allow(third_discussion).to receive(:resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.discussions_resolvable?).to be false
- end
- end
-
- context "when some discussions are unresolvable and some discussions are resolvable" do
- before do
- allow(first_discussion).to receive(:resolvable?).and_return(true)
- allow(second_discussion).to receive(:resolvable?).and_return(false)
- allow(third_discussion).to receive(:resolvable?).and_return(true)
- end
-
- it "returns true" do
- expect(subject.discussions_resolvable?).to be true
- end
- end
-
- context "when all discussions are resolvable" do
- before do
- allow(first_discussion).to receive(:resolvable?).and_return(true)
- allow(second_discussion).to receive(:resolvable?).and_return(true)
- allow(third_discussion).to receive(:resolvable?).and_return(true)
- end
-
- it "returns true" do
- expect(subject.discussions_resolvable?).to be true
- end
- end
- end
-
- describe "#discussions_resolved?" do
- context "when discussions are not resolvable" do
- before do
- allow(subject).to receive(:discussions_resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.discussions_resolved?).to be false
- end
- end
-
- context "when discussions are resolvable" do
- before do
- allow(subject).to receive(:discussions_resolvable?).and_return(true)
-
- allow(first_discussion).to receive(:resolvable?).and_return(true)
- allow(second_discussion).to receive(:resolvable?).and_return(false)
- allow(third_discussion).to receive(:resolvable?).and_return(true)
- end
-
- context "when all resolvable discussions are resolved" do
- before do
- allow(first_discussion).to receive(:resolved?).and_return(true)
- allow(third_discussion).to receive(:resolved?).and_return(true)
- end
-
- it "returns true" do
- expect(subject.discussions_resolved?).to be true
- end
- end
-
- context "when some resolvable discussions are not resolved" do
- before do
- allow(first_discussion).to receive(:resolved?).and_return(true)
- allow(third_discussion).to receive(:resolved?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.discussions_resolved?).to be false
- end
- end
- end
- end
-
- describe "#discussions_to_be_resolved?" do
- context "when discussions are not resolvable" do
- before do
- allow(subject).to receive(:discussions_resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.discussions_to_be_resolved?).to be false
- end
- end
-
- context "when discussions are resolvable" do
- before do
- allow(subject).to receive(:discussions_resolvable?).and_return(true)
-
- allow(first_discussion).to receive(:resolvable?).and_return(true)
- allow(second_discussion).to receive(:resolvable?).and_return(false)
- allow(third_discussion).to receive(:resolvable?).and_return(true)
- end
-
- context "when all resolvable discussions are resolved" do
- before do
- allow(first_discussion).to receive(:resolved?).and_return(true)
- allow(third_discussion).to receive(:resolved?).and_return(true)
- end
-
- it "returns false" do
- expect(subject.discussions_to_be_resolved?).to be false
- end
- end
-
- context "when some resolvable discussions are not resolved" do
- before do
- allow(first_discussion).to receive(:resolved?).and_return(true)
- allow(third_discussion).to receive(:resolved?).and_return(false)
- end
-
- it "returns true" do
- expect(subject.discussions_to_be_resolved?).to be true
- end
- end
- end
- end
- end
-
describe '#conflicts_can_be_resolved_in_ui?' do
def create_merge_request(source_branch)
create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start') do |mr|
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 33536487c41..3c4bf3f4ddb 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -245,14 +245,28 @@ describe Note, models: true do
end
end
+ describe '.find_discussion' do
+ let!(:note) { create(:discussion_note_on_merge_request) }
+ let!(:note2) { create(:discussion_note_on_merge_request, in_reply_to: note) }
+ let(:merge_request) { note.noteable }
+
+ it 'returns a discussion with multiple notes' do
+ discussion = merge_request.notes.find_discussion(note.discussion_id)
+
+ expect(discussion).not_to be_nil
+ expect(discussion.notes).to match_array([note, note2])
+ expect(discussion.first_note.discussion_id).to eq(note.discussion_id)
+ end
+ end
+
describe ".grouped_diff_discussions" do
let!(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let!(:active_diff_note1) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
- let!(:active_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
+ let!(:active_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, in_reply_to: active_diff_note1) }
let!(:active_diff_note3) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: active_position2) }
let!(:outdated_diff_note1) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: outdated_position) }
- let!(:outdated_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: outdated_position) }
+ let!(:outdated_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, in_reply_to: outdated_diff_note1) }
let(:active_position2) do
Gitlab::Diff::Position.new(
@@ -277,7 +291,7 @@ describe Note, models: true do
subject { merge_request.notes.grouped_diff_discussions }
it "includes active discussions" do
- discussions = subject.values
+ discussions = subject.values.flatten
expect(discussions.count).to eq(2)
expect(discussions.map(&:id)).to eq([active_diff_note1.discussion_id, active_diff_note3.discussion_id])
@@ -288,37 +302,12 @@ describe Note, models: true do
end
it "doesn't include outdated discussions" do
- expect(subject.values.map(&:id)).not_to include(outdated_diff_note1.discussion_id)
+ expect(subject.values.flatten.map(&:id)).not_to include(outdated_diff_note1.discussion_id)
end
it "groups the discussions by line code" do
- expect(subject[active_diff_note1.line_code].id).to eq(active_diff_note1.discussion_id)
- expect(subject[active_diff_note3.line_code].id).to eq(active_diff_note3.discussion_id)
- end
- end
-
- describe "#discussion_id" do
- let(:note) { create(:note) }
-
- context "when it is newly created" do
- it "has a discussion id" do
- expect(note.discussion_id).not_to be_nil
- expect(note.discussion_id).to match(/\A\h{40}\z/)
- end
- end
-
- context "when it didn't store a discussion id before" do
- before do
- note.update_column(:discussion_id, nil)
- end
-
- it "has a discussion id" do
- # The discussion_id is set in `after_initialize`, so `reload` won't work
- reloaded_note = Note.find(note.id)
-
- expect(reloaded_note.discussion_id).not_to be_nil
- expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/)
- end
+ expect(subject[active_diff_note1.line_code].first.id).to eq(active_diff_note1.discussion_id)
+ expect(subject[active_diff_note3.line_code].first.id).to eq(active_diff_note3.discussion_id)
end
end
@@ -388,6 +377,248 @@ describe Note, models: true do
end
end
+ describe '#can_be_discussion_note?' do
+ context 'for a note on a merge request' do
+ it 'returns true' do
+ note = build(:note_on_merge_request)
+
+ expect(note.can_be_discussion_note?).to be_truthy
+ end
+ end
+
+ context 'for a note on an issue' do
+ it 'returns true' do
+ note = build(:note_on_issue)
+
+ expect(note.can_be_discussion_note?).to be_truthy
+ end
+ end
+
+ context 'for a note on a commit' do
+ it 'returns true' do
+ note = build(:note_on_commit)
+
+ expect(note.can_be_discussion_note?).to be_truthy
+ end
+ end
+
+ context 'for a note on a snippet' do
+ it 'returns true' do
+ note = build(:note_on_project_snippet)
+
+ expect(note.can_be_discussion_note?).to be_truthy
+ end
+ end
+
+ context 'for a diff note on merge request' do
+ it 'returns false' do
+ note = build(:diff_note_on_merge_request)
+
+ expect(note.can_be_discussion_note?).to be_falsey
+ end
+ end
+
+ context 'for a diff note on commit' do
+ it 'returns false' do
+ note = build(:diff_note_on_commit)
+
+ expect(note.can_be_discussion_note?).to be_falsey
+ end
+ end
+
+ context 'for a discussion note' do
+ it 'returns false' do
+ note = build(:discussion_note_on_merge_request)
+
+ expect(note.can_be_discussion_note?).to be_falsey
+ end
+ end
+ end
+
+ describe '#discussion_class' do
+ let(:note) { build(:note_on_commit) }
+ let(:merge_request) { create(:merge_request) }
+
+ context 'when the note is displayed out of context' do
+ it 'returns OutOfContextDiscussion' do
+ expect(note.discussion_class(merge_request)).to be(OutOfContextDiscussion)
+ end
+ end
+
+ context 'when the note is displayed in the original context' do
+ it 'returns IndividualNoteDiscussion' do
+ expect(note.discussion_class(note.noteable)).to be(IndividualNoteDiscussion)
+ end
+ end
+ end
+
+ describe "#discussion_id" do
+ let(:note) { create(:note_on_commit) }
+
+ context "when it is newly created" do
+ it "has a discussion id" do
+ expect(note.discussion_id).not_to be_nil
+ expect(note.discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+
+ context "when it didn't store a discussion id before" do
+ before do
+ note.update_column(:discussion_id, nil)
+ end
+
+ it "has a discussion id" do
+ # The discussion_id is set in `after_initialize`, so `reload` won't work
+ reloaded_note = Note.find(note.id)
+
+ expect(reloaded_note.discussion_id).not_to be_nil
+ expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+
+ context 'when the note is displayed out of context' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'overrides the discussion id' do
+ expect(note.discussion_id(merge_request)).not_to eq(note.discussion_id)
+ end
+ end
+ end
+
+ describe '#to_discussion' do
+ subject { create(:discussion_note_on_merge_request) }
+ let!(:note2) { create(:discussion_note_on_merge_request, project: subject.project, noteable: subject.noteable, in_reply_to: subject) }
+
+ it "returns a discussion with just this note" do
+ discussion = subject.to_discussion
+
+ expect(discussion.id).to eq(subject.discussion_id)
+ expect(discussion.notes).to eq([subject])
+ end
+ end
+
+ describe "#discussion" do
+ let!(:note1) { create(:discussion_note_on_merge_request) }
+ let!(:note2) { create(:diff_note_on_merge_request, project: note1.project, noteable: note1.noteable) }
+
+ context 'when the note is part of a discussion' do
+ subject { create(:discussion_note_on_merge_request, project: note1.project, noteable: note1.noteable, in_reply_to: note1) }
+
+ it "returns the discussion this note is in" do
+ discussion = subject.discussion
+
+ expect(discussion.id).to eq(subject.discussion_id)
+ expect(discussion.notes).to eq([note1, subject])
+ end
+ end
+
+ context 'when the note is not part of a discussion' do
+ subject { create(:note) }
+
+ it "returns a discussion with just this note" do
+ discussion = subject.discussion
+
+ expect(discussion.id).to eq(subject.discussion_id)
+ expect(discussion.notes).to eq([subject])
+ end
+ end
+ end
+
+ describe "#part_of_discussion?" do
+ context 'for a regular note' do
+ let(:note) { build(:note) }
+
+ it 'returns false' do
+ expect(note.part_of_discussion?).to be_falsey
+ end
+ end
+
+ context 'for a diff note' do
+ let(:note) { build(:diff_note_on_commit) }
+
+ it 'returns true' do
+ expect(note.part_of_discussion?).to be_truthy
+ end
+ end
+
+ context 'for a discussion note' do
+ let(:note) { build(:discussion_note_on_merge_request) }
+
+ it 'returns true' do
+ expect(note.part_of_discussion?).to be_truthy
+ end
+ end
+ end
+
+ describe '#in_reply_to?' do
+ context 'for a note' do
+ context 'when part of a discussion' do
+ subject { create(:discussion_note_on_issue) }
+ let(:note) { create(:discussion_note_on_issue, in_reply_to: subject) }
+
+ it 'checks if the note is in reply to the other discussion' do
+ expect(subject).to receive(:in_reply_to?).with(note).and_call_original
+ expect(subject).to receive(:in_reply_to?).with(note.noteable).and_call_original
+ expect(subject).to receive(:in_reply_to?).with(note.to_discussion).and_call_original
+
+ subject.in_reply_to?(note)
+ end
+ end
+
+ context 'when not part of a discussion' do
+ subject { create(:note) }
+ let(:note) { create(:note, in_reply_to: subject) }
+
+ it 'checks if the note is in reply to the other noteable' do
+ expect(subject).to receive(:in_reply_to?).with(note).and_call_original
+ expect(subject).to receive(:in_reply_to?).with(note.noteable).and_call_original
+
+ subject.in_reply_to?(note)
+ end
+ end
+ end
+
+ context 'for a discussion' do
+ context 'when part of the same discussion' do
+ subject { create(:diff_note_on_merge_request) }
+ let(:note) { create(:diff_note_on_merge_request, in_reply_to: subject) }
+
+ it 'returns true' do
+ expect(subject.in_reply_to?(note.to_discussion)).to be_truthy
+ end
+ end
+
+ context 'when not part of the same discussion' do
+ subject { create(:diff_note_on_merge_request) }
+ let(:note) { create(:diff_note_on_merge_request) }
+
+ it 'returns false' do
+ expect(subject.in_reply_to?(note.to_discussion)).to be_falsey
+ end
+ end
+ end
+
+ context 'for a noteable' do
+ context 'when a comment on the same noteable' do
+ subject { create(:note) }
+ let(:note) { create(:note, in_reply_to: subject) }
+
+ it 'returns true' do
+ expect(subject.in_reply_to?(note.noteable)).to be_truthy
+ end
+ end
+
+ context 'when not a comment on the same noteable' do
+ subject { create(:note) }
+ let(:note) { create(:note) }
+
+ it 'returns false' do
+ expect(subject.in_reply_to?(note.noteable)).to be_falsey
+ end
+ end
+ end
+ end
+
describe 'expiring ETag cache' do
let(:note) { build(:note_on_issue) }
diff --git a/spec/models/sent_notification_spec.rb b/spec/models/sent_notification_spec.rb
new file mode 100644
index 00000000000..6b7eef388be
--- /dev/null
+++ b/spec/models/sent_notification_spec.rb
@@ -0,0 +1,166 @@
+require 'spec_helper'
+
+describe SentNotification, model: true do
+ describe 'validation' do
+ describe 'note validity' do
+ context "when the project doesn't match the noteable's project" do
+ subject { build(:sent_notification, noteable: create(:issue)) }
+
+ it "is invalid" do
+ expect(subject).not_to be_valid
+ end
+ end
+
+ context "when the project doesn't match the discussion project" do
+ let(:discussion_id) { create(:note).discussion_id }
+ subject { build(:sent_notification, in_reply_to_discussion_id: discussion_id) }
+
+ it "is invalid" do
+ expect(subject).not_to be_valid
+ end
+ end
+
+ context "when the noteable project and discussion project match" do
+ let(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:discussion_id) { create(:note, project: project, noteable: issue).discussion_id }
+ subject { build(:sent_notification, project: project, noteable: issue, in_reply_to_discussion_id: discussion_id) }
+
+ it "is valid" do
+ expect(subject).to be_valid
+ end
+ end
+ end
+ end
+
+ describe '.record' do
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue) }
+
+ it 'creates a new SentNotification' do
+ expect { described_class.record(issue, user.id) }.to change { SentNotification.count }.by(1)
+ end
+ end
+
+ describe '.record_note' do
+ let(:user) { create(:user) }
+ let(:note) { create(:diff_note_on_merge_request) }
+
+ it 'creates a new SentNotification' do
+ expect { described_class.record_note(note, user.id) }.to change { SentNotification.count }.by(1)
+ end
+ end
+
+ describe '#create_reply' do
+ context 'for issue' do
+ let(:issue) { create(:issue) }
+ subject { described_class.record(issue, issue.author.id) }
+
+ it 'creates a comment on the issue' do
+ note = subject.create_reply('Test')
+ expect(note.in_reply_to?(issue)).to be_truthy
+ end
+ end
+
+ context 'for issue comment' do
+ let(:note) { create(:note_on_issue) }
+ subject { described_class.record_note(note, note.author.id) }
+
+ it 'creates a comment on the issue' do
+ new_note = subject.create_reply('Test')
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ end
+ end
+
+ context 'for issue discussion' do
+ let(:note) { create(:discussion_note_on_issue) }
+ subject { described_class.record_note(note, note.author.id) }
+
+ it 'creates a reply on the discussion' do
+ new_note = subject.create_reply('Test')
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ end
+ end
+
+ context 'for merge request' do
+ let(:merge_request) { create(:merge_request) }
+ subject { described_class.record(merge_request, merge_request.author.id) }
+
+ it 'creates a comment on the merge_request' do
+ note = subject.create_reply('Test')
+ expect(note.in_reply_to?(merge_request)).to be_truthy
+ end
+ end
+
+ context 'for merge request comment' do
+ let(:note) { create(:note_on_merge_request) }
+ subject { described_class.record_note(note, note.author.id) }
+
+ it 'creates a comment on the merge request' do
+ new_note = subject.create_reply('Test')
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ end
+ end
+
+ context 'for merge request diff discussion' do
+ let(:note) { create(:diff_note_on_merge_request) }
+ subject { described_class.record_note(note, note.author.id) }
+
+ it 'creates a reply on the discussion' do
+ new_note = subject.create_reply('Test')
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ end
+ end
+
+ context 'for merge request non-diff discussion' do
+ let(:note) { create(:discussion_note_on_merge_request) }
+ subject { described_class.record_note(note, note.author.id) }
+
+ it 'creates a reply on the discussion' do
+ new_note = subject.create_reply('Test')
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ end
+ end
+
+ context 'for commit' do
+ let(:project) { create(:project) }
+ let(:commit) { project.commit }
+ subject { described_class.record(commit, project.creator.id) }
+
+ it 'creates a comment on the commit' do
+ note = subject.create_reply('Test')
+ expect(note.in_reply_to?(commit)).to be_truthy
+ end
+ end
+
+ context 'for commit comment' do
+ let(:note) { create(:note_on_commit) }
+ subject { described_class.record_note(note, note.author.id) }
+
+ it 'creates a comment on the commit' do
+ new_note = subject.create_reply('Test')
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ end
+ end
+
+ context 'for commit diff discussion' do
+ let(:note) { create(:diff_note_on_commit) }
+ subject { described_class.record_note(note, note.author.id) }
+
+ it 'creates a reply on the discussion' do
+ new_note = subject.create_reply('Test')
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ end
+ end
+
+ context 'for commit non-diff discussion' do
+ let(:note) { create(:discussion_note_on_commit) }
+ subject { described_class.record_note(note, note.author.id) }
+
+ it 'creates a reply on the discussion' do
+ new_note = subject.create_reply('Test')
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 2b27ce6390a..551aae7d701 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -840,7 +840,7 @@ describe API::Issues, api: true do
end
context 'resolving discussions' do
- let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
+ let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
let(:merge_request) { discussion.noteable }
let(:project) { merge_request.source_project }
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
index b1b398a897e..91d9057075f 100644
--- a/spec/requests/api/v3/issues_spec.rb
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -824,7 +824,7 @@ describe API::V3::Issues, api: true do
end
context 'resolving issues in a merge request' do
- let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
+ let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
let(:merge_request) { discussion.noteable }
let(:project) { merge_request.source_project }
before do
diff --git a/spec/services/discussions/resolve_service_spec.rb b/spec/services/discussions/resolve_service_spec.rb
index 12c3cdf28c6..ab8df7b74cd 100644
--- a/spec/services/discussions/resolve_service_spec.rb
+++ b/spec/services/discussions/resolve_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Discussions::ResolveService do
describe '#execute' do
- let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
+ let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
let(:project) { merge_request.project }
let(:merge_request) { discussion.noteable }
let(:user) { create(:user) }
@@ -41,7 +41,7 @@ describe Discussions::ResolveService do
end
it 'can resolve multiple discussions at once' do
- other_discussion = Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: discussion.noteable, project: discussion.noteable.source_project)]).first
+ other_discussion = create(:diff_note_on_merge_request, noteable: discussion.noteable, project: discussion.noteable.source_project).to_discussion
service.execute([discussion, other_discussion])
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 17990f41b3b..55d635235b0 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -11,7 +11,7 @@ describe Issues::BuildService, services: true do
context 'for a single discussion' do
describe '#execute' do
let(:merge_request) { create(:merge_request, title: "Hello world", source_project: project) }
- let(:discussion) { Discussion.new([create(:diff_note_on_merge_request, project: project, noteable: merge_request, note: "Almost done")]) }
+ let(:discussion) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, note: "Almost done").to_discussion }
let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id) }
it 'references the noteable title in the issue title' do
@@ -47,7 +47,7 @@ describe Issues::BuildService, services: true do
let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) }
it 'mentions the author of the note' do
- discussion = Discussion.new([create(:diff_note_on_merge_request, author: create(:user, username: 'author'))])
+ discussion = create(:diff_note_on_merge_request, author: create(:user, username: 'author')).to_discussion
expect(service.item_for_discussion(discussion)).to include('@author')
end
@@ -60,7 +60,7 @@ describe Issues::BuildService, services: true do
note_result = " > This is a string\n"\
" > > with a blockquote\n"\
" > > > That has a quote\n"
- discussion = Discussion.new([create(:diff_note_on_merge_request, note: note_text)])
+ discussion = create(:diff_note_on_merge_request, note: note_text).to_discussion
expect(service.item_for_discussion(discussion)).to include(note_result)
end
end
@@ -91,25 +91,23 @@ describe Issues::BuildService, services: true do
end
describe 'with multiple discussions' do
- before do
- create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.target_project, line_number: 15)
- end
+ let!(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.target_project, line_number: 15) }
it 'mentions all the authors in the description' do
- authors = merge_request.diff_discussions.map(&:author)
+ authors = merge_request.resolvable_discussions.map(&:author)
expect(issue.description).to include(*authors.map(&:to_reference))
end
it 'has a link for each unresolved discussion in the description' do
- notes = merge_request.diff_discussions.map(&:first_note)
+ notes = merge_request.resolvable_discussions.map(&:first_note)
links = notes.map { |note| Gitlab::UrlBuilder.build(note) }
expect(issue.description).to include(*links)
end
it 'mentions additional notes' do
- create_list(:diff_note_on_merge_request, 2, noteable: merge_request, project: merge_request.target_project, line_number: 15)
+ create_list(:diff_note_on_merge_request, 2, noteable: merge_request, project: merge_request.target_project, in_reply_to: diff_note)
expect(issue.description).to include('(+2 comments)')
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 776cbc4296b..80bfb731550 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -141,7 +141,7 @@ describe Issues::CreateService, services: true do
it_behaves_like 'new issuable record that supports slash commands'
context 'resolving discussions' do
- let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
+ let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
let(:merge_request) { discussion.noteable }
let(:project) { merge_request.source_project }
diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb
index 3a72f92383c..4a4929daefc 100644
--- a/spec/services/issues/resolve_discussions_spec.rb
+++ b/spec/services/issues/resolve_discussions_spec.rb
@@ -18,7 +18,7 @@ describe DummyService, services: true do
end
describe "for resolving discussions" do
- let(:discussion) { Discussion.new([create(:diff_note_on_merge_request, project: project, note: "Almost done")]) }
+ let(:discussion) { create(:diff_note_on_merge_request, project: project, note: "Almost done").to_discussion }
let(:merge_request) { discussion.noteable }
let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "other") }
diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb
new file mode 100644
index 00000000000..f9dd5541b10
--- /dev/null
+++ b/spec/services/notes/build_service_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Notes::BuildService, services: true do
+ let(:note) { create(:discussion_note_on_issue) }
+ let(:project) { note.project }
+ let(:author) { note.author }
+
+ describe '#execute' do
+ context 'when in_reply_to_discussion_id is specified' do
+ context 'when a note with that original discussion ID exists' do
+ it 'sets the note up to be in reply to that note' do
+ new_note = described_class.new(project, author, note: 'Test', in_reply_to_discussion_id: note.discussion_id).execute
+ expect(new_note).to be_valid
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ end
+ end
+
+ context 'when a note with that discussion ID exists' do
+ it 'sets the note up to be in reply to that note' do
+ new_note = described_class.new(project, author, note: 'Test', in_reply_to_discussion_id: note.discussion_id).execute
+ expect(new_note).to be_valid
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ end
+ end
+
+ context 'when no note with that discussion ID exists' do
+ it 'sets an error' do
+ new_note = described_class.new(project, author, note: 'Test', in_reply_to_discussion_id: 'foo').execute
+ expect(new_note.errors[:base]).to include('Discussion to reply to cannot be found')
+ end
+ end
+ end
+
+ it 'builds a note without saving it' do
+ new_note = described_class.new(project, author, noteable_type: note.noteable_type, noteable_id: note.noteable_id, note: 'Test').execute
+ expect(new_note).to be_valid
+ expect(new_note).not_to be_persisted
+ end
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index e3146a56495..617c8eaf3d5 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -439,7 +439,7 @@ describe NotificationService, services: true do
notification.new_note(note)
- expect(SentNotification.last.position).to eq(note.position)
+ expect(SentNotification.last.in_reply_to_discussion_id).to eq(note.discussion_id)
end
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 5ec1ed8237b..42d63a9f9ba 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -796,7 +796,7 @@ describe SystemNoteService, services: true do
end
describe '.discussion_continued_in_issue' do
- let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
+ let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
let(:merge_request) { discussion.noteable }
let(:project) { merge_request.source_project }
let(:issue) { create(:issue, project: project) }
diff --git a/yarn.lock b/yarn.lock
index 9f2b8fe3d6e..e08c2f7f053 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,6 +2,12 @@
# yarn lockfile v1
+"@gitlab-org/droplab@^0.1.0":
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/droplab/-/droplab-0.1.0.tgz#934889d0f19417198e277415fdd336616b2e2c87"
+ dependencies:
+ custom-event-polyfill "^0.3.0"
+
abbrev@1, abbrev@1.0.x:
version "1.0.9"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
@@ -14,8 +20,8 @@ accepts@1.3.3, accepts@~1.3.3:
negotiator "0.6.1"
acorn-dynamic-import@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.1.tgz#23f671eb6e650dab277fef477c321b1178a8cca2"
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4"
dependencies:
acorn "^4.0.3"
@@ -25,18 +31,18 @@ acorn-jsx@^3.0.0:
dependencies:
acorn "^3.0.4"
-acorn@4.0.4, acorn@^4.0.4:
- version "4.0.4"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a"
-
acorn@^3.0.4:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
-acorn@^4.0.11, acorn@^4.0.3:
+acorn@^4.0.11, acorn@^4.0.3, acorn@^4.0.4:
version "4.0.11"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0"
+acorn@^5.0.1:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.0.3.tgz#c460df08491463f028ccb82eab3730bf01087b3d"
+
after@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
@@ -45,9 +51,9 @@ ajv-keywords@^1.0.0, ajv-keywords@^1.1.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
-ajv@^4.7.0:
- version "4.11.2"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.2.tgz#f166c3c11cbc6cb9dcc102a5bcfe5b72c95287e6"
+ajv@^4.7.0, ajv@^4.9.1:
+ version "4.11.5"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.5.tgz#b6ee74657b993a01dce44b7944d56f485828d5bd"
dependencies:
co "^4.6.0"
json-stable-stringify "^1.0.1"
@@ -94,8 +100,8 @@ append-transform@^0.4.0:
default-require-extensions "^1.0.0"
aproba@^1.0.3:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.0.tgz#4d8f047a318604e18e3c06a0e52230d3d19f147b"
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.1.tgz#95d3600f07710aa0e9298c726ad5ecf2eacbabab"
are-we-there-yet@~1.1.2:
version "1.1.2"
@@ -166,14 +172,14 @@ asn1@~0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+
assert-plus@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
-assert-plus@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
-
assert@^1.1.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
@@ -184,17 +190,17 @@ async-each@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
-async@0.2.x, async@~0.2.6:
+async@0.2.x:
version "0.2.10"
resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
-async@1.x, async@^1.4.0, async@^1.4.2, async@^1.5.2:
+async@1.x, async@^1.4.0, async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
async@^2.1.2, async@^2.1.4:
- version "2.1.4"
- resolved "https://registry.yarnpkg.com/async/-/async-2.1.4.tgz#2d2160c7788032e4dd6cbe2502f1f9a2c8f6cde4"
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.3.0.tgz#1013d1051047dd320fe24e494d5c66ecaf6147d9"
dependencies:
lodash "^4.14.0"
@@ -222,15 +228,15 @@ babel-code-frame@^6.16.0, babel-code-frame@^6.22.0:
esutils "^2.0.2"
js-tokens "^3.0.0"
-babel-core@^6.22.1, babel-core@^6.23.0:
- version "6.23.1"
- resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.23.1.tgz#c143cb621bb2f621710c220c5d579d15b8a442df"
+babel-core@^6.22.1, babel-core@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.24.0.tgz#8f36a0a77f5c155aed6f920b844d23ba56742a02"
dependencies:
babel-code-frame "^6.22.0"
- babel-generator "^6.23.0"
+ babel-generator "^6.24.0"
babel-helpers "^6.23.0"
babel-messages "^6.23.0"
- babel-register "^6.23.0"
+ babel-register "^6.24.0"
babel-runtime "^6.22.0"
babel-template "^6.23.0"
babel-traverse "^6.23.1"
@@ -246,9 +252,9 @@ babel-core@^6.22.1, babel-core@^6.23.0:
slash "^1.0.0"
source-map "^0.5.0"
-babel-generator@^6.18.0, babel-generator@^6.23.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.23.0.tgz#6b8edab956ef3116f79d8c84c5a3c05f32a74bc5"
+babel-generator@^6.18.0, babel-generator@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.24.0.tgz#eba270a8cc4ce6e09a61be43465d7c62c1f87c56"
dependencies:
babel-messages "^6.23.0"
babel-runtime "^6.22.0"
@@ -378,11 +384,11 @@ babel-helpers@^6.23.0:
babel-template "^6.23.0"
babel-loader@^6.2.10:
- version "6.2.10"
- resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-6.2.10.tgz#adefc2b242320cd5d15e65b31cea0e8b1b02d4b0"
+ version "6.4.1"
+ resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-6.4.1.tgz#0b34112d5b0748a8dcdbf51acf6f9bd42d50b8ca"
dependencies:
find-cache-dir "^0.1.1"
- loader-utils "^0.2.11"
+ loader-utils "^0.2.16"
mkdirp "^0.5.1"
object-assign "^4.0.1"
@@ -399,12 +405,12 @@ babel-plugin-check-es2015-constants@^6.22.0:
babel-runtime "^6.22.0"
babel-plugin-istanbul@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.0.0.tgz#36bde8fbef4837e5ff0366531a2beabd7b1ffa10"
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.1.tgz#c12de0fc6fe42adfb16be56f1ad11e4a9782eca9"
dependencies:
find-up "^2.1.0"
- istanbul-lib-instrument "^1.4.2"
- test-exclude "^4.0.0"
+ istanbul-lib-instrument "^1.6.2"
+ test-exclude "^4.0.3"
babel-plugin-syntax-async-functions@^6.8.0:
version "6.13.0"
@@ -745,11 +751,11 @@ babel-preset-stage-3@^6.22.0:
babel-plugin-transform-exponentiation-operator "^6.22.0"
babel-plugin-transform-object-rest-spread "^6.22.0"
-babel-register@^6.23.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.23.0.tgz#c9aa3d4cca94b51da34826c4a0f9e08145d74ff3"
+babel-register@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.24.0.tgz#5e89f8463ba9970356d02eb07dabe3308b080cfd"
dependencies:
- babel-core "^6.23.0"
+ babel-core "^6.24.0"
babel-runtime "^6.22.0"
core-js "^2.4.0"
home-or-tmp "^2.0.0"
@@ -758,8 +764,8 @@ babel-register@^6.23.0:
source-map-support "^0.4.2"
babel-runtime@^6.18.0, babel-runtime@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.22.0.tgz#1cf8b4ac67c77a4ddb0db2ae1f74de52ac4ca611"
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b"
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.10.0"
@@ -798,8 +804,8 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.22.0, babel-types@^6.23
to-fast-properties "^1.0.1"
babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0:
- version "6.15.0"
- resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e"
+ version "6.16.1"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3"
backo2@1.0.2:
version "1.0.2"
@@ -856,25 +862,25 @@ block-stream@*:
inherits "~2.0.0"
bluebird@^3.3.0:
- version "3.4.7"
- resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.6"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215"
-body-parser@^1.12.4:
- version "1.16.0"
- resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.16.0.tgz#924a5e472c6229fb9d69b85a20d5f2532dec788b"
+body-parser@^1.16.1:
+ version "1.17.1"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.1.tgz#75b3bc98ddd6e7e0d8ffe750dfaca5c66993fa47"
dependencies:
bytes "2.4.0"
content-type "~1.0.2"
- debug "2.6.0"
+ debug "2.6.1"
depd "~1.1.0"
- http-errors "~1.5.1"
+ http-errors "~1.6.1"
iconv-lite "0.4.15"
on-finished "~2.3.0"
- qs "6.2.1"
+ qs "6.4.0"
raw-body "~2.2.0"
type-is "~1.6.14"
@@ -885,8 +891,8 @@ boom@2.x.x:
hoek "2.x.x"
bootstrap-sass@^3.3.6:
- version "3.3.6"
- resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.6.tgz#363b0d300e868d3e70134c1a742bb17288444fd1"
+ version "3.3.7"
+ resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.7.tgz#6596c7ab40f6637393323ab0bc80d064fc630498"
brace-expansion@^1.0.0:
version "1.1.6"
@@ -910,8 +916,8 @@ braces@^1.8.2:
repeat-element "^1.1.2"
brorand@^1.0.1:
- version "1.0.7"
- resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.0.7.tgz#6677fa5e4901bdbf9c9ec2a748e28dca407a9bfc"
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
browserify-aes@^1.0.0, browserify-aes@^1.0.4:
version "1.0.6"
@@ -947,8 +953,8 @@ browserify-rsa@^4.0.0:
randombytes "^2.0.1"
browserify-sign@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.0.tgz#10773910c3c206d5420a46aad8694f820b85968f"
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298"
dependencies:
bn.js "^4.1.1"
browserify-rsa "^4.0.0"
@@ -1022,6 +1028,10 @@ caseless@~0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+
center-align@^0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
@@ -1145,10 +1155,10 @@ component-inherit@0.0.3:
resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
compressible@~2.0.8:
- version "2.0.9"
- resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.9.tgz#6daab4e2b599c2770dd9e21e7a891b1c5a755425"
+ version "2.0.10"
+ resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.10.tgz#feda1c7f7617912732b29bf8cf26252a20b9eecd"
dependencies:
- mime-db ">= 1.24.0 < 2"
+ mime-db ">= 1.27.0 < 2"
compression-webpack-plugin@^0.3.2:
version "0.3.2"
@@ -1182,7 +1192,7 @@ concat-stream@1.5.0:
readable-stream "~2.0.0"
typedarray "~0.0.5"
-concat-stream@^1.4.6:
+concat-stream@^1.5.2:
version "1.6.0"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
dependencies:
@@ -1194,12 +1204,12 @@ connect-history-api-fallback@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.3.0.tgz#e51d17f8f0ef0db90a64fdb47de3051556e9f169"
-connect@^3.3.5:
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/connect/-/connect-3.5.0.tgz#b357525a0b4c1f50599cd983e1d9efeea9677198"
+connect@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.0.tgz#f09a4f7dcd17324b663b725c815bdb1c4158a46e"
dependencies:
- debug "~2.2.0"
- finalhandler "0.5.0"
+ debug "2.6.1"
+ finalhandler "1.0.0"
parseurl "~1.3.1"
utils-merge "1.0.0"
@@ -1230,8 +1240,8 @@ content-type@~1.0.2:
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed"
convert-source-map@^1.1.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.3.0.tgz#e9f3e9c6e2728efc2676696a70eb382f73106a67"
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
cookie-signature@1.0.6:
version "1.0.6"
@@ -1297,19 +1307,23 @@ crypto-browserify@^3.11.0:
public-encrypt "^4.0.0"
randombytes "^2.0.0"
+custom-event-polyfill@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-0.3.0.tgz#99807839be62edb446b645832e0d80ead6fa1888"
+
custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
d3@^3.5.11:
- version "3.5.11"
- resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.11.tgz#d130750eed0554db70e8432102f920a12407b69c"
+ version "3.5.17"
+ resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8"
-d@^0.1.1, d@~0.1.1:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309"
+d@1:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
dependencies:
- es5-ext "~0.10.2"
+ es5-ext "^0.10.9"
dashdash@^1.12.0:
version "1.14.1"
@@ -1337,9 +1351,15 @@ debug@2.3.3:
dependencies:
ms "0.7.2"
-debug@2.6.0, debug@^2.1.1, debug@^2.2.0:
- version "2.6.0"
- resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b"
+debug@2.6.1:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.1.tgz#79855090ba2c4e3115cc7d8769491d58f0491351"
+ dependencies:
+ ms "0.7.2"
+
+debug@2.6.3, debug@^2.1.1, debug@^2.2.0:
+ version "2.6.3"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d"
dependencies:
ms "0.7.2"
@@ -1387,7 +1407,7 @@ delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
-depd@~1.1.0:
+depd@1.1.0, depd@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3"
@@ -1420,16 +1440,23 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
-doctrine@1.5.0, doctrine@^1.2.2:
+doctrine@1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
dependencies:
esutils "^2.0.2"
isarray "^1.0.0"
+doctrine@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63"
+ dependencies:
+ esutils "^2.0.2"
+ isarray "^1.0.0"
+
document-register-element@^1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/document-register-element/-/document-register-element-1.3.0.tgz#fb3babb523c74662be47be19c6bc33e71990d940"
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/document-register-element/-/document-register-element-1.4.1.tgz#22b41e96fb86cccab2fa30f7d2a8d62ac7be8c57"
dom-serialize@^2.2.0:
version "2.2.1"
@@ -1445,8 +1472,8 @@ domain-browser@^1.1.1:
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
dropzone@^4.2.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-4.2.0.tgz#fbe7acbb9918e0706489072ef663effeef8a79f3"
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-4.3.0.tgz#48b0b8f2ad092872e4b535b672a7c3f1a1d67c91"
duplexer@^0.1.1:
version "0.1.1"
@@ -1467,13 +1494,16 @@ ejs@^2.5.5:
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.6.tgz#479636bfa3fe3b1debd52087f0acb204b4f19c88"
elliptic@^6.0.0:
- version "6.3.3"
- resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.3.3.tgz#5482d9646d54bcb89fd7d994fc9e2e9568876e3f"
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
dependencies:
bn.js "^4.4.0"
brorand "^1.0.1"
hash.js "^1.0.0"
+ hmac-drbg "^1.0.0"
inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.0"
emoji-unicode-version@^0.2.1:
version "0.2.1"
@@ -1487,9 +1517,9 @@ encodeurl@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
-engine.io-client@1.8.2:
- version "1.8.2"
- resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.2.tgz#c38767547f2a7d184f5752f6f0ad501006703766"
+engine.io-client@1.8.3:
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.3.tgz#1798ed93451246453d4c6f635d7a201fe940d5ab"
dependencies:
component-emitter "1.2.1"
component-inherit "0.0.3"
@@ -1500,7 +1530,7 @@ engine.io-client@1.8.2:
parsejson "0.0.3"
parseqs "0.0.5"
parseuri "0.0.5"
- ws "1.1.1"
+ ws "1.1.2"
xmlhttprequest-ssl "1.5.3"
yeast "0.1.2"
@@ -1515,16 +1545,16 @@ engine.io-parser@1.3.2:
has-binary "0.1.7"
wtf-8 "1.0.0"
-engine.io@1.8.2:
- version "1.8.2"
- resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.2.tgz#6b59be730b348c0125b0a4589de1c355abcf7a7e"
+engine.io@1.8.3:
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.3.tgz#8de7f97895d20d39b85f88eeee777b2bd42b13d4"
dependencies:
accepts "1.3.3"
base64id "1.0.0"
cookie "0.3.1"
debug "2.3.3"
engine.io-parser "1.3.2"
- ws "1.1.1"
+ ws "1.1.2"
enhanced-resolve@^3.0.0:
version "3.1.0"
@@ -1554,36 +1584,36 @@ errno@^0.1.3:
prr "~0.0.0"
error-ex@^1.2.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.0.tgz#e67b43f3e82c96ea3a584ffee0b9fc3325d802d9"
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
dependencies:
is-arrayish "^0.2.1"
-es5-ext@^0.10.7, es5-ext@^0.10.8, es5-ext@~0.10.11, es5-ext@~0.10.2, es5-ext@~0.10.7:
- version "0.10.12"
- resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047"
+es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14:
+ version "0.10.15"
+ resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.15.tgz#c330a5934c1ee21284a7c081a86e5fd937c91ea6"
dependencies:
es6-iterator "2"
es6-symbol "~3.1"
-es6-iterator@2:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.0.tgz#bd968567d61635e33c0b80727613c9cb4b096bac"
+es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512"
dependencies:
- d "^0.1.1"
- es5-ext "^0.10.7"
- es6-symbol "3"
+ d "1"
+ es5-ext "^0.10.14"
+ es6-symbol "^3.1"
es6-map@^0.1.3:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.4.tgz#a34b147be224773a4d7da8072794cefa3632b897"
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0"
dependencies:
- d "~0.1.1"
- es5-ext "~0.10.11"
- es6-iterator "2"
- es6-set "~0.1.3"
- es6-symbol "~3.1.0"
- event-emitter "~0.3.4"
+ d "1"
+ es5-ext "~0.10.14"
+ es6-iterator "~2.0.1"
+ es6-set "~0.1.5"
+ es6-symbol "~3.1.1"
+ event-emitter "~0.3.5"
es6-promise@~3.0.2:
version "3.0.2"
@@ -1593,31 +1623,31 @@ es6-promise@~4.0.3:
version "4.0.5"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42"
-es6-set@~0.1.3:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.4.tgz#9516b6761c2964b92ff479456233a247dc707ce8"
+es6-set@~0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
dependencies:
- d "~0.1.1"
- es5-ext "~0.10.11"
- es6-iterator "2"
- es6-symbol "3"
- event-emitter "~0.3.4"
+ d "1"
+ es5-ext "~0.10.14"
+ es6-iterator "~2.0.1"
+ es6-symbol "3.1.1"
+ event-emitter "~0.3.5"
-es6-symbol@3, es6-symbol@~3.1, es6-symbol@~3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa"
+es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbol@~3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
dependencies:
- d "~0.1.1"
- es5-ext "~0.10.11"
+ d "1"
+ es5-ext "~0.10.14"
es6-weak-map@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.1.tgz#0d2bbd8827eb5fb4ba8f97fbfea50d43db21ea81"
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f"
dependencies:
- d "^0.1.1"
- es5-ext "^0.10.8"
- es6-iterator "2"
- es6-symbol "3"
+ d "1"
+ es5-ext "^0.10.14"
+ es6-iterator "^2.0.1"
+ es6-symbol "^3.1.1"
escape-html@~1.0.3:
version "1.0.3"
@@ -1710,16 +1740,17 @@ eslint-plugin-jasmine@^2.1.0:
resolved "https://registry.yarnpkg.com/eslint-plugin-jasmine/-/eslint-plugin-jasmine-2.2.0.tgz#7135879383c39a667c721d302b9f20f0389543de"
eslint@^3.10.1:
- version "3.15.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.15.0.tgz#bdcc6a6c5ffe08160e7b93c066695362a91e30f2"
+ version "3.19.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc"
dependencies:
babel-code-frame "^6.16.0"
chalk "^1.1.3"
- concat-stream "^1.4.6"
+ concat-stream "^1.5.2"
debug "^2.1.1"
- doctrine "^1.2.2"
+ doctrine "^2.0.0"
escope "^3.6.0"
espree "^3.4.0"
+ esquery "^1.0.0"
estraverse "^4.2.0"
esutils "^2.0.2"
file-entry-cache "^2.0.0"
@@ -1749,10 +1780,10 @@ eslint@^3.10.1:
user-home "^2.0.0"
espree@^3.4.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.0.tgz#41656fa5628e042878025ef467e78f125cb86e1d"
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.1.tgz#28a83ab4aaed71ed8fe0f5efe61b76a05c13c4d2"
dependencies:
- acorn "4.0.4"
+ acorn "^5.0.1"
acorn-jsx "^3.0.0"
esprima@2.7.x, esprima@^2.7.1:
@@ -1763,6 +1794,12 @@ esprima@^3.1.1:
version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+esquery@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
+ dependencies:
+ estraverse "^4.0.0"
+
esrecurse@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220"
@@ -1774,7 +1811,7 @@ estraverse@^1.9.1:
version "1.9.3"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44"
-estraverse@^4.1.1, estraverse@^4.2.0:
+estraverse@^4.0.0, estraverse@^4.1.1, estraverse@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
@@ -1786,20 +1823,20 @@ esutils@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
-etag@~1.7.0:
- version "1.7.0"
- resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8"
+etag@~1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051"
eve-raphael@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/eve-raphael/-/eve-raphael-0.5.0.tgz#17c754b792beef3fa6684d79cf5a47c63c4cda30"
-event-emitter@~0.3.4:
- version "0.3.4"
- resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.4.tgz#8d63ddfb4cfe1fae3b32ca265c4c720222080bb5"
+event-emitter@~0.3.5:
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
dependencies:
- d "~0.1.1"
- es5-ext "~0.10.7"
+ d "1"
+ es5-ext "~0.10.14"
eventemitter3@1.x.x:
version "1.2.0"
@@ -1809,7 +1846,7 @@ events@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
-eventsource@~0.1.6:
+eventsource@0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.1.6.tgz#0acede849ed7dd1ccc32c811bb11b944d4f29232"
dependencies:
@@ -1853,8 +1890,8 @@ expand-range@^1.8.1:
fill-range "^2.1.0"
express@^4.13.3, express@^4.14.1:
- version "4.14.1"
- resolved "https://registry.yarnpkg.com/express/-/express-4.14.1.tgz#646c237f766f148c2120aff073817b9e4d7e0d33"
+ version "4.15.2"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.15.2.tgz#af107fc148504457f2dca9a6f2571d7129b97b35"
dependencies:
accepts "~1.3.3"
array-flatten "1.1.1"
@@ -1862,23 +1899,25 @@ express@^4.13.3, express@^4.14.1:
content-type "~1.0.2"
cookie "0.3.1"
cookie-signature "1.0.6"
- debug "~2.2.0"
+ debug "2.6.1"
depd "~1.1.0"
encodeurl "~1.0.1"
escape-html "~1.0.3"
- etag "~1.7.0"
- finalhandler "0.5.1"
- fresh "0.3.0"
+ etag "~1.8.0"
+ finalhandler "~1.0.0"
+ fresh "0.5.0"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "~2.3.0"
parseurl "~1.3.1"
path-to-regexp "0.1.7"
proxy-addr "~1.1.3"
- qs "6.2.0"
+ qs "6.4.0"
range-parser "~1.2.0"
- send "0.14.2"
- serve-static "~1.11.2"
+ send "0.15.1"
+ serve-static "1.12.1"
+ setprototypeof "1.0.3"
+ statuses "~1.3.1"
type-is "~1.6.14"
utils-merge "1.0.0"
vary "~1.1.0"
@@ -1960,8 +1999,8 @@ fileset@^2.0.2:
minimatch "^3.0.3"
filesize@^3.5.4:
- version "3.5.4"
- resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.4.tgz#742fc7fb6aef4ee3878682600c22f840731e1fda"
+ version "3.5.6"
+ resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.6.tgz#5fd98f3eac94ec9516ef8ed5782fad84a01a0a1a"
fill-range@^2.1.0:
version "2.2.3"
@@ -1973,23 +2012,27 @@ fill-range@^2.1.0:
repeat-element "^1.1.2"
repeat-string "^1.5.2"
-finalhandler@0.5.0:
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.0.tgz#e9508abece9b6dba871a6942a1d7911b91911ac7"
+finalhandler@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.0.tgz#b5691c2c0912092f18ac23e9416bde5cd7dc6755"
dependencies:
- debug "~2.2.0"
+ debug "2.6.1"
+ encodeurl "~1.0.1"
escape-html "~1.0.3"
on-finished "~2.3.0"
- statuses "~1.3.0"
+ parseurl "~1.3.1"
+ statuses "~1.3.1"
unpipe "~1.0.0"
-finalhandler@0.5.1:
- version "0.5.1"
- resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.1.tgz#2c400d8d4530935bc232549c5fa385ec07de6fcd"
+finalhandler@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.1.tgz#bcd15d1689c0e5ed729b6f7f541a6df984117db8"
dependencies:
- debug "~2.2.0"
+ debug "2.6.3"
+ encodeurl "~1.0.1"
escape-html "~1.0.3"
on-finished "~2.3.0"
+ parseurl "~1.3.1"
statuses "~1.3.1"
unpipe "~1.0.0"
@@ -2027,15 +2070,15 @@ flat-cache@^1.2.1:
graceful-fs "^4.1.2"
write "^0.2.1"
-for-in@^0.1.5:
- version "0.1.6"
- resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8"
+for-in@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
for-own@^0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.4.tgz#0149b41a39088c7515f51ebe1c1386d45f935072"
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
dependencies:
- for-in "^0.1.5"
+ for-in "^1.0.1"
forever-agent@~0.6.1:
version "0.6.1"
@@ -2053,9 +2096,9 @@ forwarded@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363"
-fresh@0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f"
+fresh@0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e"
fs-extra@~1.0.0:
version "1.0.0"
@@ -2070,13 +2113,13 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
fsevents@^1.0.0:
- version "1.0.17"
- resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.0.17.tgz#8537f3f12272678765b4fd6528c0f1f66f8f4558"
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.1.tgz#f19fd28f43eeaf761680e519a203c4d0b3d31aff"
dependencies:
nan "^2.3.0"
node-pre-gyp "^0.6.29"
-fstream-ignore@~1.0.5:
+fstream-ignore@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
dependencies:
@@ -2084,9 +2127,9 @@ fstream-ignore@~1.0.5:
inherits "2"
minimatch "^3.0.0"
-fstream@^1.0.0, fstream@^1.0.2, fstream@~1.0.10:
- version "1.0.10"
- resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.10.tgz#604e8a92fe26ffd9f6fae30399d4984e1ab22822"
+fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
dependencies:
graceful-fs "^4.1.2"
inherits "~2.0.0"
@@ -2098,8 +2141,8 @@ function-bind@^1.0.2:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771"
gauge@~2.7.1:
- version "2.7.2"
- resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.2.tgz#15cecc31b02d05345a5d6b0e171cdb3ad2307774"
+ version "2.7.3"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.3.tgz#1c23855f962f17b3ad3d0dc7443f304542edfe09"
dependencies:
aproba "^1.0.3"
console-control-strings "^1.0.0"
@@ -2108,7 +2151,6 @@ gauge@~2.7.1:
signal-exit "^3.0.0"
string-width "^1.0.1"
strip-ansi "^3.0.1"
- supports-color "^0.2.0"
wide-align "^1.1.0"
generate-function@^2.0.0:
@@ -2166,8 +2208,8 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1:
path-is-absolute "^1.0.0"
globals@^9.0.0, globals@^9.14.0:
- version "9.14.0"
- resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034"
+ version "9.17.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-9.17.0.tgz#0c0ca696d9b9bb694d2e5470bd37777caad50286"
globby@^5.0.0:
version "5.0.0"
@@ -2208,6 +2250,10 @@ handlebars@^4.0.1, handlebars@^4.0.3:
optionalDependencies:
uglify-js "^2.6"
+har-schema@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
+
har-validator@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d"
@@ -2217,6 +2263,13 @@ har-validator@~2.0.6:
is-my-json-valid "^2.12.4"
pinkie-promise "^2.0.0"
+har-validator@~4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
+ dependencies:
+ ajv "^4.9.1"
+ har-schema "^1.0.5"
+
has-ansi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -2247,7 +2300,7 @@ has@^1.0.1:
dependencies:
function-bind "^1.0.2"
-hash.js@^1.0.0:
+hash.js@^1.0.0, hash.js@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.0.3.tgz#1332ff00156c0a0ffdd8236013d07b77a0451573"
dependencies:
@@ -2269,6 +2322,14 @@ hawk@~3.1.3:
hoek "2.x.x"
sntp "1.x.x"
+hmac-drbg@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.0.tgz#3db471f45aae4a994a0688322171f51b8b91bee5"
+ dependencies:
+ hash.js "^1.0.3"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.1"
+
hoek@2.x.x:
version "2.16.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
@@ -2281,8 +2342,8 @@ home-or-tmp@^2.0.0:
os-tmpdir "^1.0.1"
hosted-git-info@^2.1.4:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.2.0.tgz#7a0d097863d886c0fabbdcd37bf1758d8becf8a5"
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.4.1.tgz#4b0445e41c004a8bd1337773a4ff790ca40318c8"
hpack.js@^2.1.6:
version "2.1.6"
@@ -2301,7 +2362,7 @@ http-deceiver@^1.2.4:
version "1.2.7"
resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
-http-errors@~1.5.0, http-errors@~1.5.1:
+http-errors@~1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750"
dependencies:
@@ -2309,9 +2370,18 @@ http-errors@~1.5.0, http-errors@~1.5.1:
setprototypeof "1.0.2"
statuses ">= 1.3.1 < 2"
-http-proxy-middleware@~0.17.1:
- version "0.17.3"
- resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.3.tgz#940382147149b856084f5534752d5b5a8168cd1d"
+http-errors@~1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257"
+ dependencies:
+ depd "1.1.0"
+ inherits "2.0.3"
+ setprototypeof "1.0.3"
+ statuses ">= 1.3.1 < 2"
+
+http-proxy-middleware@~0.17.4:
+ version "0.17.4"
+ resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833"
dependencies:
http-proxy "^1.16.2"
is-glob "^3.1.0"
@@ -2346,8 +2416,8 @@ ieee754@^1.1.4:
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
ignore@^3.2.0:
- version "3.2.2"
- resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.2.tgz#1c51e1ef53bab6ddc15db4d9ac4ec139eceb3410"
+ version "3.2.6"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.6.tgz#26e8da0644be0bb4cb39516f6c79f0e0f4ffe48c"
immediate@~3.0.5:
version "3.0.6"
@@ -2399,8 +2469,8 @@ inquirer@^0.12.0:
through "^2.3.6"
interpret@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c"
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.2.tgz#f4f623f0bb7122f15f5717c8e254b8161b5c5b2d"
invariant@^2.2.0:
version "2.2.2"
@@ -2412,9 +2482,9 @@ invert-kv@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
-ipaddr.js@1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.2.0.tgz#8aba49c9192799585bdd643e0ccb50e8ae777ba4"
+ipaddr.js@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec"
is-absolute@^0.2.3:
version "0.2.6"
@@ -2434,8 +2504,8 @@ is-binary-path@^1.0.0:
binary-extensions "^1.0.0"
is-buffer@^1.0.2:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.4.tgz#cfc86ccd5dc5a52fa80489111c6920c457e2d98b"
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
is-builtin-module@^1.0.0:
version "1.0.0"
@@ -2494,8 +2564,8 @@ is-glob@^3.1.0:
is-extglob "^2.1.0"
is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4:
- version "2.15.0"
- resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz#936edda3ca3c211fd98f3b2d3e08da43f7b2915b"
+ version "2.16.0"
+ resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz#f079dd9bfdae65ee2038aae8acbc86ab109e3693"
dependencies:
generate-function "^2.0.0"
generate-object-property "^1.1.0"
@@ -2586,9 +2656,9 @@ isbinaryfile@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621"
-isexe@^1.1.1:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0"
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
isobject@^2.0.0:
version "2.1.0"
@@ -2601,66 +2671,64 @@ isstream@~0.1.2:
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
istanbul-api@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.1.tgz#d36e2f1560d1a43ce304c4ff7338182de61c8f73"
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.7.tgz#f6f37f09f8002b130f891c646b70ee4a8e7345ae"
dependencies:
async "^2.1.4"
fileset "^2.0.2"
- istanbul-lib-coverage "^1.0.0"
- istanbul-lib-hook "^1.0.0"
- istanbul-lib-instrument "^1.3.0"
- istanbul-lib-report "^1.0.0-alpha.3"
- istanbul-lib-source-maps "^1.1.0"
- istanbul-reports "^1.0.0"
+ istanbul-lib-coverage "^1.0.2"
+ istanbul-lib-hook "^1.0.5"
+ istanbul-lib-instrument "^1.7.0"
+ istanbul-lib-report "^1.0.0"
+ istanbul-lib-source-maps "^1.1.1"
+ istanbul-reports "^1.0.2"
js-yaml "^3.7.0"
mkdirp "^0.5.1"
once "^1.4.0"
-istanbul-lib-coverage@^1.0.0, istanbul-lib-coverage@^1.0.0-alpha, istanbul-lib-coverage@^1.0.0-alpha.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.1.tgz#f263efb519c051c5f1f3343034fc40e7b43ff212"
+istanbul-lib-coverage@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.2.tgz#87a0c015b6910651cb3b184814dfb339337e25e1"
-istanbul-lib-hook@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.0.tgz#fc5367ee27f59268e8f060b0c7aaf051d9c425c5"
+istanbul-lib-hook@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.5.tgz#6ca3d16d60c5f4082da39f7c5cd38ea8a772b88e"
dependencies:
append-transform "^0.4.0"
-istanbul-lib-instrument@^1.3.0, istanbul-lib-instrument@^1.4.2:
- version "1.4.2"
- resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.4.2.tgz#0e2fdfac93c1dabf2e31578637dc78a19089f43e"
+istanbul-lib-instrument@^1.6.2, istanbul-lib-instrument@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.0.tgz#b8e0dc25709bb44e17336ab47b7bb5c97c23f659"
dependencies:
babel-generator "^6.18.0"
babel-template "^6.16.0"
babel-traverse "^6.18.0"
babel-types "^6.18.0"
babylon "^6.13.0"
- istanbul-lib-coverage "^1.0.0"
+ istanbul-lib-coverage "^1.0.2"
semver "^5.3.0"
-istanbul-lib-report@^1.0.0-alpha.3:
- version "1.0.0-alpha.3"
- resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.0.0-alpha.3.tgz#32d5f6ec7f33ca3a602209e278b2e6ff143498af"
+istanbul-lib-report@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.0.0.tgz#d83dac7f26566b521585569367fe84ccfc7aaecb"
dependencies:
- async "^1.4.2"
- istanbul-lib-coverage "^1.0.0-alpha"
+ istanbul-lib-coverage "^1.0.2"
mkdirp "^0.5.1"
path-parse "^1.0.5"
- rimraf "^2.4.3"
supports-color "^3.1.2"
-istanbul-lib-source-maps@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.1.0.tgz#9d429218f35b823560ea300a96ff0c3bbdab785f"
+istanbul-lib-source-maps@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.1.1.tgz#f8c8c2e8f2160d1d91526d97e5bd63b2079af71c"
dependencies:
- istanbul-lib-coverage "^1.0.0-alpha.0"
+ istanbul-lib-coverage "^1.0.2"
mkdirp "^0.5.1"
rimraf "^2.4.4"
source-map "^0.5.3"
-istanbul-reports@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.0.1.tgz#9a17176bc4a6cbebdae52b2f15961d52fa623fbc"
+istanbul-reports@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.0.2.tgz#4e8366abe6fa746cc1cd6633f108de12cc6ac6fa"
dependencies:
handlebars "^4.0.3"
@@ -2698,33 +2766,33 @@ jodid25519@^1.0.0:
jsbn "~0.1.0"
jquery-ujs@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/jquery-ujs/-/jquery-ujs-1.2.1.tgz#6ee75b1ef4e9ac95e7124f8d71f7d351f5548e92"
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/jquery-ujs/-/jquery-ujs-1.2.2.tgz#6a8ef1020e6b6dda385b90a4bddc128c21c56397"
dependencies:
jquery ">=1.8.0"
jquery@>=1.8.0, jquery@^2.2.1:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.1.tgz#3c3e16854ad3d2ac44ac65021b17426d22ad803f"
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.4.tgz#2c89d6889b5eac522a7eea32c14521559c6cbf02"
js-cookie@^2.1.3:
- version "2.1.3"
- resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.1.3.tgz#48071625217ac9ecfab8c343a13d42ec09ff0526"
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.1.4.tgz#da4ec503866f149d164cf25f579ef31015025d8d"
js-tokens@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"
js-yaml@3.x, js-yaml@^3.5.1, js-yaml@^3.7.0:
- version "3.8.1"
- resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.1.tgz#782ba50200be7b9e5a8537001b7804db3ad02628"
+ version "3.8.3"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.3.tgz#33a05ec481c850c8875929166fe1beb61c728766"
dependencies:
argparse "^1.0.7"
esprima "^3.1.1"
jsbn@~0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.0.tgz#650987da0dd74f4ebf5a11377a2aa2d273e97dfd"
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
jsesc@^1.3.0:
version "1.3.0"
@@ -2775,9 +2843,10 @@ jsonpointer@^4.0.0:
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
jsprim@^1.2.2:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.3.1.tgz#2a7256f70412a29ee3670aaca625994c4dcff252"
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918"
dependencies:
+ assert-plus "1.0.0"
extsprintf "1.0.2"
json-schema "0.2.3"
verror "1.3.6"
@@ -2797,8 +2866,8 @@ jszip@^3.1.3:
readable-stream "~2.0.6"
karma-coverage-istanbul-reporter@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-0.2.0.tgz#5766263338adeb0026f7e4ac7a89a5f056c5642c"
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-0.2.3.tgz#11f1be9cfa93755a77bac39ab16e315a7100b5c5"
dependencies:
istanbul-api "^1.1.1"
@@ -2807,14 +2876,14 @@ karma-jasmine@^1.1.0:
resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-1.1.0.tgz#22e4c06bf9a182e5294d1f705e3733811b810acf"
karma-mocha-reporter@^2.2.2:
- version "2.2.2"
- resolved "https://registry.yarnpkg.com/karma-mocha-reporter/-/karma-mocha-reporter-2.2.2.tgz#876de9a287244e54a608591732a98e66611f6abe"
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/karma-mocha-reporter/-/karma-mocha-reporter-2.2.3.tgz#04fdda45a1d9697a73871c7472223c581701ab20"
dependencies:
chalk "1.1.3"
karma-phantomjs-launcher@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.2.tgz#19e1041498fd75563ed86730a22c1fe579fa8fb1"
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.4.tgz#d23ca34801bda9863ad318e3bb4bd4062b13acd2"
dependencies:
lodash "^4.0.1"
phantomjs-prebuilt "^2.1.7"
@@ -2826,8 +2895,8 @@ karma-sourcemap-loader@^0.3.7:
graceful-fs "^4.1.2"
karma-webpack@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.2.tgz#bd38350af5645c9644090770939ebe7ce726f864"
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.3.tgz#39cebf5ca2580139b27f9ae69b78816b9c82fae6"
dependencies:
async "~0.9.0"
loader-utils "^0.2.5"
@@ -2836,15 +2905,15 @@ karma-webpack@^2.0.2:
webpack-dev-middleware "^1.0.11"
karma@^1.4.1:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/karma/-/karma-1.4.1.tgz#41981a71d54237606b0a3ea8c58c90773f41650e"
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/karma/-/karma-1.6.0.tgz#0e871d4527d5eac56c41d181f03c5c0a7e6dbf3e"
dependencies:
bluebird "^3.3.0"
- body-parser "^1.12.4"
+ body-parser "^1.16.1"
chokidar "^1.4.1"
colors "^1.1.0"
combine-lists "^1.0.0"
- connect "^3.3.5"
+ connect "^3.6.0"
core-js "^2.2.0"
di "^0.0.1"
dom-serialize "^2.2.0"
@@ -2856,16 +2925,16 @@ karma@^1.4.1:
lodash "^3.8.0"
log4js "^0.6.31"
mime "^1.3.4"
- minimatch "^3.0.0"
+ minimatch "^3.0.2"
optimist "^0.6.1"
qjobs "^1.1.4"
range-parser "^1.2.0"
- rimraf "^2.3.3"
+ rimraf "^2.6.0"
safe-buffer "^5.0.1"
- socket.io "1.7.2"
+ socket.io "1.7.3"
source-map "^0.5.3"
- tmp "0.0.28"
- useragent "^2.1.10"
+ tmp "0.0.31"
+ useragent "^2.1.12"
kew@~0.7.0:
version "0.7.0"
@@ -2920,9 +2989,9 @@ loader-runner@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
-loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5:
- version "0.2.16"
- resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d"
+loader-utils@^0.2.16, loader-utils@^0.2.5:
+ version "0.2.17"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348"
dependencies:
big.js "^3.1.3"
emojis-list "^2.0.0"
@@ -3084,15 +3153,15 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
-"mime-db@>= 1.24.0 < 2", mime-db@~1.26.0:
- version "1.26.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.26.0.tgz#eaffcd0e4fc6935cf8134da246e2e6c35305adff"
+"mime-db@>= 1.27.0 < 2", mime-db@~1.27.0:
+ version "1.27.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1"
-mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7:
- version "2.1.14"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee"
+mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7:
+ version "2.1.15"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed"
dependencies:
- mime-db "~1.26.0"
+ mime-db "~1.27.0"
mime@1.3.4, mime@^1.3.4:
version "1.3.4"
@@ -3102,6 +3171,10 @@ minimalistic-assert@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
+minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
+
"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774"
@@ -3122,19 +3195,19 @@ mkdirp@0.5.0:
dependencies:
minimist "0.0.8"
-mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
dependencies:
minimist "0.0.8"
moment@2.x:
- version "2.17.1"
- resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82"
+ version "2.18.1"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
mousetrap@^1.4.6:
- version "1.4.6"
- resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.4.6.tgz#eaca72e22e56d5b769b7555873b688c3332e390a"
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.1.tgz#2a085f5c751294c75e7e81f6ec2545b29cbf42d9"
ms@0.7.1:
version "0.7.1"
@@ -3149,8 +3222,8 @@ mute-stream@0.0.5:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
nan@^2.0.0, nan@^2.3.0:
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2"
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.1.tgz#8c84f7b14c96b89f57fbc838012180ec8ca39a01"
natural-compare@^1.4.0:
version "1.4.0"
@@ -3217,18 +3290,18 @@ node-libs-browser@^2.0.0:
vm-browserify "0.0.4"
node-pre-gyp@^0.6.29, node-pre-gyp@^0.6.4:
- version "0.6.33"
- resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.33.tgz#640ac55198f6a925972e0c16c4ac26a034d5ecc9"
- dependencies:
- mkdirp "~0.5.1"
- nopt "~3.0.6"
- npmlog "^4.0.1"
- rc "~1.1.6"
- request "^2.79.0"
- rimraf "~2.5.4"
- semver "~5.3.0"
- tar "~2.2.1"
- tar-pack "~3.3.0"
+ version "0.6.34"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.34.tgz#94ad1c798a11d7fc67381b50d47f8cc18d9799f7"
+ dependencies:
+ mkdirp "^0.5.1"
+ nopt "^4.0.1"
+ npmlog "^4.0.2"
+ rc "^1.1.7"
+ request "^2.81.0"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^2.2.1"
+ tar-pack "^3.4.0"
node-zopfli@^2.0.0:
version "2.0.2"
@@ -3239,15 +3312,22 @@ node-zopfli@^2.0.0:
nan "^2.0.0"
node-pre-gyp "^0.6.4"
-nopt@3.x, nopt@~3.0.6:
+nopt@3.x:
version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
dependencies:
abbrev "1"
+nopt@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
normalize-package-data@^2.3.2:
- version "2.3.5"
- resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.5.tgz#8d924f142960e1777e7ffe170543631cc7cb02df"
+ version "2.3.6"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.6.tgz#498fa420c96401f787402ba21e600def9f981fff"
dependencies:
hosted-git-info "^2.1.4"
is-builtin-module "^1.0.0"
@@ -3255,10 +3335,12 @@ normalize-package-data@^2.3.2:
validate-npm-package-license "^3.0.1"
normalize-path@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a"
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ dependencies:
+ remove-trailing-separator "^1.0.1"
-npmlog@^4.0.1:
+npmlog@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.2.tgz#d03950e0e78ce1527ba26d2a7592e9348ac3e75f"
dependencies:
@@ -3308,18 +3390,12 @@ on-headers@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
-once@1.x, once@^1.3.0, once@^1.4.0:
+once@1.x, once@^1.3.0, once@^1.3.3, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
dependencies:
wrappy "1"
-once@~1.3.3:
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20"
- dependencies:
- wrappy "1"
-
onetime@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
@@ -3377,10 +3453,17 @@ os-locale@^1.4.0:
dependencies:
lcid "^1.0.0"
-os-tmpdir@^1.0.1, os-tmpdir@~1.0.1:
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+osenv@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
p-limit@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc"
@@ -3400,8 +3483,8 @@ pako@~1.0.2:
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.5.tgz#d2205dfe5b9da8af797e7c163db4d1f84e4600bc"
parse-asn1@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.0.0.tgz#35060f6d5015d37628c770f4e091a0b5a278bc23"
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712"
dependencies:
asn1.js "^4.0.0"
browserify-aes "^1.0.0"
@@ -3494,6 +3577,10 @@ pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
+performance-now@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
+
phantomjs-prebuilt@^2.1.7:
version "2.1.14"
resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.14.tgz#d53d311fcfb7d1d08ddb24014558f1188c516da0"
@@ -3577,11 +3664,11 @@ progress@^1.1.8, progress@~1.1.8:
resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
proxy-addr@~1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.3.tgz#dc97502f5722e888467b3fa2297a7b1ff47df074"
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3"
dependencies:
forwarded "~0.1.0"
- ipaddr.js "1.2.0"
+ ipaddr.js "1.3.0"
prr@~0.0.0:
version "0.0.0"
@@ -3609,17 +3696,13 @@ qjobs@^1.1.4:
version "1.1.5"
resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73"
-qs@6.2.0:
- version "6.2.0"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b"
-
-qs@6.2.1:
- version "6.2.1"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.1.tgz#ce03c5ff0935bc1d9d69a9f14cbd18e568d67625"
+qs@6.4.0, qs@~6.4.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
qs@~6.3.0:
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
+ version "6.3.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c"
querystring-es3@^0.2.0:
version "0.2.1"
@@ -3666,14 +3749,14 @@ raw-loader@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa"
-rc@~1.1.6:
- version "1.1.6"
- resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.6.tgz#43651b76b6ae53b5c802f1151fa3fc3b059969c9"
+rc@^1.1.7:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
dependencies:
deep-extend "~0.4.0"
ini "~1.3.0"
minimist "^1.2.0"
- strip-json-comments "~1.0.4"
+ strip-json-comments "~2.0.1"
read-pkg-up@^1.0.1:
version "1.0.1"
@@ -3690,9 +3773,9 @@ read-pkg@^1.0.0:
normalize-package-data "^2.3.2"
path-type "^1.0.0"
-"readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.0, readable-stream@^2.2.2:
- version "2.2.2"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e"
+"readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6:
+ version "2.2.6"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.6.tgz#8b43aed76e71483938d12a8d46c6cf1a00b1f816"
dependencies:
buffer-shims "^1.0.0"
core-util-is "~1.0.0"
@@ -3702,16 +3785,7 @@ read-pkg@^1.0.0:
string_decoder "~0.10.x"
util-deprecate "~1.0.1"
-readable-stream@~1.0.2:
- version "1.0.34"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
- dependencies:
- core-util-is "~1.0.0"
- inherits "~2.0.1"
- isarray "0.0.1"
- string_decoder "~0.10.x"
-
-readable-stream@~2.0.0, readable-stream@~2.0.6:
+readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@~2.0.0, readable-stream@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
dependencies:
@@ -3722,17 +3796,14 @@ readable-stream@~2.0.0, readable-stream@~2.0.6:
string_decoder "~0.10.x"
util-deprecate "~1.0.1"
-readable-stream@~2.1.4:
- version "2.1.5"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0"
+readable-stream@~1.0.2:
+ version "1.0.34"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
dependencies:
- buffer-shims "^1.0.0"
core-util-is "~1.0.0"
inherits "~2.0.1"
- isarray "~1.0.0"
- process-nextick-args "~1.0.6"
+ isarray "0.0.1"
string_decoder "~0.10.x"
- util-deprecate "~1.0.1"
readdirp@^2.0.0:
version "2.1.0"
@@ -3762,8 +3833,8 @@ regenerate@^1.2.1:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260"
regenerator-runtime@^0.10.0:
- version "0.10.1"
- resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.1.tgz#257f41961ce44558b18f7814af48c17559f9faeb"
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.3.tgz#8c4367a904b51ea62a908ac310bf99ff90a82a3e"
regenerator-transform@0.9.8:
version "0.9.8"
@@ -3798,6 +3869,10 @@ regjsparser@^0.1.4:
dependencies:
jsesc "~0.5.0"
+remove-trailing-separator@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.1.tgz#615ebb96af559552d4bf4057c8436d486ab63cc4"
+
repeat-element@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
@@ -3822,7 +3897,34 @@ request-progress@~2.0.1:
dependencies:
throttleit "^1.0.0"
-request@^2.79.0, request@~2.79.0:
+request@^2.81.0:
+ version "2.81.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
+ dependencies:
+ aws-sign2 "~0.6.0"
+ aws4 "^1.2.1"
+ caseless "~0.12.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.0"
+ forever-agent "~0.6.1"
+ form-data "~2.1.1"
+ har-validator "~4.2.1"
+ hawk "~3.1.3"
+ http-signature "~1.1.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.7"
+ oauth-sign "~0.8.1"
+ performance-now "^0.2.0"
+ qs "~6.4.0"
+ safe-buffer "^5.0.1"
+ stringstream "~0.0.4"
+ tough-cookie "~2.3.0"
+ tunnel-agent "^0.6.0"
+ uuid "^3.0.0"
+
+request@~2.79.0:
version "2.79.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
dependencies:
@@ -3875,8 +3977,10 @@ resolve@1.1.x:
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
resolve@^1.1.6, resolve@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.2.0.tgz#9589c3f2f6149d1417a40becc1663db6ec6bc26c"
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.2.tgz#1f0442c9e0cbb8136e87b9305f932f46c7f28235"
+ dependencies:
+ path-parse "^1.0.5"
restore-cursor@^1.0.1:
version "1.0.1"
@@ -3891,9 +3995,9 @@ right-align@^0.1.1:
dependencies:
align-text "^0.1.1"
-rimraf@2, rimraf@^2.2.8, rimraf@^2.3.3, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@~2.5.1, rimraf@~2.5.4:
- version "2.5.4"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04"
+rimraf@2, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.6.0, rimraf@^2.6.1:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d"
dependencies:
glob "^7.0.5"
@@ -3923,7 +4027,7 @@ select2@3.5.2-browserify:
version "3.5.2-browserify"
resolved "https://registry.yarnpkg.com/select2/-/select2-3.5.2-browserify.tgz#dc4dafda38d67a734e8a97a46f0d3529ae05391d"
-"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@~5.3.0:
+"semver@2 || 3 || 4 || 5", semver@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
@@ -3931,18 +4035,18 @@ semver@~4.3.3:
version "4.3.6"
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
-send@0.14.2:
- version "0.14.2"
- resolved "https://registry.yarnpkg.com/send/-/send-0.14.2.tgz#39b0438b3f510be5dc6f667a11f71689368cdeef"
+send@0.15.1:
+ version "0.15.1"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.15.1.tgz#8a02354c26e6f5cca700065f5f0cdeba90ec7b5f"
dependencies:
- debug "~2.2.0"
+ debug "2.6.1"
depd "~1.1.0"
destroy "~1.0.4"
encodeurl "~1.0.1"
escape-html "~1.0.3"
- etag "~1.7.0"
- fresh "0.3.0"
- http-errors "~1.5.1"
+ etag "~1.8.0"
+ fresh "0.5.0"
+ http-errors "~1.6.1"
mime "1.3.4"
ms "0.7.2"
on-finished "~2.3.0"
@@ -3961,14 +4065,14 @@ serve-index@^1.7.2:
mime-types "~2.1.11"
parseurl "~1.3.1"
-serve-static@~1.11.2:
- version "1.11.2"
- resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.11.2.tgz#2cf9889bd4435a320cc36895c9aa57bd662e6ac7"
+serve-static@1.12.1:
+ version "1.12.1"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.1.tgz#7443a965e3ced647aceb5639fa06bf4d1bbe0039"
dependencies:
encodeurl "~1.0.1"
escape-html "~1.0.3"
parseurl "~1.3.1"
- send "0.14.2"
+ send "0.15.1"
set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
@@ -3986,6 +4090,10 @@ setprototypeof@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08"
+setprototypeof@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
+
sha.js@^2.3.6:
version "2.4.8"
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.8.tgz#37068c2c476b6baf402d14a49c67f597921f634f"
@@ -3993,8 +4101,8 @@ sha.js@^2.3.6:
inherits "^2.0.1"
shelljs@^0.7.5:
- version "0.7.6"
- resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.6.tgz#379cccfb56b91c8601e4793356eb5382924de9ad"
+ version "0.7.7"
+ resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.7.tgz#b2f5c77ef97148f4b4f6e22682e10bba8667cff1"
dependencies:
glob "^7.0.0"
interpret "^1.0.0"
@@ -4025,15 +4133,15 @@ socket.io-adapter@0.5.0:
debug "2.3.3"
socket.io-parser "2.3.1"
-socket.io-client@1.7.2:
- version "1.7.2"
- resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.7.2.tgz#39fdb0c3dd450e321b7e40cfd83612ec533dd644"
+socket.io-client@1.7.3:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.7.3.tgz#b30e86aa10d5ef3546601c09cde4765e381da377"
dependencies:
backo2 "1.0.2"
component-bind "1.0.0"
component-emitter "1.2.1"
debug "2.3.3"
- engine.io-client "1.8.2"
+ engine.io-client "1.8.3"
has-binary "0.1.7"
indexof "0.0.1"
object-component "0.0.3"
@@ -4050,24 +4158,24 @@ socket.io-parser@2.3.1:
isarray "0.0.1"
json3 "3.3.2"
-socket.io@1.7.2:
- version "1.7.2"
- resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.7.2.tgz#83bbbdf2e79263b378900da403e7843e05dc3b71"
+socket.io@1.7.3:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.7.3.tgz#b8af9caba00949e568e369f1327ea9be9ea2461b"
dependencies:
debug "2.3.3"
- engine.io "1.8.2"
+ engine.io "1.8.3"
has-binary "0.1.7"
object-assign "4.1.0"
socket.io-adapter "0.5.0"
- socket.io-client "1.7.2"
+ socket.io-client "1.7.3"
socket.io-parser "2.3.1"
-sockjs-client@1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.1.tgz#284843e9a9784d7c474b1571b3240fca9dda4bb0"
+sockjs-client@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.2.tgz#f0212a8550e4c9468c8cceaeefd2e3493c033ad5"
dependencies:
debug "^2.2.0"
- eventsource "~0.1.6"
+ eventsource "0.1.6"
faye-websocket "~0.11.0"
inherits "^2.0.1"
json3 "^3.3.2"
@@ -4080,15 +4188,19 @@ sockjs@0.3.18:
faye-websocket "^0.10.0"
uuid "^2.0.2"
+source-list-map@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-1.1.1.tgz#1a33ac210ca144d1e561f906ebccab5669ff4cb4"
+
source-list-map@~0.1.7:
version "0.1.8"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106"
source-map-support@^0.4.2:
- version "0.4.11"
- resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.11.tgz#647f939978b38535909530885303daf23279f322"
+ version "0.4.14"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.14.tgz#9d4463772598b86271b4f523f6c1f4e02a7d6aef"
dependencies:
- source-map "^0.5.3"
+ source-map "^0.5.6"
source-map@^0.1.41:
version "0.1.43"
@@ -4102,7 +4214,7 @@ source-map@^0.4.4:
dependencies:
amdefine ">=0.0.4"
-source-map@^0.5.0, source-map@^0.5.3, source-map@~0.5.1, source-map@~0.5.3:
+source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3:
version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
@@ -4151,8 +4263,8 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
sshpk@^1.7.0:
- version "1.10.2"
- resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.10.2.tgz#d5a804ce22695515638e798dbe23273de070a5fa"
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.11.0.tgz#2d8d5ebb4a6fab28ffba37fa62a90f4a3ea59d77"
dependencies:
asn1 "~0.2.3"
assert-plus "^1.0.0"
@@ -4169,7 +4281,7 @@ stats-webpack-plugin@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/stats-webpack-plugin/-/stats-webpack-plugin-0.4.3.tgz#b2f618202f28dd04ab47d7ecf54ab846137b7aea"
-"statuses@>= 1.3.1 < 2", statuses@~1.3.0, statuses@~1.3.1:
+"statuses@>= 1.3.1 < 2", statuses@~1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
@@ -4181,12 +4293,12 @@ stream-browserify@^2.0.1:
readable-stream "^2.0.2"
stream-http@^2.3.1:
- version "2.6.3"
- resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.6.3.tgz#4c3ddbf9635968ea2cfd4e48d43de5def2625ac3"
+ version "2.7.0"
+ resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.0.tgz#cec1f4e3b494bc4a81b451808970f8b20b4ed5f6"
dependencies:
builtin-status-codes "^3.0.0"
inherits "^2.0.1"
- readable-stream "^2.1.0"
+ readable-stream "^2.2.6"
to-arraybuffer "^1.0.0"
xtend "^4.0.0"
@@ -4229,18 +4341,10 @@ strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
-strip-json-comments@~1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
-
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
-supports-color@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a"
-
supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@@ -4270,20 +4374,20 @@ tapable@^0.2.5, tapable@~0.2.5:
version "0.2.6"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.6.tgz#206be8e188860b514425375e6f1ae89bfb01fd8d"
-tar-pack@~3.3.0:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.3.0.tgz#30931816418f55afc4d21775afdd6720cee45dae"
+tar-pack@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984"
dependencies:
- debug "~2.2.0"
- fstream "~1.0.10"
- fstream-ignore "~1.0.5"
- once "~1.3.3"
- readable-stream "~2.1.4"
- rimraf "~2.5.1"
- tar "~2.2.1"
- uid-number "~0.0.6"
-
-tar@~2.2.1:
+ debug "^2.2.0"
+ fstream "^1.0.10"
+ fstream-ignore "^1.0.5"
+ once "^1.3.3"
+ readable-stream "^2.1.4"
+ rimraf "^2.5.1"
+ tar "^2.2.1"
+ uid-number "^0.0.6"
+
+tar@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
dependencies:
@@ -4291,9 +4395,9 @@ tar@~2.2.1:
fstream "^1.0.2"
inherits "2"
-test-exclude@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.0.0.tgz#0ddc0100b8ae7e88b34eb4fd98a907e961991900"
+test-exclude@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.0.3.tgz#86a13ce3effcc60e6c90403cf31a27a60ac6c4e7"
dependencies:
arrify "^1.0.1"
micromatch "^2.3.11"
@@ -4341,9 +4445,9 @@ timers-browserify@^2.0.2:
dependencies:
setimmediate "^1.0.4"
-tmp@0.0.28, tmp@0.0.x:
- version "0.0.28"
- resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120"
+tmp@0.0.31, tmp@0.0.x:
+ version "0.0.31"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"
dependencies:
os-tmpdir "~1.0.1"
@@ -4381,6 +4485,12 @@ tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ dependencies:
+ safe-buffer "^5.0.1"
+
tunnel-agent@~0.4.1:
version "0.4.3"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb"
@@ -4396,30 +4506,30 @@ type-check@~0.3.2:
prelude-ls "~1.1.2"
type-is@~1.6.14:
- version "1.6.14"
- resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2"
+ version "1.6.15"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410"
dependencies:
media-typer "0.3.0"
- mime-types "~2.1.13"
+ mime-types "~2.1.15"
typedarray@^0.0.6, typedarray@~0.0.5:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
-uglify-js@^2.6, uglify-js@^2.7.5:
- version "2.7.5"
- resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.5.tgz#4612c0c7baaee2ba7c487de4904ae122079f2ca8"
+uglify-js@^2.6, uglify-js@^2.8.5:
+ version "2.8.21"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.21.tgz#1733f669ae6f82fc90c7b25ec0f5c783ee375314"
dependencies:
- async "~0.2.6"
source-map "~0.5.1"
- uglify-to-browserify "~1.0.0"
yargs "~3.10.0"
+ optionalDependencies:
+ uglify-to-browserify "~1.0.0"
uglify-to-browserify@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
-uid-number@~0.0.6:
+uid-number@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
@@ -4447,8 +4557,8 @@ url-parse@1.0.x:
requires-port "1.0.x"
url-parse@^1.1.1:
- version "1.1.7"
- resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.7.tgz#025cff999653a459ab34232147d89514cc87d74a"
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.8.tgz#7a65b3a8d57a1e86af6b4e2276e34774167c0156"
dependencies:
querystringify "0.0.x"
requires-port "1.0.x"
@@ -4466,9 +4576,9 @@ user-home@^2.0.0:
dependencies:
os-homedir "^1.0.0"
-useragent@^2.1.10:
- version "2.1.12"
- resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.1.12.tgz#aa7da6cdc48bdc37ba86790871a7321d64edbaa2"
+useragent@^2.1.12:
+ version "2.1.13"
+ resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.1.13.tgz#bba43e8aa24d5ceb83c2937473e102e21df74c10"
dependencies:
lru-cache "2.2.x"
tmp "0.0.x"
@@ -4503,8 +4613,8 @@ validate-npm-package-license@^3.0.1:
spdx-expression-parse "~1.0.0"
vary@~1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140"
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37"
verror@1.3.6:
version "1.3.6"
@@ -4531,12 +4641,12 @@ vue-resource@^0.9.3:
resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-0.9.3.tgz#ab46e1c44ea219142dcc28ae4043b3b04c80959d"
vue@^2.2.4:
- version "2.2.4"
- resolved "https://registry.yarnpkg.com/vue/-/vue-2.2.4.tgz#d0a3a050a80a12356d7950ae5a7b3131048209cc"
+ version "2.2.6"
+ resolved "https://registry.yarnpkg.com/vue/-/vue-2.2.6.tgz#451714b394dd6d4eae7b773c40c2034a59621aed"
-watchpack@^1.2.0:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.2.1.tgz#01efa80c5c29e5c56ba55d6f5470a35b6402f0b2"
+watchpack@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.3.1.tgz#7d8693907b28ce6013e7f3610aa2a1acf07dad87"
dependencies:
async "^2.1.2"
chokidar "^1.4.3"
@@ -4549,8 +4659,8 @@ wbuf@^1.1.0, wbuf@^1.4.0:
minimalistic-assert "^1.0.0"
webpack-bundle-analyzer@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.3.0.tgz#0d05e96a43033f7cc57f6855b725782ba61e93a4"
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.3.1.tgz#d97f8aadbcce68fc865c5787741d8549359a25cd"
dependencies:
acorn "^4.0.11"
chalk "^1.1.3"
@@ -4564,8 +4674,8 @@ webpack-bundle-analyzer@^2.3.0:
opener "^1.4.2"
webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.9.0:
- version "1.10.0"
- resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.10.0.tgz#7d5be2651e692fddfafd8aaed177c16ff51f0eb8"
+ version "1.10.1"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.10.1.tgz#c6b4cf428139cf1aefbe06a0c00fdb4f8da2f893"
dependencies:
memory-fs "~0.4.1"
mime "^1.3.4"
@@ -4573,8 +4683,8 @@ webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.9.0:
range-parser "^1.0.3"
webpack-dev-server@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.3.0.tgz#0437704bbd4d941a6e4c061eb3cc232ed7d06101"
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.4.2.tgz#cf595d6b40878452b6d2ad7229056b686f8a16be"
dependencies:
ansi-html "0.0.7"
chokidar "^1.6.0"
@@ -4582,28 +4692,35 @@ webpack-dev-server@^2.3.0:
connect-history-api-fallback "^1.3.0"
express "^4.13.3"
html-entities "^1.2.0"
- http-proxy-middleware "~0.17.1"
+ http-proxy-middleware "~0.17.4"
opn "4.0.2"
portfinder "^1.0.9"
serve-index "^1.7.2"
sockjs "0.3.18"
- sockjs-client "1.1.1"
+ sockjs-client "1.1.2"
spdy "^3.4.1"
strip-ansi "^3.0.0"
supports-color "^3.1.1"
webpack-dev-middleware "^1.9.0"
yargs "^6.0.0"
-webpack-sources@^0.1.0, webpack-sources@^0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.1.4.tgz#ccc2c817e08e5fa393239412690bb481821393cd"
+webpack-sources@^0.1.0:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.1.5.tgz#aa1f3abf0f0d74db7111c40e500b84f966640750"
dependencies:
source-list-map "~0.1.7"
source-map "~0.5.3"
+webpack-sources@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.2.3.tgz#17c62bfaf13c707f9d02c479e0dcdde8380697fb"
+ dependencies:
+ source-list-map "^1.1.1"
+ source-map "~0.5.3"
+
webpack@^2.2.1:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.2.1.tgz#7bb1d72ae2087dd1a4af526afec15eed17dda475"
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.3.3.tgz#eecc083c18fb7bf958ea4f40b57a6640c5a0cc78"
dependencies:
acorn "^4.0.4"
acorn-dynamic-import "^2.0.0"
@@ -4621,9 +4738,9 @@ webpack@^2.2.1:
source-map "^0.5.3"
supports-color "^3.1.0"
tapable "~0.2.5"
- uglify-js "^2.7.5"
- watchpack "^1.2.0"
- webpack-sources "^0.1.4"
+ uglify-js "^2.8.5"
+ watchpack "^1.3.1"
+ webpack-sources "^0.2.3"
yargs "^6.0.0"
websocket-driver@>=0.5.1:
@@ -4641,10 +4758,10 @@ which-module@^1.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
which@^1.1.1, which@~1.2.10:
- version "1.2.12"
- resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192"
+ version "1.2.14"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5"
dependencies:
- isexe "^1.1.1"
+ isexe "^2.0.0"
wide-align@^1.1.0:
version "1.1.0"
@@ -4685,9 +4802,9 @@ write@^0.2.1:
dependencies:
mkdirp "^0.5.1"
-ws@1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.1.tgz#082ddb6c641e85d4bb451f03d52f06eabdb1f018"
+ws@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.2.tgz#8a244fa052401e08c9886cf44a85189e1fd4067f"
dependencies:
options ">=0.0.5"
ultron "1.0.x"