diff --git a/app/assets/javascripts/discourse/adapters/rest.js.es6 b/app/assets/javascripts/discourse/adapters/rest.js.es6 index 959bf9bee..da499c963 100644 --- a/app/assets/javascripts/discourse/adapters/rest.js.es6 +++ b/app/assets/javascripts/discourse/adapters/rest.js.es6 @@ -1,31 +1,45 @@ const ADMIN_MODELS = ['plugin']; export default Ember.Object.extend({ - pathFor(type, id) { - let path = "/" + Ember.String.underscore(type + 's'); + pathFor(store, type, findArgs) { + let path = "/" + Ember.String.underscore(store.pluralize(type)); if (ADMIN_MODELS.indexOf(type) !== -1) { path = "/admin/" + path; } - if (id) { path += "/" + id; } + + if (findArgs) { + if (typeof findArgs === "object") { + const queryString = Object.keys(findArgs) + .reject(k => !findArgs[k]) + .map(k => k + "=" + encodeURIComponent(findArgs[k])); + + if (queryString.length) { + path += "?" + queryString.join('&'); + } + } else { + // It's serializable as a string if not an object + path += "/" + findArgs; + } + } return path; }, findAll(store, type) { - return Discourse.ajax(this.pathFor(type)); + return Discourse.ajax(this.pathFor(store, type)); }, - find(store, type, id) { - return Discourse.ajax(this.pathFor(type, id)); + find(store, type, findArgs) { + return Discourse.ajax(this.pathFor(store, type, findArgs)); }, update(store, type, id, attrs) { const data = {}; data[Ember.String.underscore(type)] = attrs; - return Discourse.ajax(this.pathFor(type, id), { method: 'PUT', data }); + return Discourse.ajax(this.pathFor(store, type, id), { method: 'PUT', data }); }, destroyRecord(store, type, record) { - return Discourse.ajax(this.pathFor(type, record.get('id')), { method: 'DELETE' }); + return Discourse.ajax(this.pathFor(store, type, record.get('id')), { method: 'DELETE' }); } }); diff --git a/app/assets/javascripts/discourse/components/directory-toggle.js.es6 b/app/assets/javascripts/discourse/components/directory-toggle.js.es6 new file mode 100644 index 000000000..8c4b4761b --- /dev/null +++ b/app/assets/javascripts/discourse/components/directory-toggle.js.es6 @@ -0,0 +1,28 @@ +import StringBuffer from 'discourse/mixins/string-buffer'; +import { iconHTML } from 'discourse/helpers/fa-icon'; + +export default Ember.Component.extend(StringBuffer, { + tagName: 'th', + classNames: ['sortable'], + rerenderTriggers: ['order', 'asc'], + + renderString(buffer) { + const field = this.get('field'); + buffer.push(I18n.t('directory.' + field)); + + if (field === this.get('order')) { + buffer.push(iconHTML(this.get('asc') ? 'chevron-up' : 'chevron-down')); + } + }, + + click() { + const currentOrder = this.get('order'), + field = this.get('field'); + + if (currentOrder === field) { + this.set('asc', this.get('asc') ? null : true); + } else { + this.setProperties({ order: field, asc: null }); + } + } +}); diff --git a/app/assets/javascripts/discourse/components/top-period-chooser.js.es6 b/app/assets/javascripts/discourse/components/period-chooser.js.es6 similarity index 68% rename from app/assets/javascripts/discourse/components/top-period-chooser.js.es6 rename to app/assets/javascripts/discourse/components/period-chooser.js.es6 index ee63a0ae4..1bbf20c03 100644 --- a/app/assets/javascripts/discourse/components/top-period-chooser.js.es6 +++ b/app/assets/javascripts/discourse/components/period-chooser.js.es6 @@ -10,9 +10,9 @@ export default Ember.Component.extend(CleansUp, { }, _clickToClose: function() { - var self = this; + const self = this; $('html').off('mousedown.top-period').on('mousedown.top-period', function(e) { - var $target = $(e.target); + const $target = $(e.target); if (($target.prop('id') === 'topic-entrance') || (self.$().has($target).length !== 0)) { return; } @@ -20,12 +20,23 @@ export default Ember.Component.extend(CleansUp, { }); }, - click: function() { + click(e) { + if ($(e.target).closest('.period-popup').length) { return; } + if (!this.get('showPeriods')) { - var $chevron = this.$('i.fa-caret-down'); + const $chevron = this.$('i.fa-caret-down'); this.$('#period-popup').css($chevron.position()); this.set('showPeriods', true); this._clickToClose(); } + }, + + actions: { + changePeriod(p) { + this.cleanUp(); + this.set('period', p); + this.sendAction('action', p); + } } + }); diff --git a/app/assets/javascripts/discourse/components/top-period-buttons.js.es6 b/app/assets/javascripts/discourse/components/top-period-buttons.js.es6 index eec8000a4..3273805d7 100644 --- a/app/assets/javascripts/discourse/components/top-period-buttons.js.es6 +++ b/app/assets/javascripts/discourse/components/top-period-buttons.js.es6 @@ -1,3 +1,14 @@ export default Ember.Component.extend({ - classNames: ['top-title-buttons'] + classNames: ['top-title-buttons'], + + periods: function() { + const period = this.get('period'); + return this.site.get('periods').filter(p => p !== period); + }.property('period'), + + actions: { + changePeriod(p) { + this.sendAction('action', p); + } + } }); diff --git a/app/assets/javascripts/discourse/components/top-title-button.js.es6 b/app/assets/javascripts/discourse/components/top-title-button.js.es6 deleted file mode 100644 index 8da2fd507..000000000 --- a/app/assets/javascripts/discourse/components/top-title-button.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -import TopTitle from 'discourse/components/top-title'; - -export default TopTitle.extend({ - tagName: 'button', - classNameBindings: [':btn', ':btn-default', 'unless:hidden'], - - click: function() { - var url = this.get('period.showMoreUrl'); - if (url) { - Discourse.URL.routeTo(url); - } - } -}); diff --git a/app/assets/javascripts/discourse/components/top-title.js.es6 b/app/assets/javascripts/discourse/components/top-title.js.es6 deleted file mode 100644 index cfa065429..000000000 --- a/app/assets/javascripts/discourse/components/top-title.js.es6 +++ /dev/null @@ -1,10 +0,0 @@ -import StringBuffer from 'discourse/mixins/string-buffer'; - -export default Ember.Component.extend(StringBuffer, { - tagName: 'h2', - rerenderTriggers: ['period.title'], - - renderString: function(buffer) { - buffer.push(" " + this.get('period.title')); - } -}); diff --git a/app/assets/javascripts/discourse/controllers/directory-show.js.es6 b/app/assets/javascripts/discourse/controllers/directory-show.js.es6 new file mode 100644 index 000000000..2ea363076 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/directory-show.js.es6 @@ -0,0 +1,13 @@ +export default Ember.Controller.extend({ + queryParams: ['order', 'asc'], + order: 'likes_received', + asc: null, + + showTimeRead: Ember.computed.equal('period', 'all'), + + actions: { + loadMore() { + this.get('model').loadMore(); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/discovery.js.es6 b/app/assets/javascripts/discourse/controllers/discovery.js.es6 index 45fa7d295..abaaa1e2b 100644 --- a/app/assets/javascripts/discourse/controllers/discovery.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery.js.es6 @@ -1,5 +1,4 @@ import ObjectController from 'discourse/controllers/object'; -import TopPeriod from 'discourse/models/top-period'; export default ObjectController.extend({ needs: ['navigation/category', 'discovery/topics', 'application'], @@ -15,7 +14,7 @@ export default ObjectController.extend({ }.observes("loadedAllItems"), showMoreUrl(period) { - var url = '', category = this.get('category'); + let url = '', category = this.get('category'); if (category) { url = '/c/' + Discourse.Category.slugFor(category) + (this.get('noSubcategories') ? '/none' : '') + '/l'; } @@ -23,15 +22,10 @@ export default ObjectController.extend({ return url; }, - periods: function() { - const self = this, - periods = []; - this.site.get('periods').forEach(function(p) { - periods.pushObject(TopPeriod.create({ id: p, - showMoreUrl: self.showMoreUrl(p), - periods })); - }); - return periods; - }.property('category', 'noSubcategories'), + actions: { + changePeriod(p) { + Discourse.URL.routeTo(this.showMoreUrl(p)); + } + } }); diff --git a/app/assets/javascripts/discourse/helpers/period-title.js.es6 b/app/assets/javascripts/discourse/helpers/period-title.js.es6 new file mode 100644 index 000000000..c85643934 --- /dev/null +++ b/app/assets/javascripts/discourse/helpers/period-title.js.es6 @@ -0,0 +1,11 @@ +import { iconHTML } from 'discourse/helpers/fa-icon'; + +const TITLE_SUBS = { yearly: 'this_year', + monthly: 'this_month', + daily: 'today', + all: 'all' }; + +export default Ember.Handlebars.makeBoundHelper(function (period) { + const title = I18n.t('filters.top.' + (TITLE_SUBS[period] || 'this_week')); + return new Handlebars.SafeString(iconHTML('calendar-o') + " " + title); +}); diff --git a/app/assets/javascripts/discourse/mixins/string-buffer.js.es6 b/app/assets/javascripts/discourse/mixins/string-buffer.js.es6 index a11f7419a..469873d22 100644 --- a/app/assets/javascripts/discourse/mixins/string-buffer.js.es6 +++ b/app/assets/javascripts/discourse/mixins/string-buffer.js.es6 @@ -1,33 +1,30 @@ export default Ember.Mixin.create({ _watchProps: function() { - var args = this.get('rerenderTriggers'); + const args = this.get('rerenderTriggers'); if (!Ember.isNone(args)) { - var self = this; - args.forEach(function(k) { - self.addObserver(k, self.rerenderString); - }); + args.forEach(k => this.addObserver(k, this.rerenderString)); } }.on('init'), - render: function(buffer) { + render(buffer) { this.renderString(buffer); }, - renderString: function(buffer){ - var template = Discourse.__container__.lookup('template:' + this.rawTemplate); + renderString(buffer){ + const template = Discourse.__container__.lookup('template:' + this.rawTemplate); if (template) { buffer.push(template(this)); } }, - _rerenderString: function() { - var buffer = []; + _rerenderString() { + const buffer = []; this.renderString(buffer); this.$().html(buffer.join('')); }, - rerenderString: function() { + rerenderString() { Ember.run.once(this, '_rerenderString'); } diff --git a/app/assets/javascripts/discourse/models/rest.js.es6 b/app/assets/javascripts/discourse/models/rest.js.es6 new file mode 100644 index 000000000..d85475644 --- /dev/null +++ b/app/assets/javascripts/discourse/models/rest.js.es6 @@ -0,0 +1,20 @@ +export default Ember.Object.extend({ + update(attrs) { + const self = this, + type = this.get('__type'); + return this.store.update(type, this.get('id'), attrs).then(function(result) { + if (result && result[type]) { + Object.keys(result).forEach(function(k) { + attrs[k] = result[k]; + }); + } + self.setProperties(attrs); + return result; + }); + }, + + destroyRecord() { + const type = this.get('__type'); + return this.store.destroyRecord(type, this); + } +}); diff --git a/app/assets/javascripts/discourse/models/result-set.js.es6 b/app/assets/javascripts/discourse/models/result-set.js.es6 new file mode 100644 index 000000000..210a83e4a --- /dev/null +++ b/app/assets/javascripts/discourse/models/result-set.js.es6 @@ -0,0 +1,22 @@ +export default Ember.ArrayProxy.extend({ + loading: false, + loadingMore: false, + totalRows: 0, + + loadMore() { + const loadMoreUrl = this.get('loadMoreUrl'); + if (!loadMoreUrl) { return; } + + const totalRows = this.get('totalRows'); + if (this.get('length') < totalRows && !this.get('loadingMore')) { + this.set('loadingMore', true); + + const self = this; + return this.store.appendResults(this, this.get('__type'), loadMoreUrl).then(function() { + self.set('loadingMore', false); + }); + } + + return Ember.RSVP.resolve(); + } +}); diff --git a/app/assets/javascripts/discourse/models/store.js.es6 b/app/assets/javascripts/discourse/models/store.js.es6 index a55d8849a..72f36db37 100644 --- a/app/assets/javascripts/discourse/models/store.js.es6 +++ b/app/assets/javascripts/discourse/models/store.js.es6 @@ -1,40 +1,49 @@ +import RestModel from 'discourse/models/rest'; +import ResultSet from 'discourse/models/result-set'; + const _identityMap = {}; -const RestModel = Ember.Object.extend({ - update(attrs) { - const self = this, - type = this.get('__type'); - return this.store.update(type, this.get('id'), attrs).then(function(result) { - if (result && result[type]) { - Object.keys(result).forEach(function(k) { - attrs[k] = result[k]; - }); - } - self.setProperties(attrs); - return result; - }); +export default Ember.Object.extend({ + pluralize(thing) { + return thing + "s"; }, - destroyRecord() { - const type = this.get('__type'); - return this.store.destroyRecord(type, this); - } -}); - -export default Ember.Object.extend({ findAll(type) { const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest'); const self = this; return adapter.findAll(this, type).then(function(result) { - return result[Ember.String.underscore(type + 's')].map(obj => self._hydrate(type, obj)); + return self._resultSet(type, result); }); }, - find(type, id) { + find(type, findArgs) { const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest'); const self = this; - return adapter.find(this, type, id).then(function(result) { - return self._hydrate(type, result[Ember.String.underscore(type)]); + return adapter.find(this, type, findArgs).then(function(result) { + if (typeof findArgs === "object") { + return self._resultSet(type, result); + } else { + return self._hydrate(type, result[Ember.String.underscore(type)]); + } + }); + }, + + appendResults(resultSet, type, url) { + const self = this; + + return Discourse.ajax(url).then(function(result) { + const typeName = Ember.String.underscore(self.pluralize(type)), + totalRows = result["total_rows_" + typeName] || result.get('totalRows'), + loadMoreUrl = result["load_more_" + typeName], + content = result[typeName].map(obj => self._hydrate(type, obj)); + + resultSet.setProperties({ totalRows, loadMoreUrl }); + resultSet.get('content').pushObjects(content); + + // If we've loaded them all, clear the load more URL + if (resultSet.get('length') >= totalRows) { + resultSet.set('loadMoreUrl', null); + } }); }, @@ -63,6 +72,15 @@ export default Ember.Object.extend({ }); }, + _resultSet(type, result) { + const typeName = Ember.String.underscore(this.pluralize(type)), + content = result[typeName].map(obj => this._hydrate(type, obj)), + totalRows = result["total_rows_" + typeName] || content.length, + loadMoreUrl = result["load_more_" + typeName]; + + return ResultSet.create({ content, totalRows, loadMoreUrl, store: this, __type: type }); + }, + _hydrate(type, obj) { if (!obj) { throw "Can't hydrate " + type + " of `null`"; } if (!obj.id) { throw "Can't hydrate " + type + " without an `id`"; } diff --git a/app/assets/javascripts/discourse/models/top-period.js.es6 b/app/assets/javascripts/discourse/models/top-period.js.es6 deleted file mode 100644 index ad4323b68..000000000 --- a/app/assets/javascripts/discourse/models/top-period.js.es6 +++ /dev/null @@ -1,32 +0,0 @@ -export default Ember.Object.extend({ - title: null, - - availablePeriods: function() { - var periods = this.get('periods'); - if (!periods) { return; } - - var self = this; - return periods.filter(function(p) { - return p !== self; - }); - }.property('showMoreUrl'), - - _createTitle: function() { - var id = this.get('id'); - if (id) { - var title = "this_week"; - if (id === "yearly") { - title = "this_year"; - } else if (id === "monthly") { - title = "this_month"; - } else if (id === "daily") { - title = "today"; - } else if (id === "all") { - title = "all"; - } - - this.set('title', I18n.t("filters.top." + title)); - } - }.on('init') - -}); diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 index f73f132b1..e6adf8630 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -11,6 +11,10 @@ export default function() { }); this.resource('topicBySlug', { path: '/t/:slug' }); + this.resource('directory', function() { + this.route('show', {path: '/:period'}); + }); + this.resource('discovery', { path: '/' }, function() { // top this.route('top'); diff --git a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 index e3567ef85..8113104ea 100644 --- a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 @@ -67,14 +67,13 @@ export default function(filter, params) { setupController: function(controller, model) { var topics = this.get('topics'), - periods = this.controllerFor('discovery').get('periods'), periodId = topics.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : ''); this.controllerFor('navigation/category').set('canCreateTopic', topics.get('can_create_topic')); this.controllerFor('discovery/topics').setProperties({ model: topics, category: model, - period: periods.findBy('id', periodId), + period: periodId, selected: [], noSubcategories: params && !!params.no_subcategories, order: topics.get('params.order'), diff --git a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 index 84127c3a8..4b21e9b0b 100644 --- a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 @@ -45,13 +45,11 @@ export default function(filter, extras) { }))); } - const periods = this.controllerFor('discovery').get('periods'), - periodId = model.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : ''); - + const period = model.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : ''); const topicOpts = { model, category: null, - period: periods.findBy('id', periodId), + period, selected: [], expandGloballyPinned: true }; diff --git a/app/assets/javascripts/discourse/routes/directory-index.js.es6 b/app/assets/javascripts/discourse/routes/directory-index.js.es6 new file mode 100644 index 000000000..cc8c80730 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/directory-index.js.es6 @@ -0,0 +1,6 @@ +export default Discourse.Route.extend({ + beforeModel: function() { + this.controllerFor('directory-show').setProperties({ sort: null, asc: null }); + this.replaceWith('directory.show', 'all'); + } +}); diff --git a/app/assets/javascripts/discourse/routes/directory-show.js.es6 b/app/assets/javascripts/discourse/routes/directory-show.js.es6 new file mode 100644 index 000000000..fc319f689 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/directory-show.js.es6 @@ -0,0 +1,31 @@ +export default Discourse.Route.extend({ + queryParams: { + order: { refreshModel: true }, + asc: { refreshModel: true }, + }, + + model(params) { + // If we refresh via `refreshModel` set the old model to loading + const existing = this.modelFor('directory-show'); + if (existing) { + existing.set('loading', true); + } + + this._period = params.period; + return this.store.find('directoryItem', { + id: params.period, + asc: params.asc, + order: params.order + }); + }, + + setupController(controller, model) { + controller.setProperties({ model, period: this._period }); + }, + + actions: { + changePeriod(period) { + this.transitionTo('directory.show', period); + } + } +}); diff --git a/app/assets/javascripts/discourse/templates/components/period-chooser.hbs b/app/assets/javascripts/discourse/templates/components/period-chooser.hbs new file mode 100644 index 000000000..8f7fddc94 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/period-chooser.hbs @@ -0,0 +1,11 @@ +
+ {{directory-toggle field="likes_received" order=order asc=asc}} + {{directory-toggle field="likes_given" order=order asc=asc}} + {{directory-toggle field="topic_count" order=order asc=asc}} + {{directory-toggle field="post_count" order=order asc=asc}} + {{directory-toggle field="topics_entered" order=order asc=asc}} + {{#if showTimeRead}} + | {{i18n "directory.time_read"}} | + {{/if}} + + + {{#each item in model}} +|||||
---|---|---|---|---|---|---|
+ {{avatar item imageSize="tiny"}} + {{#link-to 'user' item.username}}{{unbound item.username}}{{/link-to}} + | ++ {{fa-icon "heart"}} + {{number item.likes_received}} + | ++ {{fa-icon "heart"}} + {{number item.likes_given}} + | +{{number item.topic_count}} | +{{number item.post_count}} | +{{number item.topics_entered}} | + {{#if showTimeRead}} +{{unbound item.time_read}} | + {{/if}} +
{{i18n "directory.no_results"}}
+ {{/if}} +{{/loading-spinner}} diff --git a/app/assets/javascripts/discourse/templates/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/discovery/topics.hbs index 41c9ecd37..494da1abb 100644 --- a/app/assets/javascripts/discourse/templates/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/discovery/topics.hbs @@ -19,7 +19,7 @@