From 7a4b9d1d27c5f5d0aeb669640cb1b2720e9553c1 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Wed, 12 Oct 2016 16:53:51 +0200 Subject: [PATCH 01/26] Convert GfmAutoComplete into Factory, to track atwho state. --- .../javascripts/gfm_auto_complete.js.es6 | 429 +++++++++++------- .../javascripts/issuable_resource.js.es6 | 94 ++++ app/views/shared/issuable/_sidebar.html.haml | 1 + 3 files changed, 353 insertions(+), 171 deletions(-) create mode 100644 app/assets/javascripts/issuable_resource.js.es6 diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 845313b6b38e..7a29840be719 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -1,120 +1,60 @@ // Creates the variables for setting up GFM auto-completion -(function() { +(() => { if (window.GitLab == null) { window.GitLab = {}; } - GitLab.GfmAutoComplete = { - dataLoading: false, - dataLoaded: false, - cachedData: {}, - dataSource: '', - // Emoji - Emoji: { - template: '
  • ${name} ${name}
  • ' - }, - // Team Members - Members: { - template: '
  • ${username} ${title}
  • ' - }, - Labels: { - template: '
  • ${title}
  • ' - }, - // Issues and MergeRequests - Issues: { - template: '
  • ${id} ${title}
  • ' - }, - // Milestones - Milestones: { - template: '
  • ${title}
  • ' - }, - Loading: { - template: '
  • Loading...
  • ' - }, - DefaultOptions: { - sorter: function(query, items, searchKey) { - if ((items[0].name != null) && items[0].name === 'loading') { - return items; - } - return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); - }, - filter: function(query, data, searchKey) { - if (data[0] === 'loading') { - return data; - } - return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); - }, - beforeInsert: function(value) { - if (!GitLab.GfmAutoComplete.dataLoaded) { - return this.at; - } else { - return value; - } - } - }, - setup: _.debounce(function(input) { - // Add GFM auto-completion to all input fields, that accept GFM input. - this.input = input || $('.js-gfm-input'); - // destroy previous instances + class GfmInputor { + constructor(elem) { + this.gfm = GitLab.GfmAutoComplete; + this.elem = $(elem); + this.setup(); + } + + setup() { this.destroyAtWho(); - // set up instances this.setupAtWho(); + this.loadData(); + } - if (this.dataSource && !this.dataLoading && !this.cachedData) { - this.dataLoading = true; - return this.fetchData(this.dataSource) - .done((data) => { - this.dataLoading = false; - this.loadData(data); - }); - }; - - if (this.cachedData != null) { - return this.loadData(this.cachedData); - } - }, 1000), - setupAtWho: function() { - // Emoji - this.input.atwho({ + setupAtWho() { + // Emoji + this.elem.atwho({ at: ':', - displayTpl: (function(_this) { - return function(value) { - if (value.path != null) { - return _this.Emoji.template; - } else { - return _this.Loading.template; - } - }; - })(this), + displayTpl: (value) => { + if (value.path != null) { + return this.gfm.Emoji.template; + } else { + return this.gfm.Loading.template; + } + }, insertTpl: ':${name}:', data: ['loading'], callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert + sorter: this.gfm.DefaultOptions.sorter, + filter: this.gfm.DefaultOptions.filter, + beforeInsert: this.gfm.DefaultOptions.beforeInsert } }); // Team Members - this.input.atwho({ + this.elem.atwho({ at: '@', - displayTpl: (function(_this) { - return function(value) { - if (value.username != null) { - return _this.Members.template; - } else { - return _this.Loading.template; - } - }; - })(this), + displayTpl: (value) => { + if (value.username != null) { + return this.gfm.Members.template; + } else { + return this.gfm.Loading.template; + } + }, insertTpl: '${atwho-at}${username}', searchKey: 'search', data: ['loading'], callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - beforeSave: function(members) { - return $.map(members, function(m) { + sorter: this.gfm.DefaultOptions.sorter, + filter: this.gfm.DefaultOptions.filter, + beforeInsert: this.gfm.DefaultOptions.beforeInsert, + beforeSave: (members) => { + return $.map(members, (m) => { var title; if (m.username == null) { return m; @@ -132,27 +72,25 @@ } } }); - this.input.atwho({ + this.elem.atwho({ at: '#', alias: 'issues', searchKey: 'search', - displayTpl: (function(_this) { - return function(value) { - if (value.title != null) { - return _this.Issues.template; - } else { - return _this.Loading.template; - } - }; - })(this), + displayTpl: (value) => { + if (value.title != null) { + return this.gfm.Issues.template; + } else { + return this.gfm.Loading.template; + } + }, data: ['loading'], insertTpl: '${atwho-at}${id}', callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - beforeSave: function(issues) { - return $.map(issues, function(i) { + sorter: this.gfm.DefaultOptions.sorter, + filter: this.gfm.DefaultOptions.filter, + beforeInsert: this.gfm.DefaultOptions.beforeInsert, + beforeSave: (issues) => { + return $.map(issues, (i) => { if (i.title == null) { return i; } @@ -165,24 +103,22 @@ } } }); - this.input.atwho({ + this.elem.atwho({ at: '%', alias: 'milestones', searchKey: 'search', - displayTpl: (function(_this) { - return function(value) { - if (value.title != null) { - return _this.Milestones.template; - } else { - return _this.Loading.template; - } - }; - })(this), + displayTpl: (value) => { + if (value.title != null) { + return this.gfm.Milestones.template; + } else { + return this.gfm.Loading.template; + } + }, insertTpl: '${atwho-at}"${title}"', data: ['loading'], callbacks: { - beforeSave: function(milestones) { - return $.map(milestones, function(m) { + beforeSave: (milestones) => { + return $.map(milestones, (m) => { if (m.title == null) { return m; } @@ -195,27 +131,25 @@ } } }); - this.input.atwho({ + this.elem.atwho({ at: '!', alias: 'mergerequests', searchKey: 'search', - displayTpl: (function(_this) { - return function(value) { - if (value.title != null) { - return _this.Issues.template; - } else { - return _this.Loading.template; - } - }; - })(this), + displayTpl: (value) => { + if (value.title != null) { + return this.gfm.Issues.template; + } else { + return this.gfm.Loading.template; + } + }, data: ['loading'], insertTpl: '${atwho-at}${id}', callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - beforeSave: function(merges) { - return $.map(merges, function(m) { + sorter: this.gfm.DefaultOptions.sorter, + filter: this.gfm.DefaultOptions.filter, + beforeInsert: this.gfm.DefaultOptions.beforeInsert, + beforeSave: (merges) => { + return $.map(merges, (m) => { if (m.title == null) { return m; } @@ -228,23 +162,23 @@ } } }); - this.input.atwho({ + this.elem.atwho({ at: '~', alias: 'labels', searchKey: 'search', - displayTpl: this.Labels.template, + displayTpl: this.gfm.Labels.template, insertTpl: '${atwho-at}${title}', callbacks: { - beforeSave: function(merges) { + beforeSave: (merges) => { var sanitizeLabelTitle; - sanitizeLabelTitle = function(title) { + sanitizeLabelTitle = (title) => { if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { return "\"" + (sanitize(title)) + "\""; } else { return sanitize(title); } }; - return $.map(merges, function(m) { + return $.map(merges, (m) => { return { title: sanitizeLabelTitle(m.title), color: m.color, @@ -255,11 +189,12 @@ } }); // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms - this.input.filter('[data-supports-slash-commands="true"]').atwho({ + // Can't filter elem tho + this.elem.filter('[data-supports-slash-commands="true"]').atwho({ at: '/', alias: 'commands', searchKey: 'search', - displayTpl: function(value) { + displayTpl: (value) => { var tpl = '
  • /${name}'; if (value.aliases.length > 0) { tpl += ' (or /<%- aliases.join(", /") %>)'; @@ -273,7 +208,7 @@ tpl += '
  • '; return _.template(tpl)(value); }, - insertTpl: function(value) { + insertTpl: (value) => { var tpl = "/${name} "; var reference_prefix = null; if (value.params.length > 0) { @@ -286,11 +221,11 @@ }, suffix: '', callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - beforeSave: function(commands) { - return $.map(commands, function(c) { + sorter: this.gfm.DefaultOptions.sorter, + filter: this.gfm.DefaultOptions.filter, + beforeInsert: this.gfm.DefaultOptions.beforeInsert, + beforeSave: (commands) => { + return $.map(commands, (c) => { var search = c.name; if (c.aliases.length > 0) { search = search + " " + c.aliases.join(" "); @@ -304,7 +239,7 @@ }; }); }, - matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { + matcher: (flag, subtext, should_startWithSpace, acceptSpaceBar) => { var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi var match = regexp.exec(subtext); if (match) { @@ -316,34 +251,186 @@ } }); return; - }, - destroyAtWho: function() { - return this.input.atwho('destroy'); - }, - fetchData: function(dataSource) { - return $.getJSON(dataSource); - }, - loadData: function(data) { - this.cachedData = data; - this.dataLoaded = true; + } + // inputtor + destroyAtWho() { + return this.elem.atwho('destroy'); + } + + loadData() { + const data = this.gfm.cachedData; // load members - this.input.atwho('load', '@', data.members); + this.elem.atwho('load', '@', data.members); // load issues - this.input.atwho('load', 'issues', data.issues); + this.elem.atwho('load', 'issues', data.issues); // load milestones - this.input.atwho('load', 'milestones', data.milestones); + this.elem.atwho('load', 'milestones', data.milestones); // load merge requests - this.input.atwho('load', 'mergerequests', data.mergerequests); + this.elem.atwho('load', 'mergerequests', data.mergerequests); // load emojis - this.input.atwho('load', ':', data.emojis); + this.elem.atwho('load', ':', data.emojis); // load labels - this.input.atwho('load', '~', data.labels); + this.elem.atwho('load', '~', data.labels); // load commands - this.input.atwho('load', '/', data.commands); + this.elem.atwho('load', '/', data.commands); // This trigger at.js again // otherwise we would be stuck with loading until the user types return $(':focus').trigger('keyup'); } - }; + } + +/* Implementation of Gfm Pub/Sub + + function myMatcher(val) { + return val.indexOf('hello') > -1; + } + + function myCallback(val) { + console.log(val); + } + + GitLab.GfmAutoComplete.subscribe(myMatcher, myCallback); + +*/ +let GfmAutoComplete; + + class GfmFactory { + constructor() { + if (!GfmAutoComplete) { + GfmAutoComplete = this; + GfmAutoComplete.init(); + } + return GfmAutoComplete; + } + + init() { + // TODO: at this point, don't need to keep track of inputors. Reconsider storing. + this.inputors = []; + this.subscribers = []; + this.dataLoading = false; + this.dataLoaded = false; + this.cachedData = {}; + this.dataSource = ''; + this.listenForAddSuccess(); + + this.Emoji = { + template: '
  • ${name} ${name}
  • ' + }; + + this.Members = { + template: '
  • ${username} ${title}
  • ' + }; + + this.Labels = { + template: '
  • ${title}
  • ' + }; + + this.Issues = { + template: '
  • ${id} ${title}
  • ' + }; + + this.Milestones = { + template: '
  • ${title}
  • ' + }; + + this.Loading = { + template: '
  • Loading...
  • ' + }; + + this.DefaultOptions = { + sorter: function(query, items, searchKey) { + if ((items[0].name != null) && items[0].name === 'loading') { + return items; + } + return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); + }, + filter: function(query, data, searchKey) { + if (data[0] === 'loading') { + return data; + } + return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); + }, + beforeInsert: function(value) { + if (!GitLab.GfmAutoComplete.dataLoaded) { + return this.at; + } else { + return value; + } + } + }; + } + + listenForAddSuccess() { + $(document).on('ajax:success', '.gfm-form', this.publish.bind(this)); + } + + subscribe(matcher, callback) { + // TODO: Store by resource key -- this will depend on how Luke breaks things up + this.subscribers.push({ matcher, callback }); + } + + publish(event) { + const target = $(event.currentTarget).find('textarea'); + const targetInputor = this.inputors.filter((inputor) => { + return inputor.elem.attr('data-noteable-iid') == target.attr('data-noteable-iid'); + })[0]; + const inputorText = targetInputor.elem.val(); + // after submit event + const matched = this.subscribers.filter((subscriber) => { + const matcher = subscriber.matcher; + // matcher must return boolean + return matcher(inputorText); + }); + matched.forEach((subscriber) => { + subscriber.callback(inputorText); + }); + } + + initInputors() { + $('.js-gfm-input').each((i, inputor) => { + const inputorModel = new GfmInputor(inputor); + this.inputors.push(inputorModel); + }); + } + + setup() { + if (this.dataSource && !this.dataLoading && !this.cachedData) { + this.dataLoading = true; + return this.fetchData(this.dataSource) + .done((data) => { + // TODO: Make this DRY + this.cachedData = data; + this.dataLoading = false; + this.dataLoaded = true; + this.initInputors(); + }); + }; + + if (this.cachedData != null) { + this.dataLoading = false; + this.dataLoaded = true; + this.initInputors(); + } + } + + fetchData(dataSource) { + return $.getJSON(dataSource); + } + } + + GitLab.GfmAutoComplete = new GfmFactory(); + + function mymatcher(text) { + return text === 'hello'; + } + function mycallback(text) { + console.log("GOT SOME TEXT", text); + } + + $(() => { + GitLab.GfmAutoComplete.subscribe(mymatcher, mycallback ); + }); + + }).call(this); diff --git a/app/assets/javascripts/issuable_resource.js.es6 b/app/assets/javascripts/issuable_resource.js.es6 new file mode 100644 index 000000000000..b7c7891ed08d --- /dev/null +++ b/app/assets/javascripts/issuable_resource.js.es6 @@ -0,0 +1,94 @@ +// TODO: Bring in increasing interval util +// TODO: return a promise to subscribers? + +/* +* gl.IssuableResource.subscribe('assignee_id', (state) => { +* console.log("Do something with the new state", state); +* }); +* +* +* */ + +((global) => { + + let singleton; + + class IssuableResource { + constructor(path, issuable) { + if (!singleton) { + singleton = global.IssuableResource = this; + singleton.init(path, issuable); + } + return singleton; + } + + init(path, issuable) { + this.state = JSON.parse(issuable); + this.resource = Vue.resource(path); + this.subscribers = {}; + this.initPolling(); + } + + initPolling() { + setInterval(() => { + this.getIssuable(); + }, 1000); + } + + getIssuable() { + return this.resource.get() + .then((res) => this.updateState(res.data)) + .then((newState) => this.publish(newState)); + } + + putIssuable() { + + } + + deleteIssuable() { + + } + + addSubscriber(prop, callback) { + const isNewProp = !this.subscribers.hasOwnProperty(prop); + if (isNewProp) { + this.subscribers[prop] = []; + } + this.subscribers[prop].push(callback); + } + + publish(diff) { + // prevent subscribers mutating state + const stateCopy = _.extend({}, this.state); + for (var key in diff) { + const hasSubscribers = this.subscribers.hasOwnProperty(key); + if (hasSubscribers) { + this.subscribers[key].forEach((fn) => { + fn(stateCopy); + }); + } + } + } + + subscribe(propToWatch, callback) { + this.addSubscriber(propToWatch, callback); + } + + updateState(res) { + const diff = {}; + if (res.updated_at !== this.state.updated_at) { + for (var key in res) { + const val = res[key]; + if (this.state[key] !== val) { + diff[key] = val; + } + } + this.state = _.extend(this.state, diff); + } + return diff; + } + } + + global.IssuableResource = IssuableResource; + +})(window.gl || (window.gl = {})); diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 7363ead09ff7..81059882b16b 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -173,3 +173,4 @@ new Subscription('.subscription') new gl.DueDateSelectors(); sidebar = new Sidebar(); + new gl.IssuableResource('#{issuable_json_path(issuable)}', '#{issuable.to_json}'); -- GitLab From a7162b12a8fd6994dbd8033f1158a1ab9e14efc0 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Fri, 14 Oct 2016 17:46:50 +0200 Subject: [PATCH 02/26] Get gfm events working again. --- .../javascripts/gfm_auto_complete.js.es6 | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 7a29840be719..eb484a4321c1 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -8,6 +8,7 @@ constructor(elem) { this.gfm = GitLab.GfmAutoComplete; this.elem = $(elem); + this.form = this.elem.parents('.gfm-form'); this.setup(); } @@ -370,20 +371,17 @@ let GfmAutoComplete; } publish(event) { - const target = $(event.currentTarget).find('textarea'); - const targetInputor = this.inputors.filter((inputor) => { - return inputor.elem.attr('data-noteable-iid') == target.attr('data-noteable-iid'); - })[0]; - const inputorText = targetInputor.elem.val(); - // after submit event - const matched = this.subscribers.filter((subscriber) => { - const matcher = subscriber.matcher; - // matcher must return boolean - return matcher(inputorText); - }); - matched.forEach((subscriber) => { - subscriber.callback(inputorText); - }); + const submittedText = this.findSubmittedText(event.currentTarget); + const matched = this.subscribers + .filter((subscriber) => subscriber.matcher(submittedText)) + .forEach((subscriber) => subscriber.callback(submittedText)); + } + + findSubmittedText(form) { + const formId = $(form).attr('data-noteable-iid'); + const targetInputor = this.inputors + .filter((inputor) => inputor.form.attr('data-noteable-iid') === formId)[0]; + return targetInputor.elem.val(); } initInputors() { @@ -420,17 +418,15 @@ let GfmAutoComplete; GitLab.GfmAutoComplete = new GfmFactory(); - function mymatcher(text) { - return text === 'hello'; - } - function mycallback(text) { - console.log("GOT SOME TEXT", text); + function myMatcher(val) { + return val.indexOf('hello') > -1; } - $(() => { - GitLab.GfmAutoComplete.subscribe(mymatcher, mycallback ); - }); + function myCallback(val) { + console.log(val); + } + GitLab.GfmAutoComplete.subscribe(myMatcher, myCallback); }).call(this); -- GitLab From 4cf1ea0031bdbe389b21c1cca9243361e292c121 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Fri, 14 Oct 2016 18:13:52 +0200 Subject: [PATCH 03/26] Document simple and Vue usage of gfm_autocomplete. --- .../javascripts/gfm_auto_complete.js.es6 | 58 +++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index eb484a4321c1..9a882f2bf330 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -1,3 +1,51 @@ +/* +* +* +* +* Simple usage: +* +* Matcher is passed the text successfully submitted in the gfm enabled form. The matcher +* should return a boolean representing whether or not the text contains information your +* callback would like to be passed when updated. +* + function myMatcher(val) { + return val === 'hello'; + } + + function myCallback(text) { + console.log(`Submitted text: ${text}`); + } + + GitLab.GfmAutoComplete.subscribe(myMatcher, myCallback); + + Integration with Vue: + + $(() => { + new Vue({ + el: '#example-el', + data: { + myVal: 'initial estimate value' + }, + methods: { + matchSubmitted: function(val) { + return val.indexOf('/estimate') > -1; + }, + updateMyVal: function(val) { + this.myVal = val; + }, + }, + ready: function() { + GitLab.GfmAutoComplete.subscribe(this.matchSubmitted, this.updateTester.bind(this)); + } + }); + }); +* +* +* +* +* */ + + // Creates the variables for setting up GFM auto-completion (() => { if (window.GitLab == null) { @@ -418,15 +466,5 @@ let GfmAutoComplete; GitLab.GfmAutoComplete = new GfmFactory(); - function myMatcher(val) { - return val.indexOf('hello') > -1; - } - - function myCallback(val) { - console.log(val); - } - - GitLab.GfmAutoComplete.subscribe(myMatcher, myCallback); - }).call(this); -- GitLab From 275a2ebc6e9252a3b1f1e7afd1b96912046d23ab Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Fri, 14 Oct 2016 20:36:09 +0200 Subject: [PATCH 04/26] Document proposed usage of IssuableResource. --- .../javascripts/issuable_resource.js.es6 | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/issuable_resource.js.es6 b/app/assets/javascripts/issuable_resource.js.es6 index b7c7891ed08d..c9f0f209d5f6 100644 --- a/app/assets/javascripts/issuable_resource.js.es6 +++ b/app/assets/javascripts/issuable_resource.js.es6 @@ -1,14 +1,50 @@ -// TODO: Bring in increasing interval util -// TODO: return a promise to subscribers? - /* -* gl.IssuableResource.subscribe('assignee_id', (state) => { -* console.log("Do something with the new state", state); -* }); * +* IssuableResource is a pubsub-style service that polls the server for updates to +* an Issuable model and propagates changes to subscribers throughout the page. It is designed +* to update Vue-ized and non-Vue-ized components. +* +* Subscribe by passing in the Issuable property you want to be notified of updates to, and pass +* a callback or render method you will use to render your component's updated state. +* +* Currently this service only handles fetching new data. Eventually it would make sense to +* route more, if not all, Issuable ajax traffic through this class, to prevent conflicts and/or +* unneccessary requests. +* +* JQuery usage: * + class IssuableAssigneeComponent { + constructor() { + this.$elem = $('#assignee'); + gl.IssuableResource.subscribe('assignee_id', (newState) => { + this.renderState(newState); + }); + } + + renderState(issuable) { + this.$elem.val(issuable.assignee_id); + } + } + + Vue usage: + + const app = new Vue({ + data: { + assignee_id: '' + }, + ready: function() { + gl.IssuableResource.subscribe('assignee_id', (newState) => { + this.assignee_id = newState.assignee_id; + }); + } + }); + + * */ +//= require vue +//= require vue-resource + ((global) => { let singleton; -- GitLab From 901f57889b2198a9626126cc375274dd926afe9b Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Fri, 14 Oct 2016 20:36:33 +0200 Subject: [PATCH 05/26] Clean up IssuableResource class. --- .../javascripts/issuable_resource.js.es6 | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/issuable_resource.js.es6 b/app/assets/javascripts/issuable_resource.js.es6 index c9f0f209d5f6..ff1122b6c290 100644 --- a/app/assets/javascripts/issuable_resource.js.es6 +++ b/app/assets/javascripts/issuable_resource.js.es6 @@ -68,29 +68,11 @@ initPolling() { setInterval(() => { this.getIssuable(); - }, 1000); + }, 3000); } - getIssuable() { - return this.resource.get() - .then((res) => this.updateState(res.data)) - .then((newState) => this.publish(newState)); - } - - putIssuable() { - - } - - deleteIssuable() { - - } - - addSubscriber(prop, callback) { - const isNewProp = !this.subscribers.hasOwnProperty(prop); - if (isNewProp) { - this.subscribers[prop] = []; - } - this.subscribers[prop].push(callback); + subscribe(propToWatch, callback) { + this.addSubscriber(propToWatch, callback); } publish(diff) { @@ -106,8 +88,12 @@ } } - subscribe(propToWatch, callback) { - this.addSubscriber(propToWatch, callback); + addSubscriber(prop, callback) { + const isNewProp = !this.subscribers.hasOwnProperty(prop); + if (isNewProp) { + this.subscribers[prop] = []; + } + this.subscribers[prop].push(callback); } updateState(res) { @@ -123,6 +109,17 @@ } return diff; } + + getIssuable() { + return this.resource.get() + .then((res) => this.updateState(res.data)) + .then((newState) => this.publish(newState)); + } + + putIssuable() {} + + deleteIssuable() {} + } global.IssuableResource = IssuableResource; -- GitLab From 6af0dc6450908c7c96d846eca84f35b0cddc3969 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Fri, 14 Oct 2016 20:56:39 +0200 Subject: [PATCH 06/26] Document gfm_autocomplete. --- .../javascripts/gfm_auto_complete.js.es6 | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 9a882f2bf330..63530e79ee52 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -2,12 +2,21 @@ * * * -* Simple usage: * -* Matcher is passed the text successfully submitted in the gfm enabled form. The matcher -* should return a boolean representing whether or not the text contains information your -* callback would like to be passed when updated. +* Using GfmAutocomplete pubsub: * +* A pubsub notification system is needed with gfm forms because gfm notes have can change to +* change the issueable state with slash commands. These changes need to be instantly propagated +* to representations of state throughout the page, to prevent outdated information from persisting +* on the page. +* +* To subscribe to successfully saved gfm forms, call the `subcribe` method, passing a matcher function +* and a handler function. The matcher will be passed the submitted text, and should return +* a boolean representing whether or not the text contains string(s) (e.g. '/assign' you would +* you would like to be notified of being submitted. The handler will be called and passed the submitted text +* when a new note for which the matcher returned true. +* +* Examples: function myMatcher(val) { return val === 'hello'; } @@ -18,35 +27,28 @@ GitLab.GfmAutoComplete.subscribe(myMatcher, myCallback); - Integration with Vue: + With Vue: - $(() => { - new Vue({ - el: '#example-el', - data: { - myVal: 'initial estimate value' + new Vue({ + el: '#example-el', + data: { + myVal: 'initial estimate value' + }, + methods: { + matchSubmitted: function(val) { + return val.indexOf('/estimate') > -1; }, - methods: { - matchSubmitted: function(val) { - return val.indexOf('/estimate') > -1; - }, - updateMyVal: function(val) { - this.myVal = val; - }, + updateMyVal: function(val) { + this.myVal = val; }, - ready: function() { - GitLab.GfmAutoComplete.subscribe(this.matchSubmitted, this.updateTester.bind(this)); - } - }); + }, + ready: function() { + GitLab.GfmAutoComplete.subscribe(this.matchSubmitted, this.updateTester.bind(this)); + } }); * -* -* -* * */ - -// Creates the variables for setting up GFM auto-completion (() => { if (window.GitLab == null) { window.GitLab = {}; -- GitLab From efc105bb02d4b35d6bc0756eaa3136b70adfae65 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 17 Oct 2016 13:10:48 +0200 Subject: [PATCH 07/26] Abstract IssuableResource into SubbableResource. --- .../javascripts/issuable_resource.js.es6 | 127 ------------ .../javascripts/subbable_resource.js.es6 | 192 ++++++++++++++++++ 2 files changed, 192 insertions(+), 127 deletions(-) delete mode 100644 app/assets/javascripts/issuable_resource.js.es6 create mode 100644 app/assets/javascripts/subbable_resource.js.es6 diff --git a/app/assets/javascripts/issuable_resource.js.es6 b/app/assets/javascripts/issuable_resource.js.es6 deleted file mode 100644 index ff1122b6c290..000000000000 --- a/app/assets/javascripts/issuable_resource.js.es6 +++ /dev/null @@ -1,127 +0,0 @@ -/* -* -* IssuableResource is a pubsub-style service that polls the server for updates to -* an Issuable model and propagates changes to subscribers throughout the page. It is designed -* to update Vue-ized and non-Vue-ized components. -* -* Subscribe by passing in the Issuable property you want to be notified of updates to, and pass -* a callback or render method you will use to render your component's updated state. -* -* Currently this service only handles fetching new data. Eventually it would make sense to -* route more, if not all, Issuable ajax traffic through this class, to prevent conflicts and/or -* unneccessary requests. -* -* JQuery usage: -* - class IssuableAssigneeComponent { - constructor() { - this.$elem = $('#assignee'); - gl.IssuableResource.subscribe('assignee_id', (newState) => { - this.renderState(newState); - }); - } - - renderState(issuable) { - this.$elem.val(issuable.assignee_id); - } - } - - Vue usage: - - const app = new Vue({ - data: { - assignee_id: '' - }, - ready: function() { - gl.IssuableResource.subscribe('assignee_id', (newState) => { - this.assignee_id = newState.assignee_id; - }); - } - }); - - -* */ - -//= require vue -//= require vue-resource - -((global) => { - - let singleton; - - class IssuableResource { - constructor(path, issuable) { - if (!singleton) { - singleton = global.IssuableResource = this; - singleton.init(path, issuable); - } - return singleton; - } - - init(path, issuable) { - this.state = JSON.parse(issuable); - this.resource = Vue.resource(path); - this.subscribers = {}; - this.initPolling(); - } - - initPolling() { - setInterval(() => { - this.getIssuable(); - }, 3000); - } - - subscribe(propToWatch, callback) { - this.addSubscriber(propToWatch, callback); - } - - publish(diff) { - // prevent subscribers mutating state - const stateCopy = _.extend({}, this.state); - for (var key in diff) { - const hasSubscribers = this.subscribers.hasOwnProperty(key); - if (hasSubscribers) { - this.subscribers[key].forEach((fn) => { - fn(stateCopy); - }); - } - } - } - - addSubscriber(prop, callback) { - const isNewProp = !this.subscribers.hasOwnProperty(prop); - if (isNewProp) { - this.subscribers[prop] = []; - } - this.subscribers[prop].push(callback); - } - - updateState(res) { - const diff = {}; - if (res.updated_at !== this.state.updated_at) { - for (var key in res) { - const val = res[key]; - if (this.state[key] !== val) { - diff[key] = val; - } - } - this.state = _.extend(this.state, diff); - } - return diff; - } - - getIssuable() { - return this.resource.get() - .then((res) => this.updateState(res.data)) - .then((newState) => this.publish(newState)); - } - - putIssuable() {} - - deleteIssuable() {} - - } - - global.IssuableResource = IssuableResource; - -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 new file mode 100644 index 000000000000..53791200ce68 --- /dev/null +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -0,0 +1,192 @@ +//= require vue +//= require vue-resource + +((global) => { + +/* +* SubbableResource can be extended to provide a pubsub-style service sending and receiving updates for +* a model and propagating changes to subscribers throughout the page. It is usable by both Vue-ized and +* non-Vue-ized components. +* +* Subscribe by passing a property you want to be notified of updates to, and pass +* a callback or render method you will use to render your component's updated state. +* +* JQuery usage: + + class IssuableAssigneeComponent { + constructor() { + this.$elem = $('#assignee'); + gl.IssuableResource.subscribe('assignee_id', (newState) => { + this.renderState(newState); + }); + } + + renderState(issuable) { + this.$elem.val(issuable.assignee_id); + } + } + + Vue usage: + + const app = new Vue({ + data: { + assignee_id: '' + }, + ready: function() { + gl.IssuableResource.subscribe('assignee_id', (newState) => { + this.assignee_id = newState.assignee_id; + }); + } + }); + +* */ + + class SubbableResource { + constructor({ path, data, pollInterval }) { + this.resource = Vue.resource(path); + this.data = JSON.parse(data); + this.pollInterval = poll; + + this.subscribers = {}; + this.state = { + loading: false, + last_updated: null + }; + + this.init(); + } + /* private methods */ + + init() { + this.initPolling(); + } + + initPolling() { + if (this.pollInterval) { + setInterval(() => { + this.getResource(); + }, this.pollInterval); + } + } + + + publish(diff) { + // prevent subscribers mutating state + const stateCopy = _.extend({}, this.data); + for (var key in diff) { + const hasSubscribers = this.subscribers.hasOwnProperty(key); + if (hasSubscribers) { + this.subscribers[key].forEach((fn) => { + fn(stateCopy); + }); + } + } + } + + addSubscriber(prop, callback) { + const isNewProp = !this.subscribers.hasOwnProperty(prop); + if (isNewProp) { + this.subscribers[prop] = []; + } + this.subscribers[prop].push(callback); + } + + updateState(res) { + const diff = {}; + if (res.updated_at !== this.data.updated_at) { + for (var key in res) { + const val = res[key]; + if (this.data[key] !== val) { + diff[key] = val; + } + } + this.data = _.extend(this.data, diff); + } + this.loading = false; + return diff; + } + + /* public methods */ + + subscribe(propToWatch, callback) { + this.addSubscriber(propToWatch, callback); + } + + getResource() { + if (this.loading && this.subscribers.length) { + return; + } + this.loading = true; + return this.resource.get() + .then((res) => this.updateState(res.data)) + .then((newState) => this.publish(newState)); + } + + // The following are only stubs. They would be used to provide DRY + // access to a remote resource used/modified by multiple components + + postResource(payload) { + this.resource.post(payload) + .then((res) => this.updateState(payload)) + .then((newState) => this.publish(newState)); + } + + putResource(payload) { + this.resource.put(state) + .then((res) => this.updateState(payload)) + .then((newState) => this.publish(newState)); + } + + deleteResource(payload) { + this.resource.delete(); + .then((res) => this.updateState(payload)) + .then((newState) => this.publish(newState)); + } + } + +/* + * SubbableResourceFactory is a gatekeeper for SubbableResources. It allows us to ensure that + * that resources for a given path are only created once. In time, it may provide additional + * opportunities for resource configuration. + + Usage: + + gl.IssuableResource = createSubbableResource({ + path: issuable_path_to_json, + data: issuable, + pollInterval: 15000, + }); + + */ + + class SubbableResourceFactory { + constructor() { + this.resources = []; + } + create(opts) { + // ensure only one resource per endpoint + const existingResource = this.existingResource(opts.path); + + if (existingResource) { + return existingResource; + } + + const newResource = new SubbableResource(opts); + this.resources.push(newResource); + + return newResource; + } + + existingResource(optsPath) { + this.resources.find((resource) => { + return resource.path === optsPath; + }); + } + } + + const resourceFactory = new SubbableResourceFactory(); + + // only expose creation method + global.createSubbableResource = resourceFactory.create; + +})(window.gl || (window.gl = {})); -- GitLab From d9049c136e9d97e6eafed44c4cf972ad0887bfc4 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 17 Oct 2016 13:13:54 +0200 Subject: [PATCH 08/26] Create SubbableResource in Issuable. --- app/views/shared/issuable/_sidebar.html.haml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 81059882b16b..a7ec7bc6eb6f 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -172,5 +172,10 @@ new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); new Subscription('.subscription') new gl.DueDateSelectors(); + new DueDateSelect(); + gl.IssuableResource = createSubbableResource({ + path: '#{issuable_json_path(issuable)}', + data: '#{issuable.to_json}' + pollInterval: 15000, + }); sidebar = new Sidebar(); - new gl.IssuableResource('#{issuable_json_path(issuable)}', '#{issuable.to_json}'); -- GitLab From 071f404d2cae2e3b84a2be7664b296f73343cbf1 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 17 Oct 2016 13:15:10 +0200 Subject: [PATCH 09/26] Fix syntax errors in template. --- app/views/shared/issuable/_sidebar.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index a7ec7bc6eb6f..c1deffe3cfec 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -175,7 +175,7 @@ new DueDateSelect(); gl.IssuableResource = createSubbableResource({ path: '#{issuable_json_path(issuable)}', - data: '#{issuable.to_json}' - pollInterval: 15000, + data: '#{issuable.to_json}', + pollInterval: 15000 }); sidebar = new Sidebar(); -- GitLab From 6d0352c93c635f1561056a45fe9b9f9041c47af1 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 17 Oct 2016 13:21:39 +0200 Subject: [PATCH 10/26] Cleanup subbable_resource.js.es6. --- app/assets/javascripts/subbable_resource.js.es6 | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 index 53791200ce68..22ed49b8f089 100644 --- a/app/assets/javascripts/subbable_resource.js.es6 +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -73,7 +73,7 @@ publish(diff) { // prevent subscribers mutating state const stateCopy = _.extend({}, this.data); - for (var key in diff) { + for (let key in diff) { const hasSubscribers = this.subscribers.hasOwnProperty(key); if (hasSubscribers) { this.subscribers[key].forEach((fn) => { @@ -85,20 +85,16 @@ addSubscriber(prop, callback) { const isNewProp = !this.subscribers.hasOwnProperty(prop); - if (isNewProp) { - this.subscribers[prop] = []; - } + if (isNewProp) this.subscribers[prop] = []; this.subscribers[prop].push(callback); } updateState(res) { const diff = {}; if (res.updated_at !== this.data.updated_at) { - for (var key in res) { + for (let key in res) { const val = res[key]; - if (this.data[key] !== val) { - diff[key] = val; - } + if (this.data[key] !== val) diff[key] = val; } this.data = _.extend(this.data, diff); } @@ -138,7 +134,7 @@ } deleteResource(payload) { - this.resource.delete(); + this.resource.delete() .then((res) => this.updateState(payload)) .then((newState) => this.publish(newState)); } -- GitLab From 2b1ec680672ad99cfb0e674a2033022d29da692e Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 17 Oct 2016 13:25:03 +0200 Subject: [PATCH 11/26] Rename GfmInputor elem to inputor. --- .../javascripts/gfm_auto_complete.js.es6 | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 63530e79ee52..48756b9bde4b 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -1,8 +1,4 @@ /* -* -* -* -* * Using GfmAutocomplete pubsub: * * A pubsub notification system is needed with gfm forms because gfm notes have can change to @@ -54,11 +50,12 @@ window.GitLab = {}; } + // 'Inputor' is atwho's name for the textarea/input element that will display autocomplete resources class GfmInputor { - constructor(elem) { + constructor(inputor) { this.gfm = GitLab.GfmAutoComplete; - this.elem = $(elem); - this.form = this.elem.parents('.gfm-form'); + this.$inputor = $(inputor); + this.form = this.$inputor.parents('.gfm-form'); this.setup(); } @@ -70,7 +67,7 @@ setupAtWho() { // Emoji - this.elem.atwho({ + this.$inputor.atwho({ at: ':', displayTpl: (value) => { if (value.path != null) { @@ -88,7 +85,7 @@ } }); // Team Members - this.elem.atwho({ + this.$inputor.atwho({ at: '@', displayTpl: (value) => { if (value.username != null) { @@ -123,7 +120,7 @@ } } }); - this.elem.atwho({ + this.$inputor.atwho({ at: '#', alias: 'issues', searchKey: 'search', @@ -154,7 +151,7 @@ } } }); - this.elem.atwho({ + this.$inputor.atwho({ at: '%', alias: 'milestones', searchKey: 'search', @@ -182,7 +179,7 @@ } } }); - this.elem.atwho({ + this.$inputor.atwho({ at: '!', alias: 'mergerequests', searchKey: 'search', @@ -213,7 +210,7 @@ } } }); - this.elem.atwho({ + this.$inputor.atwho({ at: '~', alias: 'labels', searchKey: 'search', @@ -241,7 +238,7 @@ }); // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms // Can't filter elem tho - this.elem.filter('[data-supports-slash-commands="true"]').atwho({ + this.$inputor.filter('[data-supports-slash-commands="true"]').atwho({ at: '/', alias: 'commands', searchKey: 'search', @@ -305,25 +302,25 @@ } // inputtor destroyAtWho() { - return this.elem.atwho('destroy'); + return this.$inputor.atwho('destroy'); } loadData() { const data = this.gfm.cachedData; // load members - this.elem.atwho('load', '@', data.members); + this.$inputor.atwho('load', '@', data.members); // load issues - this.elem.atwho('load', 'issues', data.issues); + this.$inputor.atwho('load', 'issues', data.issues); // load milestones - this.elem.atwho('load', 'milestones', data.milestones); + this.$inputor.atwho('load', 'milestones', data.milestones); // load merge requests - this.elem.atwho('load', 'mergerequests', data.mergerequests); + this.$inputor.atwho('load', 'mergerequests', data.mergerequests); // load emojis - this.elem.atwho('load', ':', data.emojis); + this.$inputor.atwho('load', ':', data.emojis); // load labels - this.elem.atwho('load', '~', data.labels); + this.$inputor.atwho('load', '~', data.labels); // load commands - this.elem.atwho('load', '/', data.commands); + this.$inputor.atwho('load', '/', data.commands); // This trigger at.js again // otherwise we would be stuck with loading until the user types return $(':focus').trigger('keyup'); @@ -356,7 +353,7 @@ let GfmAutoComplete; init() { // TODO: at this point, don't need to keep track of inputors. Reconsider storing. - this.inputors = []; + this.$inputors = []; this.subscribers = []; this.dataLoading = false; this.dataLoaded = false; @@ -429,7 +426,7 @@ let GfmAutoComplete; findSubmittedText(form) { const formId = $(form).attr('data-noteable-iid'); - const targetInputor = this.inputors + const targetInputor = this.$inputors .filter((inputor) => inputor.form.attr('data-noteable-iid') === formId)[0]; return targetInputor.elem.val(); } @@ -437,7 +434,7 @@ let GfmAutoComplete; initInputors() { $('.js-gfm-input').each((i, inputor) => { const inputorModel = new GfmInputor(inputor); - this.inputors.push(inputorModel); + this.$inputors.push(inputorModel); }); } -- GitLab From 3bdb7539235d05ed21beee6bbc0723705b98410c Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 17 Oct 2016 13:27:46 +0200 Subject: [PATCH 12/26] Make a note to break out atwho config into a separate file. --- app/assets/javascripts/gfm_auto_complete.js.es6 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 48756b9bde4b..8e16b5e633dd 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -66,7 +66,9 @@ } setupAtWho() { - // Emoji + // TODO: Break this up, and move to a separate file for config. It's annoying to have to scroll/jump around these 200+ LOC + + // Emoji this.$inputor.atwho({ at: ':', displayTpl: (value) => { -- GitLab From bf402d1a4f7890a113ec59c896ed09463938815c Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 17 Oct 2016 13:32:36 +0200 Subject: [PATCH 13/26] Filter and publish gfm_autocomplete in one shot. --- app/assets/javascripts/gfm_auto_complete.js.es6 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 8e16b5e633dd..db85fe66fbc2 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -421,9 +421,12 @@ let GfmAutoComplete; publish(event) { const submittedText = this.findSubmittedText(event.currentTarget); - const matched = this.subscribers - .filter((subscriber) => subscriber.matcher(submittedText)) - .forEach((subscriber) => subscriber.callback(submittedText)); + this.subscribers.forEach((subscriber) => { + const doesSubscribe = subscriber.matcher(submittedText); + if (doesSubscribe) { + subscriber.callback(submittedText); + } + }); } findSubmittedText(form) { -- GitLab From a88352053c1f87fa8f3b3ab3de4d37862213b097 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 17 Oct 2016 13:37:19 +0200 Subject: [PATCH 14/26] Put createSubbableResource on gl scope. --- app/views/shared/issuable/_sidebar.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index c1deffe3cfec..757bb08e10eb 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -173,7 +173,7 @@ new Subscription('.subscription') new gl.DueDateSelectors(); new DueDateSelect(); - gl.IssuableResource = createSubbableResource({ + gl.IssuableResource = gl.createSubbableResource({ path: '#{issuable_json_path(issuable)}', data: '#{issuable.to_json}', pollInterval: 15000 -- GitLab From 3f5ea70b4e9b192947afe9a10ffa2099119a8d2c Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 17 Oct 2016 13:45:07 +0200 Subject: [PATCH 15/26] Fix subbable refs to get it working. --- app/assets/javascripts/subbable_resource.js.es6 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 index 22ed49b8f089..64911a9aed26 100644 --- a/app/assets/javascripts/subbable_resource.js.es6 +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -45,7 +45,7 @@ constructor({ path, data, pollInterval }) { this.resource = Vue.resource(path); this.data = JSON.parse(data); - this.pollInterval = poll; + this.pollInterval = pollInterval; this.subscribers = {}; this.state = { @@ -98,7 +98,7 @@ } this.data = _.extend(this.data, diff); } - this.loading = false; + this.state.loading = false; return diff; } @@ -109,10 +109,10 @@ } getResource() { - if (this.loading && this.subscribers.length) { + if (this.state.loading && this.subscribers.length) { return; } - this.loading = true; + this.state.loading = true; return this.resource.get() .then((res) => this.updateState(res.data)) .then((newState) => this.publish(newState)); @@ -183,6 +183,6 @@ const resourceFactory = new SubbableResourceFactory(); // only expose creation method - global.createSubbableResource = resourceFactory.create; + global.createSubbableResource = resourceFactory.create.bind(resourceFactory); })(window.gl || (window.gl = {})); -- GitLab From c54de692b0328e767a845d44a879e753171c45f3 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 17 Oct 2016 13:59:27 +0200 Subject: [PATCH 16/26] Clean and document pubsub setup and logic. --- .../javascripts/gfm_auto_complete.js.es6 | 77 ++++++++++--------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index db85fe66fbc2..8f9af02b2368 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -355,13 +355,11 @@ let GfmAutoComplete; init() { // TODO: at this point, don't need to keep track of inputors. Reconsider storing. - this.$inputors = []; this.subscribers = []; this.dataLoading = false; this.dataLoaded = false; this.cachedData = {}; this.dataSource = ''; - this.listenForAddSuccess(); this.Emoji = { template: '
  • ${name} ${name}
  • ' @@ -410,12 +408,50 @@ let GfmAutoComplete; }; } + setup() { + if (this.dataSource && !this.dataLoading && !this.cachedData) { + this.dataLoading = true; + return this.fetchData(this.dataSource) + .done((data) => { + // TODO: Make this DRY, once setup logic is determined + this.cachedData = data; + this.dataLoading = false; + this.dataLoaded = true; + this.initInputorPubSub(); + }); + }; + + if (this.cachedData != null) { + this.dataLoading = false; + this.dataLoaded = true; + this.initInputorPubSub(); + } + } + + fetchData(dataSource) { + return $.getJSON(dataSource); + } + + /* pubsub setup/logic */ + + initInputorPubSub() { + this.inputors = []; + + $('.js-gfm-input').each((i, inputor) => { + const inputorModel = new GfmInputor(inputor); + this.inputors.push(inputorModel); + }); + + this.listenForAddSuccess(); + } + listenForAddSuccess() { $(document).on('ajax:success', '.gfm-form', this.publish.bind(this)); } subscribe(matcher, callback) { - // TODO: Store by resource key -- this will depend on how Luke breaks things up + // To, init/test quickly, you can execute the following in the console, and then submit comment 'hello' + // GitLab.GfmAutoComplete.subscribe(str => str === 'hello', str => alert(str)) this.subscribers.push({ matcher, callback }); } @@ -431,44 +467,13 @@ let GfmAutoComplete; findSubmittedText(form) { const formId = $(form).attr('data-noteable-iid'); - const targetInputor = this.$inputors + const targetInputor = this.inputors .filter((inputor) => inputor.form.attr('data-noteable-iid') === formId)[0]; - return targetInputor.elem.val(); - } - - initInputors() { - $('.js-gfm-input').each((i, inputor) => { - const inputorModel = new GfmInputor(inputor); - this.$inputors.push(inputorModel); - }); - } - - setup() { - if (this.dataSource && !this.dataLoading && !this.cachedData) { - this.dataLoading = true; - return this.fetchData(this.dataSource) - .done((data) => { - // TODO: Make this DRY - this.cachedData = data; - this.dataLoading = false; - this.dataLoaded = true; - this.initInputors(); - }); - }; - - if (this.cachedData != null) { - this.dataLoading = false; - this.dataLoaded = true; - this.initInputors(); - } + return targetInputor.$inputor.val(); } - fetchData(dataSource) { - return $.getJSON(dataSource); - } } GitLab.GfmAutoComplete = new GfmFactory(); - }).call(this); -- GitLab From 6ad49a07c0e715c7f75c4eb5d7202bff0a52a530 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 17 Oct 2016 14:42:56 +0200 Subject: [PATCH 17/26] Separate/Clean up and document pubsub logic for gfm_autocomplete. --- .../javascripts/gfm_auto_complete.js.es6 | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 8f9af02b2368..767725ba12a6 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -46,6 +46,7 @@ * */ (() => { + // TODO: Is there a reason this namespace is being used here? if (window.GitLab == null) { window.GitLab = {}; } @@ -56,6 +57,7 @@ this.gfm = GitLab.GfmAutoComplete; this.$inputor = $(inputor); this.form = this.$inputor.parents('.gfm-form'); + this.text = null; this.setup(); } @@ -446,17 +448,20 @@ let GfmAutoComplete; } listenForAddSuccess() { - $(document).on('ajax:success', '.gfm-form', this.publish.bind(this)); + const $document = $(document); + $document.on('ajax:success', '.gfm-form', this.publish.bind(this)); + $document.on('submit', '.gfm-form', this.storeInputorText.bind(this)); } + /* To, init/test quickly, you can execute the following in the console, and then submit comment 'hello' + * GitLab.GfmAutoComplete.subscribe(str => str === 'hello', str => alert(str)) + */ subscribe(matcher, callback) { - // To, init/test quickly, you can execute the following in the console, and then submit comment 'hello' - // GitLab.GfmAutoComplete.subscribe(str => str === 'hello', str => alert(str)) this.subscribers.push({ matcher, callback }); } publish(event) { - const submittedText = this.findSubmittedText(event.currentTarget); + const submittedText = this.findSubmittedText(event); this.subscribers.forEach((subscriber) => { const doesSubscribe = subscriber.matcher(submittedText); if (doesSubscribe) { @@ -465,11 +470,25 @@ let GfmAutoComplete; }); } - findSubmittedText(form) { + storeInputorText(event) { + const targetInputor = this.findTargetInputor(event); + targetInputor.text = targetInputor.$inputor.val(); + } + + findTargetInputor(event) { + const form = event.currentTarget; const formId = $(form).attr('data-noteable-iid'); - const targetInputor = this.inputors + return this.inputors .filter((inputor) => inputor.form.attr('data-noteable-iid') === formId)[0]; - return targetInputor.$inputor.val(); + } + + findSubmittedText(event) { + // TODO: Fix this. It's a bit of a hack, as 'ajax:success' is triggered more than once. Resetting inputor text + // ensures publishing isn't called twice for it. I know there's a cleaner way to do this. + const targetInputor = this.findTargetInputor(event); + const submitted = targetInputor.text; + targetInputor.text = null; + return submitted; } } -- GitLab From 0b0a58b8e80df9c7a4a216e86d78502ddb4bcaef Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 24 Oct 2016 17:49:41 +0200 Subject: [PATCH 18/26] Intermediate state -- squash eventually. --- .../javascripts/subbable_resource.js.es6 | 120 +++++++++++++++++- app/views/shared/issuable/_sidebar.html.haml | 1 - 2 files changed, 116 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 index 64911a9aed26..ab580106f8c1 100644 --- a/app/assets/javascripts/subbable_resource.js.es6 +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -52,7 +52,6 @@ loading: false, last_updated: null }; - this.init(); } /* private methods */ @@ -63,9 +62,12 @@ initPolling() { if (this.pollInterval) { - setInterval(() => { - this.getResource(); - }, this.pollInterval); + this.interval = new SmartInterval({ + callback: this.getResource.bind(this), + high: 15000, + low: 1000, + increment: 2000 + }); } } @@ -185,4 +187,114 @@ // only expose creation method global.createSubbableResource = resourceFactory.create.bind(resourceFactory); + + + class SmartInterval { + constructor({ callback, high = 120000, low = 15000, increment = 0, immediate = true, runInBackground = false, runInCache = false }) { + if (!callback) { + throw Error("You need to pass a callback to create a smart interval."); + } + this.callback = callback; + this.high = high; + this.low = low; + this.runInBackground = runInBackground; + this.increment = increment; + this.currentInterval = low; + this.immediate = immediate; + this.intervalId = null; + this.iterations = 0; + this.init(); + } + + init() { + if (this.immediate) { + this.start(); + } + + if (!this.runInBackground) { + // cancel interval when tab no longer shown + $(document).on('visibilitychange', (e) => { + const visState = document.visibilityState; + if (visState === 'hidden') { + this.cancel(); + } else { + this.start(); + } + }); + } + + if (!this.runInCache) { + // prevent interval continuing after page change, when kept in cache by Turbolinks + $(document).on('page:before-unload', (e) => { + this.cancel(); + }); + } + } + + incrementInterval() { + if (this.currentInterval < this.high) { + let nextInterval = this.currentInterval + this.increment; + if (nextInterval > this.high) { + nextInterval = this.high; + } + this.cancel(); + this.start(); + } + } + + logIteration() { + // TODO: Remove after development + this.iterations++; + console.log(`interval callback executed -- iterations: ${ this.iterations } -- current interval: ${this.currentInterval}`); + } + + /* public methods */ + + resetInterval() { + // set the currentInterval to low + // set the interval + } + + restartInterval() { + } + + setNextInterval() { + // set the new interval + this.intervalId = setInterval(() => { + // on interval trigger, cancel the current interval, increment the interval, and call the passed method + this.incrementInterval(); + this.callback(); + this.logIteration(); + }, this.currentInterval); + } + + clearPrevious() { + if (this.intervalId) { + clearInterval(this.intervalId); + this.currentInterval = this.low; + } + } + + start() { + // if it's currently set, clear it + this.clearPrevious(); + this.setNextInterval(); + } + + cancel() { + this.currentInterval = this.low; + return clearInterval(this.intervalId); + } + + } + + + + + + + + + + })(window.gl || (window.gl = {})); diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 757bb08e10eb..dae213b49f67 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -172,7 +172,6 @@ new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); new Subscription('.subscription') new gl.DueDateSelectors(); - new DueDateSelect(); gl.IssuableResource = gl.createSubbableResource({ path: '#{issuable_json_path(issuable)}', data: '#{issuable.to_json}', -- GitLab From 3f0222a05533164a13baa13b6148480f8476062a Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 24 Oct 2016 20:03:23 +0200 Subject: [PATCH 19/26] Finish up v1 of SmartInterval. --- .../javascripts/subbable_resource.js.es6 | 113 ++++++++---------- 1 file changed, 51 insertions(+), 62 deletions(-) diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 index ab580106f8c1..e51399ca8e88 100644 --- a/app/assets/javascripts/subbable_resource.js.es6 +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -187,38 +187,42 @@ // only expose creation method global.createSubbableResource = resourceFactory.create.bind(resourceFactory); - - class SmartInterval { - constructor({ callback, high = 120000, low = 15000, increment = 0, immediate = true, runInBackground = false, runInCache = false }) { - if (!callback) { - throw Error("You need to pass a callback to create a smart interval."); - } + constructor({ name = 'SmartIntervalInstance', callback, high = 120000, low = 15000, increment = 0, delay = 5000, immediate = true, runInBackground = false, runInCache = false }) { this.callback = callback; this.high = high; this.low = low; + this.delay = delay; this.runInBackground = runInBackground; this.increment = increment; - this.currentInterval = low; this.immediate = immediate; - this.intervalId = null; - this.iterations = 0; + this.name = name; + + this.state = { + iterations: 0, + currentInterval: low, + intervalId: null + }; + this.init(); } init() { if (this.immediate) { - this.start(); + window.setTimeout(() => { + this.start(); + }, this.delay); } if (!this.runInBackground) { // cancel interval when tab no longer shown - $(document).on('visibilitychange', (e) => { + const visChangeEventName = `visibilitychange:${this.name}`; + $(document).off(visChangeEventName).on(visChangeEventName, (e) => { const visState = document.visibilityState; if (visState === 'hidden') { - this.cancel(); + this.pause(); } else { - this.start(); + this.restart(); } }); } @@ -231,70 +235,55 @@ } } - incrementInterval() { - if (this.currentInterval < this.high) { - let nextInterval = this.currentInterval + this.increment; - if (nextInterval > this.high) { - nextInterval = this.high; - } - this.cancel(); - this.start(); - } + stopTimer() { + window.clearInterval(this.state.intervalId); + this.state.intervalId = null; } + // TODO: Remove after development logIteration() { - // TODO: Remove after development - this.iterations++; - console.log(`interval callback executed -- iterations: ${ this.iterations } -- current interval: ${this.currentInterval}`); + const iterations = this.state.iterations++; + console.log(`interval callback executed -- iterations: ${ iterations } -- current interval: ${ this.state.currentInterval }`); } /* public methods */ - resetInterval() { - // set the currentInterval to low - // set the interval - } - - restartInterval() { - } - - setNextInterval() { - // set the new interval - this.intervalId = setInterval(() => { - // on interval trigger, cancel the current interval, increment the interval, and call the passed method - this.incrementInterval(); + start() { + this.state.intervalId = setInterval(() => { this.callback(); + this.logIteration(); - }, this.currentInterval); - } - clearPrevious() { - if (this.intervalId) { - clearInterval(this.intervalId); - this.currentInterval = this.low; - } - } + if (this.state.currentInterval === this.high) { + return; + } - start() { - // if it's currently set, clear it - this.clearPrevious(); - this.setNextInterval(); + let nextInterval = this.state.currentInterval + this.increment; + + if (nextInterval > this.high) { + nextInterval = this.high; + } + + this.state.currentInterval = nextInterval; + this.restart(); + }, this.state.currentInterval); } + // cancel the existing timer, setting the currentInterval back to low cancel() { - this.currentInterval = this.low; - return clearInterval(this.intervalId); + this.state.currentInterval = this.low; + this.stopTimer(); } - } - - - - - - - - - + // cancel the existing timer, without setting the currentInterval back to low + pause() { + this.stopTimer(); + } + // start a timer, using the existing interval + restart() { + this.stopTimer(); + this.start(); + } + } })(window.gl || (window.gl = {})); -- GitLab From 3a54e6fa76ce8ca81cc56d4a16b84b0a87c9e598 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 24 Oct 2016 20:08:09 +0200 Subject: [PATCH 20/26] Move smart_interval to its own file. --- app/assets/javascripts/smart_timer.js.es6 | 100 ++++++++++++++++++ .../javascripts/subbable_resource.js.es6 | 99 ----------------- 2 files changed, 100 insertions(+), 99 deletions(-) create mode 100644 app/assets/javascripts/smart_timer.js.es6 diff --git a/app/assets/javascripts/smart_timer.js.es6 b/app/assets/javascripts/smart_timer.js.es6 new file mode 100644 index 000000000000..8a9b426aec15 --- /dev/null +++ b/app/assets/javascripts/smart_timer.js.es6 @@ -0,0 +1,100 @@ + + class SmartInterval { + constructor({ name = 'SmartIntervalInstance', callback, high = 120000, low = 15000, increment = 0, delay = 5000, immediate = true, runInBackground = false, runInCache = false }) { + this.callback = callback; + this.high = high; + this.low = low; + this.delay = delay; + this.runInBackground = runInBackground; + this.increment = increment; + this.immediate = immediate; + this.name = name; + + this.state = { + iterations: 0, + currentInterval: low, + intervalId: null + }; + + this.init(); + } + + init() { + if (this.immediate) { + window.setTimeout(() => { + this.start(); + }, this.delay); + } + + if (!this.runInBackground) { + // cancel interval when tab no longer shown + const visChangeEventName = `visibilitychange:${this.name}`; + $(document).off(visChangeEventName).on(visChangeEventName, (e) => { + const visState = document.visibilityState; + if (visState === 'hidden') { + this.pause(); + } else { + this.restart(); + } + }); + } + + if (!this.runInCache) { + // prevent interval continuing after page change, when kept in cache by Turbolinks + $(document).on('page:before-unload', (e) => { + this.cancel(); + }); + } + } + + stopTimer() { + window.clearInterval(this.state.intervalId); + this.state.intervalId = null; + } + + // TODO: Remove after development + logIteration() { + const iterations = this.state.iterations++; + console.log(`interval callback executed -- iterations: ${ iterations } -- current interval: ${ this.state.currentInterval }`); + } + + /* public methods */ + + start() { + this.state.intervalId = setInterval(() => { + this.callback(); + + this.logIteration(); + + if (this.state.currentInterval === this.high) { + return; + } + + let nextInterval = this.state.currentInterval + this.increment; + + if (nextInterval > this.high) { + nextInterval = this.high; + } + + this.state.currentInterval = nextInterval; + this.restart(); + }, this.state.currentInterval); + } + + // cancel the existing timer, setting the currentInterval back to low + cancel() { + this.state.currentInterval = this.low; + this.stopTimer(); + } + + // cancel the existing timer, without setting the currentInterval back to low + pause() { + this.stopTimer(); + } + + // start a timer, using the existing interval + restart() { + this.stopTimer(); + this.start(); + } + } diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 index e51399ca8e88..c6879b97f457 100644 --- a/app/assets/javascripts/subbable_resource.js.es6 +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -187,103 +187,4 @@ // only expose creation method global.createSubbableResource = resourceFactory.create.bind(resourceFactory); - class SmartInterval { - constructor({ name = 'SmartIntervalInstance', callback, high = 120000, low = 15000, increment = 0, delay = 5000, immediate = true, runInBackground = false, runInCache = false }) { - this.callback = callback; - this.high = high; - this.low = low; - this.delay = delay; - this.runInBackground = runInBackground; - this.increment = increment; - this.immediate = immediate; - this.name = name; - - this.state = { - iterations: 0, - currentInterval: low, - intervalId: null - }; - - this.init(); - } - - init() { - if (this.immediate) { - window.setTimeout(() => { - this.start(); - }, this.delay); - } - - if (!this.runInBackground) { - // cancel interval when tab no longer shown - const visChangeEventName = `visibilitychange:${this.name}`; - $(document).off(visChangeEventName).on(visChangeEventName, (e) => { - const visState = document.visibilityState; - if (visState === 'hidden') { - this.pause(); - } else { - this.restart(); - } - }); - } - - if (!this.runInCache) { - // prevent interval continuing after page change, when kept in cache by Turbolinks - $(document).on('page:before-unload', (e) => { - this.cancel(); - }); - } - } - - stopTimer() { - window.clearInterval(this.state.intervalId); - this.state.intervalId = null; - } - - // TODO: Remove after development - logIteration() { - const iterations = this.state.iterations++; - console.log(`interval callback executed -- iterations: ${ iterations } -- current interval: ${ this.state.currentInterval }`); - } - - /* public methods */ - - start() { - this.state.intervalId = setInterval(() => { - this.callback(); - - this.logIteration(); - - if (this.state.currentInterval === this.high) { - return; - } - - let nextInterval = this.state.currentInterval + this.increment; - - if (nextInterval > this.high) { - nextInterval = this.high; - } - - this.state.currentInterval = nextInterval; - this.restart(); - }, this.state.currentInterval); - } - - // cancel the existing timer, setting the currentInterval back to low - cancel() { - this.state.currentInterval = this.low; - this.stopTimer(); - } - - // cancel the existing timer, without setting the currentInterval back to low - pause() { - this.stopTimer(); - } - - // start a timer, using the existing interval - restart() { - this.stopTimer(); - this.start(); - } - } })(window.gl || (window.gl = {})); -- GitLab From 12d80edd7dad5d9372c4bde2a9fe2356db1fa3c8 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Tue, 25 Oct 2016 13:13:11 +0200 Subject: [PATCH 21/26] Separate SmartInterval into its own file and update refs. --- .../{smart_timer.js.es6 => smart_interval.js.es6} | 5 ++++- app/assets/javascripts/subbable_resource.js.es6 | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) rename app/assets/javascripts/{smart_timer.js.es6 => smart_interval.js.es6} (96%) diff --git a/app/assets/javascripts/smart_timer.js.es6 b/app/assets/javascripts/smart_interval.js.es6 similarity index 96% rename from app/assets/javascripts/smart_timer.js.es6 rename to app/assets/javascripts/smart_interval.js.es6 index 8a9b426aec15..af153ded42db 100644 --- a/app/assets/javascripts/smart_timer.js.es6 +++ b/app/assets/javascripts/smart_interval.js.es6 @@ -1,4 +1,4 @@ - +((global) => { class SmartInterval { constructor({ name = 'SmartIntervalInstance', callback, high = 120000, low = 15000, increment = 0, delay = 5000, immediate = true, runInBackground = false, runInCache = false }) { this.callback = callback; @@ -98,3 +98,6 @@ this.start(); } } + + global.SmartInterval = SmartInterval; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 index c6879b97f457..8aa7c1f90292 100644 --- a/app/assets/javascripts/subbable_resource.js.es6 +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -62,7 +62,7 @@ initPolling() { if (this.pollInterval) { - this.interval = new SmartInterval({ + this.interval = new global.SmartInterval({ callback: this.getResource.bind(this), high: 15000, low: 1000, -- GitLab From 2762acfcdc32b764e435759cd95c034d8375e242 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Tue, 25 Oct 2016 13:38:34 +0200 Subject: [PATCH 22/26] Move polling config out of subbable resource. --- .../javascripts/subbable_resource.js.es6 | 28 ++++++------------- app/views/shared/issuable/_sidebar.html.haml | 6 +++- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 index 8aa7c1f90292..e5f9d70f0d24 100644 --- a/app/assets/javascripts/subbable_resource.js.es6 +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -42,36 +42,24 @@ * */ class SubbableResource { - constructor({ path, data, pollInterval }) { + constructor({ path, data, pollCfg }) { this.resource = Vue.resource(path); this.data = JSON.parse(data); - this.pollInterval = pollInterval; - this.subscribers = {}; this.state = { - loading: false, - last_updated: null + loading: false }; - this.init(); + this.init(pollCfg); } /* private methods */ - init() { - this.initPolling(); - } - - initPolling() { - if (this.pollInterval) { - this.interval = new global.SmartInterval({ - callback: this.getResource.bind(this), - high: 15000, - low: 1000, - increment: 2000 - }); - } + init(pollCfg) { + const preppedCfg = _.extend(pollCfg, { + callback: this.getResource.bind(this) + }); + this.interval = new global.SmartInterval(preppedCfg); } - publish(diff) { // prevent subscribers mutating state const stateCopy = _.extend({}, this.data); diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index dae213b49f67..881915ab2bf6 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -175,6 +175,10 @@ gl.IssuableResource = gl.createSubbableResource({ path: '#{issuable_json_path(issuable)}', data: '#{issuable.to_json}', - pollInterval: 15000 + pollCfg: { + high: 15000, + low: 1000, + increment: 2000 + } }); sidebar = new Sidebar(); -- GitLab From a2ee7ccc0c5565cf4bab4124030911d2e8aef769 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Tue, 25 Oct 2016 13:38:57 +0200 Subject: [PATCH 23/26] Only get resource if not already getting and there are subscribers. --- app/assets/javascripts/subbable_resource.js.es6 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 index e5f9d70f0d24..fe61054c4202 100644 --- a/app/assets/javascripts/subbable_resource.js.es6 +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -99,7 +99,8 @@ } getResource() { - if (this.state.loading && this.subscribers.length) { + const totalSubscribers = Object.keys(this.subscribers).length; + if (!this.state.loading || !totalSubscribers) { return; } this.state.loading = true; -- GitLab From d4ad03b15dfd799467181be8eecb365fc42da386 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Tue, 25 Oct 2016 13:43:47 +0200 Subject: [PATCH 24/26] Fix resource method refs to match vue resource API. --- app/assets/javascripts/subbable_resource.js.es6 | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 index fe61054c4202..e3c9cedfd0c6 100644 --- a/app/assets/javascripts/subbable_resource.js.es6 +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -109,23 +109,20 @@ .then((newState) => this.publish(newState)); } - // The following are only stubs. They would be used to provide DRY - // access to a remote resource used/modified by multiple components - - postResource(payload) { - this.resource.post(payload) + save(payload) { + this.resource.save(payload) .then((res) => this.updateState(payload)) .then((newState) => this.publish(newState)); } - putResource(payload) { - this.resource.put(state) + update(payload) { + this.resource.update(payload) .then((res) => this.updateState(payload)) .then((newState) => this.publish(newState)); } - deleteResource(payload) { - this.resource.delete() + remove(payload) { + this.resource.remove(payload) .then((res) => this.updateState(payload)) .then((newState) => this.publish(newState)); } -- GitLab From bf37c477a2f08632f5b49ca4d2e18df718efa794 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Tue, 25 Oct 2016 13:58:09 +0200 Subject: [PATCH 25/26] Return promises in subbable http methods. --- app/assets/javascripts/subbable_resource.js.es6 | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 index e3c9cedfd0c6..f86fe2769d11 100644 --- a/app/assets/javascripts/subbable_resource.js.es6 +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -104,26 +104,30 @@ return; } this.state.loading = true; + return this.get(); + } + + get() { return this.resource.get() .then((res) => this.updateState(res.data)) .then((newState) => this.publish(newState)); } save(payload) { - this.resource.save(payload) - .then((res) => this.updateState(payload)) + return this.resource.save(payload) + .then((res) => this.updateState(res.data)) .then((newState) => this.publish(newState)); } update(payload) { - this.resource.update(payload) - .then((res) => this.updateState(payload)) + return this.resource.update(payload) + .then((res) => this.updateState(res.data)) .then((newState) => this.publish(newState)); } remove(payload) { - this.resource.remove(payload) - .then((res) => this.updateState(payload)) + return this.resource.remove(payload) + .then((res) => this.updateState(res.data)) .then((newState) => this.publish(newState)); } } -- GitLab From 963eb2ff3b5643bc04428f053aaab079a4999774 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Tue, 25 Oct 2016 14:02:09 +0200 Subject: [PATCH 26/26] Clean up method order in subbable. --- app/assets/javascripts/subbable_resource.js.es6 | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 index f86fe2769d11..520751a6ecb1 100644 --- a/app/assets/javascripts/subbable_resource.js.es6 +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -51,6 +51,7 @@ }; this.init(pollCfg); } + /* private methods */ init(pollCfg) { @@ -92,12 +93,6 @@ return diff; } - /* public methods */ - - subscribe(propToWatch, callback) { - this.addSubscriber(propToWatch, callback); - } - getResource() { const totalSubscribers = Object.keys(this.subscribers).length; if (!this.state.loading || !totalSubscribers) { @@ -107,6 +102,13 @@ return this.get(); } + + /* public methods */ + + subscribe(propToWatch, callback) { + this.addSubscriber(propToWatch, callback); + } + get() { return this.resource.get() .then((res) => this.updateState(res.data)) -- GitLab