diff --git a/Gemfile.lock b/Gemfile.lock index 725ea9068b..d10b146ad8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -145,7 +145,7 @@ GEM thor (~> 0.15) libv8 (3.16.14.7) listen (0.7.3) - logster (0.8.4.1.pre) + logster (0.8.4.5.pre) lru_redux (1.1.0) mail (2.5.4) mime-types (~> 1.16) diff --git a/app/assets/javascripts/discourse/adapters/build-plugin.js.es6 b/app/assets/javascripts/admin/adapters/build-plugin.js.es6 similarity index 100% rename from app/assets/javascripts/discourse/adapters/build-plugin.js.es6 rename to app/assets/javascripts/admin/adapters/build-plugin.js.es6 diff --git a/app/assets/javascripts/discourse/adapters/user-field.js.es6 b/app/assets/javascripts/admin/adapters/customization-base.js.es6 similarity index 100% rename from app/assets/javascripts/discourse/adapters/user-field.js.es6 rename to app/assets/javascripts/admin/adapters/customization-base.js.es6 diff --git a/app/assets/javascripts/admin/adapters/site-text-type.js.es6 b/app/assets/javascripts/admin/adapters/site-text-type.js.es6 new file mode 100644 index 0000000000..b547b06f3c --- /dev/null +++ b/app/assets/javascripts/admin/adapters/site-text-type.js.es6 @@ -0,0 +1,2 @@ +import CustomizationBase from 'admin/adapters/customization-base'; +export default CustomizationBase; diff --git a/app/assets/javascripts/admin/adapters/site-text.js.es6 b/app/assets/javascripts/admin/adapters/site-text.js.es6 new file mode 100644 index 0000000000..b547b06f3c --- /dev/null +++ b/app/assets/javascripts/admin/adapters/site-text.js.es6 @@ -0,0 +1,2 @@ +import CustomizationBase from 'admin/adapters/customization-base'; +export default CustomizationBase; diff --git a/app/assets/javascripts/admin/adapters/user-field.js.es6 b/app/assets/javascripts/admin/adapters/user-field.js.es6 new file mode 100644 index 0000000000..b547b06f3c --- /dev/null +++ b/app/assets/javascripts/admin/adapters/user-field.js.es6 @@ -0,0 +1,2 @@ +import CustomizationBase from 'admin/adapters/customization-base'; +export default CustomizationBase; diff --git a/app/assets/javascripts/admin/components/admin-user-field-item.js.es6 b/app/assets/javascripts/admin/components/admin-user-field-item.js.es6 index bccec30932..d10280daf7 100644 --- a/app/assets/javascripts/admin/components/admin-user-field-item.js.es6 +++ b/app/assets/javascripts/admin/components/admin-user-field-item.js.es6 @@ -1,13 +1,14 @@ -import { bufferedProperty } from 'discourse/mixins/buffered-content'; import UserField from 'admin/models/user-field'; +import { bufferedProperty } from 'discourse/mixins/buffered-content'; import { popupAjaxError } from 'discourse/lib/ajax-error'; +import { propertyEqual } from 'discourse/lib/computed'; export default Ember.Component.extend(bufferedProperty('userField'), { editing: Ember.computed.empty('userField.id'), classNameBindings: [':user-field'], - cantMoveUp: Discourse.computed.propertyEqual('userField', 'firstField'), - cantMoveDown: Discourse.computed.propertyEqual('userField', 'lastField'), + cantMoveUp: propertyEqual('userField', 'firstField'), + cantMoveDown: propertyEqual('userField', 'lastField'), userFieldsDescription: function() { return I18n.t('admin.user_fields.description'); diff --git a/app/assets/javascripts/admin/components/customize-link.js.es6 b/app/assets/javascripts/admin/components/customize-link.js.es6 new file mode 100644 index 0000000000..79d8cc26f4 --- /dev/null +++ b/app/assets/javascripts/admin/components/customize-link.js.es6 @@ -0,0 +1,10 @@ +export default Ember.Component.extend({ + router: function() { + return this.container.lookup('router:main'); + }.property(), + + active: function() { + const id = this.get('customization.id'); + return this.get('router.url').indexOf(`/customize/css_html/${id}/css`) !== -1; + }.property('router.url', 'customization.id') +}); diff --git a/app/assets/javascripts/admin/components/group-member.js.es6 b/app/assets/javascripts/admin/components/group-member.js.es6 new file mode 100644 index 0000000000..abf0f2984a --- /dev/null +++ b/app/assets/javascripts/admin/components/group-member.js.es6 @@ -0,0 +1,9 @@ +export default Ember.Component.extend({ + classNames: ["item"], + + actions: { + remove() { + this.sendAction('removeAction', this.get('member')); + } + } +}); diff --git a/app/assets/javascripts/admin/components/site-setting.js.es6 b/app/assets/javascripts/admin/components/site-setting.js.es6 index c7375af4fc..10925e8a2e 100644 --- a/app/assets/javascripts/admin/components/site-setting.js.es6 +++ b/app/assets/javascripts/admin/components/site-setting.js.es6 @@ -1,20 +1,21 @@ import BufferedContent from 'discourse/mixins/buffered-content'; import ScrollTop from 'discourse/mixins/scroll-top'; import SiteSetting from 'admin/models/site-setting'; +import { propertyNotEqual } from 'discourse/lib/computed'; const CustomTypes = ['bool', 'enum', 'list', 'url_list', 'host_list']; export default Ember.Component.extend(BufferedContent, ScrollTop, { classNameBindings: [':row', ':setting', 'setting.overridden', 'typeClass'], content: Ember.computed.alias('setting'), - dirty: Discourse.computed.propertyNotEqual('buffered.value', 'setting.value'), + dirty: propertyNotEqual('buffered.value', 'setting.value'), validationMessage: null, preview: function() { const preview = this.get('setting.preview'); if (preview) { return new Handlebars.SafeString("
" + - preview.replace("{{value}}", this.get('buffered.value')) + + preview.replace(/\{\{value\}\}/g, this.get('buffered.value')) + "
"); } }.property('buffered.value'), diff --git a/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 index 8eceb0bb7d..fb052e6fc5 100644 --- a/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 @@ -1,20 +1,20 @@ export default Ember.ArrayController.extend({ needs: ["adminBackups"], status: Em.computed.alias("controllers.adminBackups"), - isOperationRunning: Em.computed.alias("status.isOperationRunning"), - restoreDisabled: Em.computed.alias("status.restoreDisabled"), + isOperationRunning: Ember.computed.alias("status.model.isOperationRunning"), + restoreDisabled: Ember.computed.alias("status.model.restoreDisabled"), uploadLabel: function() { return I18n.t("admin.backups.upload.label"); }.property(), restoreTitle: function() { - if (!this.get('status.allowRestore')) { + if (!this.get('status.model.allowRestore')) { return "admin.backups.operations.restore.is_disabled"; - } else if (this.get("status.isOperationRunning")) { + } else if (this.get("status.model.isOperationRunning")) { return "admin.backups.operations.is_running"; } else { return "admin.backups.operations.restore.title"; } - }.property("status.{allowRestore,isOperationRunning}"), + }.property("status.model.{allowRestore,isOperationRunning}"), actions: { diff --git a/app/assets/javascripts/admin/controllers/admin-backups.js.es6 b/app/assets/javascripts/admin/controllers/admin-backups.js.es6 index 64bb7ad105..05aa974150 100644 --- a/app/assets/javascripts/admin/controllers/admin-backups.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-backups.js.es6 @@ -1,5 +1,5 @@ export default Ember.ObjectController.extend({ - noOperationIsRunning: Em.computed.not("isOperationRunning"), - rollbackEnabled: Em.computed.and("canRollback", "restoreEnabled", "noOperationIsRunning"), - rollbackDisabled: Em.computed.not("rollbackEnabled") + noOperationIsRunning: Ember.computed.not("model.isOperationRunning"), + rollbackEnabled: Ember.computed.and("model.canRollback", "model.restoreEnabled", "noOperationIsRunning"), + rollbackDisabled: Ember.computed.not("rollbackEnabled") }); diff --git a/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 index ac648462b2..15ba6bc011 100644 --- a/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 @@ -1,5 +1,6 @@ import { popupAjaxError } from 'discourse/lib/ajax-error'; import BufferedContent from 'discourse/mixins/buffered-content'; +import { propertyNotEqual } from 'discourse/lib/computed'; export default Ember.ObjectController.extend(BufferedContent, { needs: ['admin-badges'], @@ -12,7 +13,7 @@ export default Ember.ObjectController.extend(BufferedContent, { protectedSystemFields: Em.computed.alias('controllers.admin-badges.protectedSystemFields'), readOnly: Ember.computed.alias('buffered.system'), - showDisplayName: Discourse.computed.propertyNotEqual('name', 'displayName'), + showDisplayName: propertyNotEqual('name', 'displayName'), canEditDescription: Em.computed.none('buffered.translatedDescription'), _resetSaving: function() { diff --git a/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 index 636905c009..4a6a58021b 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 @@ -1,13 +1,4 @@ -/** - This controller supports interface for creating custom CSS skins in Discourse. - - @class AdminCustomizeColorsController - @extends Ember.Controller - @namespace Discourse - @module Discourse -**/ export default Ember.ArrayController.extend({ - onlyOverridden: false, baseColorScheme: function() { diff --git a/app/assets/javascripts/admin/controllers/admin-customize-css-html-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-css-html-show.js.es6 new file mode 100644 index 0000000000..03c855a969 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-customize-css-html-show.js.es6 @@ -0,0 +1,79 @@ +import { url } from 'discourse/lib/computed'; + +const sections = ['css', 'header', 'top', 'footer', 'head-tag', 'body-tag', + 'mobile-css', 'mobile-header', 'mobile-top', 'mobile-footer', + 'embedded-css']; + +const activeSections = {}; +sections.forEach(function(s) { + activeSections[Ember.String.camelize(s) + "Active"] = Ember.computed.equal('section', s); +}); + + +export default Ember.Controller.extend(activeSections, { + maximized: false, + section: null, + + previewUrl: url("model.key", "/?preview-style=%@"), + downloadUrl: url('model.id', '/admin/site_customizations/%@'), + + mobile: function() { + return this.get('section').indexOf('mobile-') === 0; + }.property('section'), + + maximizeIcon: function() { + return this.get('maximized') ? 'compress' : 'expand'; + }.property('maximized'), + + saveButtonText: function() { + return this.get('model.isSaving') ? I18n.t('saving') : I18n.t('admin.customize.save'); + }.property('model.isSaving'), + + saveDisabled: function() { + return !this.get('model.changed') || this.get('model.isSaving'); + }.property('model.changed', 'model.isSaving'), + + needs: ['adminCustomizeCssHtml'], + + undoPreviewUrl: url('/?preview-style='), + defaultStyleUrl: url('/?preview-style=default'), + + actions: { + save() { + this.get('model').saveChanges(); + }, + + destroy() { + const self = this; + return bootbox.confirm(I18n.t("admin.customize.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) { + if (result) { + const model = self.get('model'); + model.destroyRecord().then(function() { + self.get('controllers.adminCustomizeCssHtml').get('model').removeObject(model); + self.transitionToRoute('adminCustomizeCssHtml'); + }); + } + }); + }, + + toggleMaximize: function() { + this.toggleProperty('maximized'); + }, + + toggleMobile: function() { + const section = this.get('section'); + + // Try to send to the same tab as before + let dest; + if (this.get('mobile')) { + dest = section.replace('mobile-', ''); + if (sections.indexOf(dest) === -1) { dest = 'css'; } + } else { + dest = 'mobile-' + section; + if (sections.indexOf(dest) === -1) { dest = 'mobile-css'; } + } + this.replaceRoute('adminCustomizeCssHtml.show', this.get('model.id'), dest); + } + } + +}); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-css-html.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-css-html.js.es6 deleted file mode 100644 index 08e4e3215b..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-customize-css-html.js.es6 +++ /dev/null @@ -1,77 +0,0 @@ -import showModal from 'discourse/lib/show-modal'; - -/** - This controller supports interface for creating custom CSS skins in Discourse. - - @class AdminCustomizeCssHtmlController - @extends Ember.Controller - @namespace Discourse - @module Discourse -**/ -export default Ember.ArrayController.extend({ - - undoPreviewUrl: function() { - return Discourse.getURL("/?preview-style="); - }.property(), - - defaultStyleUrl: function() { - return Discourse.getURL("/?preview-style=default"); - }.property(), - - actions: { - - /** - Create a new customization style - - @method newCustomization - **/ - newCustomization: function() { - var item = Discourse.SiteCustomization.create({name: I18n.t("admin.customize.new_style")}); - this.pushObject(item); - this.set('selectedItem', item); - }, - - importModal: function() { - showModal('upload-customization'); - }, - - /** - Select a given style - - @method selectStyle - @param {Discourse.SiteCustomization} style The style we are selecting - **/ - selectStyle: function(style) { - this.set('selectedItem', style); - }, - - /** - Save the current customization - - @method save - **/ - save: function() { - this.get('selectedItem').save(); - }, - - /** - Destroy the current customization - - @method destroy - **/ - destroy: function() { - var _this = this; - return bootbox.confirm(I18n.t("admin.customize.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) { - var selected; - if (result) { - selected = _this.get('selectedItem'); - selected.destroy(); - _this.set('selectedItem', null); - return _this.removeObject(selected); - } - }); - } - - } - -}); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 index ad572337e0..ab304adc7a 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 @@ -1,17 +1,12 @@ -/** - This controller supports the default interface when you enter the admin section. +import { setting } from 'discourse/lib/computed'; - @class AdminDashboardController - @extends Ember.Controller - @namespace Discourse - @module Discourse -**/ +// This controller supports the default interface when you enter the admin section. export default Ember.Controller.extend({ loading: true, versionCheck: null, problemsCheckMinutes: 1, - showVersionChecks: Discourse.computed.setting('version_checks'), + showVersionChecks: setting('version_checks'), foundProblems: function() { return(Discourse.User.currentProp('admin') && this.get('problems') && this.get('problems').length > 0); diff --git a/app/assets/javascripts/admin/controllers/admin-group.js.es6 b/app/assets/javascripts/admin/controllers/admin-group.js.es6 index 2256714084..93213e2c66 100644 --- a/app/assets/javascripts/admin/controllers/admin-group.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-group.js.es6 @@ -1,21 +1,22 @@ import { popupAjaxError } from 'discourse/lib/ajax-error'; +import { propertyEqual } from 'discourse/lib/computed'; export default Em.ObjectController.extend({ needs: ['adminGroupsType'], disableSave: false, currentPage: function() { - if (this.get("user_count") === 0) { return 0; } - return Math.floor(this.get("offset") / this.get("limit")) + 1; - }.property("limit", "offset", "user_count"), + if (this.get("model.user_count") === 0) { return 0; } + return Math.floor(this.get("model.offset") / this.get("model.limit")) + 1; + }.property("model.limit", "model.offset", "model.user_count"), totalPages: function() { - if (this.get("user_count") === 0) { return 0; } - return Math.floor(this.get("user_count") / this.get("limit")) + 1; - }.property("limit", "user_count"), + if (this.get("model.user_count") === 0) { return 0; } + return Math.floor(this.get("model.user_count") / this.get("model.limit")) + 1; + }.property("model.limit", "model.user_count"), showingFirst: Em.computed.lte("currentPage", 1), - showingLast: Discourse.computed.propertyEqual("currentPage", "totalPages"), + showingLast: propertyEqual("currentPage", "totalPages"), aliasLevelOptions: function() { return [ @@ -31,7 +32,7 @@ export default Em.ObjectController.extend({ if (this.get("showingLast")) { return; } const group = this.get("model"), - offset = Math.min(group.get("offset") + group.get("limit"), group.get("user_count")); + offset = Math.min(group.get("offset") + group.get("model.limit"), group.get("user_count")); group.set("offset", offset); @@ -42,7 +43,7 @@ export default Em.ObjectController.extend({ if (this.get("showingFirst")) { return; } const group = this.get("model"), - offset = Math.max(group.get("offset") - group.get("limit"), 0); + offset = Math.max(group.get("offset") - group.get("model.limit"), 0); group.set("offset", offset); @@ -51,7 +52,7 @@ export default Em.ObjectController.extend({ removeMember(member) { const self = this, - message = I18n.t("admin.groups.delete_member_confirm", { username: member.get("username"), group: this.get("name") }); + message = I18n.t("admin.groups.delete_member_confirm", { username: member.get("username"), group: this.get("model.name") }); return bootbox.confirm(message, I18n.t("no_value"), I18n.t("yes_value"), function(confirm) { if (confirm) { self.get("model").removeMember(member); @@ -60,10 +61,9 @@ export default Em.ObjectController.extend({ }, addMembers() { - if (Em.isEmpty(this.get("usernames"))) { return; } - this.get("model").addMembers(this.get("usernames")); - // clear the user selector - this.set("usernames", null); + if (Em.isEmpty(this.get("model.usernames"))) { return; } + this.get("model").addMembers(this.get("model.usernames")).catch(popupAjaxError); + this.set("model.usernames", null); }, save() { diff --git a/app/assets/javascripts/admin/controllers/admin-site-text-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-text-edit.js.es6 index c95db1e8b8..943317b1bf 100644 --- a/app/assets/javascripts/admin/controllers/admin-site-text-edit.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-site-text-edit.js.es6 @@ -1,20 +1,16 @@ -export default Ember.ObjectController.extend({ - saving: false, +export default Ember.Controller.extend({ saved: false, saveDisabled: function() { - if (this.get('saving')) { return true; } - if ((!this.get('allow_blank')) && Ember.isEmpty(this.get('value'))) { return true; } + if (this.get('model.isSaving')) { return true; } + if ((!this.get('allow_blank')) && Ember.isEmpty(this.get('model.value'))) { return true; } return false; - }.property('saving', 'value'), + }.property('model.iSaving', 'model.value'), actions: { - saveChanges: function() { - var self = this; - self.setProperties({saving: true, saved: false}); - self.get('model').save().then(function () { - self.setProperties({saving: false, saved: true}); - }); + saveChanges() { + const model = this.get('model'); + model.save(model.getProperties('value')).then(() => this.set('saved', true)); } } }); diff --git a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 index b544aafc63..339f34e247 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 @@ -1,15 +1,16 @@ import ObjectController from 'discourse/controllers/object'; import CanCheckEmails from 'discourse/mixins/can-check-emails'; +import { propertyNotEqual, setting } from 'discourse/lib/computed'; export default ObjectController.extend(CanCheckEmails, { editingTitle: false, originalPrimaryGroupId: null, availableGroups: null, - showApproval: Discourse.computed.setting('must_approve_users'), - showBadges: Discourse.computed.setting('enable_badges'), + showApproval: setting('must_approve_users'), + showBadges: setting('enable_badges'), - primaryGroupDirty: Discourse.computed.propertyNotEqual('originalPrimaryGroupId', 'model.primary_group_id'), + primaryGroupDirty: propertyNotEqual('originalPrimaryGroupId', 'model.primary_group_id'), automaticGroups: function() { return this.get("model.automaticGroups").map((g) => g.name).join(", "); diff --git a/app/assets/javascripts/admin/controllers/admin-users-list-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-users-list-show.js.es6 index 708207f84a..dcca2e31ba 100644 --- a/app/assets/javascripts/admin/controllers/admin-users-list-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-users-list-show.js.es6 @@ -1,3 +1,5 @@ +import { i18n } from 'discourse/lib/computed'; + export default Ember.ArrayController.extend({ query: null, showEmails: false, @@ -9,7 +11,7 @@ export default Ember.ArrayController.extend({ queryPending: Em.computed.equal('query', 'pending'), queryHasApproval: Em.computed.or('queryNew', 'queryPending'), showApproval: Em.computed.and('siteSettings.must_approve_users', 'queryHasApproval'), - searchHint: Discourse.computed.i18n('search_hint'), + searchHint: i18n('search_hint'), hasSelection: Em.computed.gt('selectedCount', 0), selectedCount: function() { diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index 8416f53585..7cda05c820 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -1,3 +1,4 @@ +import { propertyNotEqual } from 'discourse/lib/computed'; import { popupAjaxError } from 'discourse/lib/ajax-error'; const AdminUser = Discourse.User.extend({ @@ -144,7 +145,7 @@ const AdminUser = Discourse.User.extend({ this.set('originalTrustLevel', this.get('trust_level')); }, - dirty: Discourse.computed.propertyNotEqual('originalTrustLevel', 'trustLevel.id'), + dirty: propertyNotEqual('originalTrustLevel', 'trustLevel.id'), saveTrustLevel() { return Discourse.ajax("/admin/users/" + this.id + "/trust_level", { diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 index 1ce48a1448..a28601d0ce 100644 --- a/app/assets/javascripts/admin/models/report.js.es6 +++ b/app/assets/javascripts/admin/models/report.js.es6 @@ -1,7 +1,8 @@ import round from "discourse/lib/round"; +import { fmt } from 'discourse/lib/computed'; const Report = Discourse.Model.extend({ - reportUrl: Discourse.computed.fmt("type", "/admin/reports/%@"), + reportUrl: fmt("type", "/admin/reports/%@"), valueAt(numDaysAgo) { if (this.data) { @@ -26,7 +27,7 @@ const Report = Discourse.Model.extend({ count++; } }); - if (this.get("method") === "average") { sum /= count; } + if (this.get("method") === "average" && count > 0) { sum /= count; } return round(sum, -2); } }, diff --git a/app/assets/javascripts/admin/models/site-customization.js.es6 b/app/assets/javascripts/admin/models/site-customization.js.es6 new file mode 100644 index 0000000000..fe2176bf11 --- /dev/null +++ b/app/assets/javascripts/admin/models/site-customization.js.es6 @@ -0,0 +1,31 @@ +import RestModel from 'discourse/models/rest'; + +const trackedProperties = [ + 'enabled', 'name', 'stylesheet', 'header', 'top', 'footer', 'mobile_stylesheet', + 'mobile_header', 'mobile_top', 'mobile_footer', 'head_tag', 'body_tag', 'embedded_css' +]; + +function changed() { + const originals = this.get('originals'); + if (!originals) { return false; } + return _.some(trackedProperties, (p) => originals[p] !== this.get(p)); +} + +const SiteCustomization = RestModel.extend({ + description: function() { + return "" + this.name + (this.enabled ? ' (*)' : ''); + }.property('selected', 'name', 'enabled'), + + changed: changed.property.apply(changed, trackedProperties.concat('originals')), + + startTrackingChanges: function() { + this.set('originals', this.getProperties(trackedProperties)); + }.on('init'), + + saveChanges() { + return this.save(this.getProperties(trackedProperties)).then(() => this.startTrackingChanges()); + }, + +}); + +export default SiteCustomization; diff --git a/app/assets/javascripts/admin/models/site-text-type.js.es6 b/app/assets/javascripts/admin/models/site-text-type.js.es6 new file mode 100644 index 0000000000..7cb1171e90 --- /dev/null +++ b/app/assets/javascripts/admin/models/site-text-type.js.es6 @@ -0,0 +1,2 @@ +import RestModel from 'discourse/models/rest'; +export default RestModel.extend(); diff --git a/app/assets/javascripts/admin/models/site-text.js.es6 b/app/assets/javascripts/admin/models/site-text.js.es6 new file mode 100644 index 0000000000..edbaf2a444 --- /dev/null +++ b/app/assets/javascripts/admin/models/site-text.js.es6 @@ -0,0 +1,8 @@ +import RestModel from 'discourse/models/rest'; + +export default RestModel.extend({ + markdown: Em.computed.equal('format', 'markdown'), + plainText: Em.computed.equal('format', 'plain'), + html: Em.computed.equal('format', 'html'), + css: Em.computed.equal('format', 'css'), +}); diff --git a/app/assets/javascripts/admin/models/site_customization.js b/app/assets/javascripts/admin/models/site_customization.js deleted file mode 100644 index 19fcbad266..0000000000 --- a/app/assets/javascripts/admin/models/site_customization.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - Our data model for interacting with site customizations. - - @class SiteCustomization - @extends Discourse.Model - @namespace Discourse - @module Discourse -**/ -Discourse.SiteCustomization = Discourse.Model.extend({ - trackedProperties: [ - 'enabled', 'name', - 'stylesheet', 'header', 'top', 'footer', - 'mobile_stylesheet', 'mobile_header', 'mobile_top', 'mobile_footer', - 'head_tag', 'body_tag' - ], - - description: function() { - return "" + this.name + (this.enabled ? ' (*)' : ''); - }.property('selected', 'name', 'enabled'), - - changed: function() { - var self = this; - - if (!this.originals) { return false; } - - var changed = _.some(this.trackedProperties, function (p) { - return self.originals[p] !== self.get(p); - }); - - if (changed) { this.set('savingStatus', ''); } - - return changed; - }.property('enabled', 'name', 'originals', - 'stylesheet', 'header', 'top', 'footer', - 'mobile_stylesheet', 'mobile_header', 'mobile_top', 'mobile_footer', - 'head_tag', 'body_tag'), - - startTrackingChanges: function() { - var self = this; - var originals = {}; - _.each(this.trackedProperties, function (prop) { - originals[prop] = self.get(prop); - }); - this.set('originals', originals); - }.on('init'), - - previewUrl: function() { return Discourse.getURL("/?preview-style=" + this.get('key')); }.property('key'), - disableSave: function() { return !this.get('changed') || this.get('saving'); }.property('changed'), - - save: function() { - this.set('savingStatus', I18n.t('saving')); - this.set('saving',true); - - var data = { - name: this.name, - enabled: this.enabled, - stylesheet: this.stylesheet, - header: this.header, - top: this.top, - footer: this.footer, - mobile_stylesheet: this.mobile_stylesheet, - mobile_header: this.mobile_header, - mobile_top: this.mobile_top, - mobile_footer: this.mobile_footer, - head_tag: this.head_tag, - body_tag: this.body_tag - }; - - var siteCustomization = this; - return Discourse.ajax("/admin/site_customizations" + (this.id ? '/' + this.id : ''), { - data: { site_customization: data }, - type: this.id ? 'PUT' : 'POST' - }).then(function (result) { - if (!siteCustomization.id) { - siteCustomization.set('id', result.id); - siteCustomization.set('key', result.key); - } - siteCustomization.set('savingStatus', I18n.t('saved')); - siteCustomization.set('saving',false); - siteCustomization.startTrackingChanges(); - return siteCustomization; - }); - }, - - destroy: function() { - if (!this.id) return; - return Discourse.ajax("/admin/site_customizations/" + this.id, { type: 'DELETE' }); - }, - - download_url: function() { - return Discourse.getURL('/admin/site_customizations/' + this.id); - }.property('id') -}); - -var SiteCustomizations = Ember.ArrayProxy.extend({ - selectedItemChanged: function() { - var selected = this.get('selectedItem'); - _.each(this.get('content'), function (i) { - i.set('selected', selected === i); - }); - }.observes('selectedItem') -}); - -Discourse.SiteCustomization.reopenClass({ - findAll: function() { - return Discourse.ajax("/admin/site_customizations").then(function (data) { - var content = []; - if (data) { - content = data.site_customizations.map(function(c) { - return Discourse.SiteCustomization.create(c); - }); - } - return SiteCustomizations.create({ content: content }); - }); - } -}); diff --git a/app/assets/javascripts/admin/models/site_text.js b/app/assets/javascripts/admin/models/site_text.js deleted file mode 100644 index 0d671c4334..0000000000 --- a/app/assets/javascripts/admin/models/site_text.js +++ /dev/null @@ -1,21 +0,0 @@ -Discourse.SiteText = Discourse.Model.extend({ - markdown: Em.computed.equal('format', 'markdown'), - plainText: Em.computed.equal('format', 'plain'), - html: Em.computed.equal('format', 'html'), - css: Em.computed.equal('format', 'css'), - - save: function() { - return Discourse.ajax("/admin/customize/site_text/" + this.get('text_type'), { - type: 'PUT', - data: {value: this.get('value')} - }); - } -}); - -Discourse.SiteText.reopenClass({ - find: function(type) { - return Discourse.ajax("/admin/customize/site_text/" + type).then(function (data) { - return Discourse.SiteText.create(data.site_text); - }); - } -}); diff --git a/app/assets/javascripts/admin/models/site_text_type.js b/app/assets/javascripts/admin/models/site_text_type.js deleted file mode 100644 index 1a181bd8e8..0000000000 --- a/app/assets/javascripts/admin/models/site_text_type.js +++ /dev/null @@ -1,11 +0,0 @@ -Discourse.SiteTextType = Discourse.Model.extend(); - -Discourse.SiteTextType.reopenClass({ - findAll: function() { - return Discourse.ajax("/admin/customize/site_text_types").then(function(data) { - return data.map(function(ct) { - return Discourse.SiteTextType.create(ct); - }); - }); - } -}); diff --git a/app/assets/javascripts/admin/models/user-field.js.es6 b/app/assets/javascripts/admin/models/user-field.js.es6 index c6a1a1f6be..9e657f2204 100644 --- a/app/assets/javascripts/admin/models/user-field.js.es6 +++ b/app/assets/javascripts/admin/models/user-field.js.es6 @@ -1,9 +1,10 @@ import RestModel from 'discourse/models/rest'; +import { i18n } from 'discourse/lib/computed'; const UserField = RestModel.extend(); const UserFieldType = Ember.Object.extend({ - name: Discourse.computed.i18n('id', 'admin.user_fields.field_types.%@') + name: i18n('id', 'admin.user_fields.field_types.%@') }); UserField.reopenClass({ diff --git a/app/assets/javascripts/admin/routes/admin-api.js.es6 b/app/assets/javascripts/admin/routes/admin-api.js.es6 new file mode 100644 index 0000000000..82212c9657 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-api.js.es6 @@ -0,0 +1,5 @@ +export default Ember.Route.extend({ + model() { + return Discourse.ApiKey.find(); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-backups-index.js.es6 b/app/assets/javascripts/admin/routes/admin-backups-index.js.es6 new file mode 100644 index 0000000000..651551f857 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-backups-index.js.es6 @@ -0,0 +1,5 @@ +export default Ember.Route.extend({ + model() { + return Discourse.Backup.find(); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin_backups_logs_route.js b/app/assets/javascripts/admin/routes/admin-backups-logs.js.es6 similarity index 71% rename from app/assets/javascripts/admin/routes/admin_backups_logs_route.js rename to app/assets/javascripts/admin/routes/admin-backups-logs.js.es6 index e1f3101d69..8a6d32fddf 100644 --- a/app/assets/javascripts/admin/routes/admin_backups_logs_route.js +++ b/app/assets/javascripts/admin/routes/admin-backups-logs.js.es6 @@ -1,15 +1,15 @@ -Discourse.AdminBackupsLogsRoute = Discourse.Route.extend({ +export default Ember.Route.extend({ // since the logs are pushed via the message bus // we only want to preload them (hence the beforeModel hook) - beforeModel: function() { - var logsController = this.controllerFor("adminBackupsLogs"); + beforeModel() { + const logsController = this.controllerFor("adminBackupsLogs"); // preload the logs if any PreloadStore.getAndRemove("logs").then(function (preloadedLogs) { if (preloadedLogs && preloadedLogs.length) { // we need to filter out message like: "[SUCCESS]" // and convert POJOs to Ember Objects - var logs = _.chain(preloadedLogs) + const logs = _.chain(preloadedLogs) .reject(function (log) { return log.message.length === 0 || log.message[0] === "["; }) .map(function (log) { return Em.Object.create(log); }) .value(); @@ -18,6 +18,6 @@ Discourse.AdminBackupsLogsRoute = Discourse.Route.extend({ }); }, - setupController: function() { /* prevent default behavior */ } + setupController() { /* prevent default behavior */ } }); diff --git a/app/assets/javascripts/admin/routes/admin-backups.js.es6 b/app/assets/javascripts/admin/routes/admin-backups.js.es6 index 981dc5394b..d3b632e525 100644 --- a/app/assets/javascripts/admin/routes/admin-backups.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-backups.js.es6 @@ -10,14 +10,14 @@ export default Discourse.Route.extend({ _processLogMessage(log) { if (log.message === "[STARTED]") { - this.controllerFor("adminBackups").set("isOperationRunning", true); + this.controllerFor("adminBackups").set("model.isOperationRunning", true); this.controllerFor("adminBackupsLogs").clear(); } else if (log.message === "[FAILED]") { - this.controllerFor("adminBackups").set("isOperationRunning", false); + this.controllerFor("adminBackups").set("model.isOperationRunning", false); bootbox.alert(I18n.t("admin.backups.operations.failed", { operation: log.operation })); } else if (log.message === "[SUCCESS]") { Discourse.User.currentProp("hideReadOnlyAlert", false); - this.controllerFor("adminBackups").set("isOperationRunning", false); + this.controllerFor("adminBackups").set("model.isOperationRunning", false); if (log.operation === "restore") { // redirect to homepage when the restore is done (session might be lost) window.location.pathname = Discourse.getURL("/"); @@ -30,7 +30,7 @@ export default Discourse.Route.extend({ model() { return PreloadStore.getAndRemove("operations_status", function() { return Discourse.ajax("/admin/backups/status.json"); - }).then(function (status) { + }).then(status => { return Discourse.BackupStatus.create({ isOperationRunning: status.is_operation_running, canRollback: status.can_rollback, @@ -82,7 +82,7 @@ export default Discourse.Route.extend({ Discourse.User.currentProp("hideReadOnlyAlert", true); backup.restore().then(function() { self.controllerFor("adminBackupsLogs").clear(); - self.modelFor("adminBackups").set("isOperationRunning", true); + self.modelFor("adminBackups").set("model.isOperationRunning", true); self.transitionTo("admin.backups.logs"); }); } @@ -99,7 +99,7 @@ export default Discourse.Route.extend({ function(confirmed) { if (confirmed) { Discourse.Backup.cancel().then(function() { - self.controllerFor("adminBackups").set("isOperationRunning", false); + self.controllerFor("adminBackups").set("model.isOperationRunning", false); }); } } diff --git a/app/assets/javascripts/admin/routes/admin-customize-colors.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-colors.js.es6 new file mode 100644 index 0000000000..b0f4745a38 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-customize-colors.js.es6 @@ -0,0 +1,12 @@ +export default Ember.Route.extend({ + + model() { + return Discourse.ColorScheme.findAll(); + }, + + deactivate() { + this._super(); + this.controllerFor('adminCustomizeColors').set('selectedItem', null); + }, + +}); diff --git a/app/assets/javascripts/admin/routes/admin-customize-css-html-show.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-css-html-show.js.es6 new file mode 100644 index 0000000000..dc38f3c51f --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-customize-css-html-show.js.es6 @@ -0,0 +1,11 @@ +export default Ember.Route.extend({ + model(params) { + const all = this.modelFor('adminCustomizeCssHtml'); + const model = all.findProperty('id', parseInt(params.site_customization_id)); + return model ? { model, section: params.section } : this.replaceWith('adminCustomizeCssHtml.index'); + }, + + setupController(controller, hash) { + controller.setProperties(hash); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-customize-css-html.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-css-html.js.es6 new file mode 100644 index 0000000000..5bbb460959 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-customize-css-html.js.es6 @@ -0,0 +1,26 @@ +import showModal from 'discourse/lib/show-modal'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; + +export default Ember.Route.extend({ + model() { + return this.store.findAll('site-customization'); + }, + + actions: { + importModal() { + showModal('upload-customization'); + }, + + newCustomization(obj) { + obj = obj || {name: I18n.t("admin.customize.new_style")}; + const item = this.store.createRecord('site-customization'); + + const all = this.modelFor('adminCustomizeCssHtml'); + const self = this; + item.save(obj).then(function() { + all.pushObject(item); + self.transitionTo('adminCustomizeCssHtml.show', item.get('id'), 'css'); + }).catch(popupAjaxError); + } + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-customize-index.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-index.js.es6 new file mode 100644 index 0000000000..725b9fa8dd --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-customize-index.js.es6 @@ -0,0 +1,5 @@ +export default Ember.Route.extend({ + beforeModel() { + this.replaceWith('adminCustomize.colors'); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-group.js.es6 b/app/assets/javascripts/admin/routes/admin-group.js.es6 index f397297767..fd56aeca52 100644 --- a/app/assets/javascripts/admin/routes/admin-group.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-group.js.es6 @@ -11,9 +11,7 @@ export default Discourse.Route.extend({ setupController: function(controller, model) { controller.set("model", model); - // clear the user selector - controller.set("usernames", null); - // load the members of the group + controller.set("model.usernames", null); model.findMembers(); } diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 index daf3b1ced6..047e5d51af 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -16,8 +16,12 @@ export default { this.resource('adminCustomize', { path: '/customize' } ,function() { this.route('colors'); - this.route('css_html'); - this.resource('adminSiteText', { path: '/site_text' }, function() { + + this.resource('adminCustomizeCssHtml', { path: 'css_html' }, function() { + this.route('show', {path: '/:site_customization_id/:section'}); + }); + + this.resource('adminSiteText', { path: '/site_texts' }, function() { this.route('edit', {path: '/:text_type'}); }); this.resource('adminUserFields', { path: '/user_fields' }); diff --git a/app/assets/javascripts/admin/routes/admin-site-text-edit.js.es6 b/app/assets/javascripts/admin/routes/admin-site-text-edit.js.es6 index 6eba6e76e1..847746d039 100644 --- a/app/assets/javascripts/admin/routes/admin-site-text-edit.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-site-text-edit.js.es6 @@ -1,5 +1,5 @@ export default Discourse.Route.extend({ - model: function(params) { - return Discourse.SiteText.find(params.text_type); + model(params) { + return this.store.find('site-text', params.text_type); } }); diff --git a/app/assets/javascripts/admin/routes/admin-site-text.js.es6 b/app/assets/javascripts/admin/routes/admin-site-text.js.es6 index e850fd6c6c..f69c3c3954 100644 --- a/app/assets/javascripts/admin/routes/admin-site-text.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-site-text.js.es6 @@ -1,5 +1,5 @@ export default Discourse.Route.extend({ - model: function() { - return Discourse.SiteTextType.findAll(); + model() { + return this.store.findAll('site-text-type'); } }); diff --git a/app/assets/javascripts/admin/routes/admin.js.es6 b/app/assets/javascripts/admin/routes/admin.js.es6 new file mode 100644 index 0000000000..c1b80b2df5 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin.js.es6 @@ -0,0 +1,16 @@ +export default Discourse.Route.extend({ + titleToken() { + return I18n.t('admin_title'); + }, + + activate() { + this.controllerFor("application").setProperties({ + showTop: false, + showFooter: false, + }); + }, + + deactivate() { + this.controllerFor("application").set("showTop", true); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin_api_route.js b/app/assets/javascripts/admin/routes/admin_api_route.js deleted file mode 100644 index e13085fa1a..0000000000 --- a/app/assets/javascripts/admin/routes/admin_api_route.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - Handles routes related to api - - @class AdminApiRoute - @extends Discourse.Route - @namespace Discourse - @module Discourse -**/ -Discourse.AdminApiRoute = Discourse.Route.extend({ - - model: function() { - return Discourse.ApiKey.find(); - } - -}); diff --git a/app/assets/javascripts/admin/routes/admin_backups_index_route.js b/app/assets/javascripts/admin/routes/admin_backups_index_route.js deleted file mode 100644 index 8ba5e6539f..0000000000 --- a/app/assets/javascripts/admin/routes/admin_backups_index_route.js +++ /dev/null @@ -1,7 +0,0 @@ -Discourse.AdminBackupsIndexRoute = Discourse.Route.extend({ - - model: function() { - return Discourse.Backup.find(); - } - -}); diff --git a/app/assets/javascripts/admin/routes/admin_customize_colors_route.js b/app/assets/javascripts/admin/routes/admin_customize_colors_route.js deleted file mode 100644 index 953031ab40..0000000000 --- a/app/assets/javascripts/admin/routes/admin_customize_colors_route.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - Handles routes related to colors customization - - @class AdminCustomizeColorsRoute - @extends Discourse.Route - @namespace Discourse - @module Discourse -**/ -Discourse.AdminCustomizeColorsRoute = Discourse.Route.extend({ - - model: function() { - return Discourse.ColorScheme.findAll(); - }, - - deactivate: function() { - this._super(); - this.controllerFor('adminCustomizeColors').set('selectedItem', null); - }, - -}); diff --git a/app/assets/javascripts/admin/routes/admin_customize_css_html_route.js b/app/assets/javascripts/admin/routes/admin_customize_css_html_route.js deleted file mode 100644 index a9684add0f..0000000000 --- a/app/assets/javascripts/admin/routes/admin_customize_css_html_route.js +++ /dev/null @@ -1,5 +0,0 @@ -Discourse.AdminCustomizeCssHtmlRoute = Discourse.Route.extend({ - model: function() { - return Discourse.SiteCustomization.findAll(); - } -}); diff --git a/app/assets/javascripts/admin/routes/admin_customize_route.js b/app/assets/javascripts/admin/routes/admin_customize_route.js deleted file mode 100644 index 5b6d853170..0000000000 --- a/app/assets/javascripts/admin/routes/admin_customize_route.js +++ /dev/null @@ -1,5 +0,0 @@ -Discourse.AdminCustomizeIndexRoute = Discourse.Route.extend({ - beforeModel: function() { - this.replaceWith('adminCustomize.colors'); - } -}); diff --git a/app/assets/javascripts/admin/routes/admin_route.js b/app/assets/javascripts/admin/routes/admin_route.js deleted file mode 100644 index 07fc597f03..0000000000 --- a/app/assets/javascripts/admin/routes/admin_route.js +++ /dev/null @@ -1,5 +0,0 @@ -Discourse.AdminRoute = Discourse.Route.extend({ - titleToken: function() { - return I18n.t('admin_title'); - } -}); diff --git a/app/assets/javascripts/admin/templates/backups.hbs b/app/assets/javascripts/admin/templates/backups.hbs index d274285b2b..7562754c08 100644 --- a/app/assets/javascripts/admin/templates/backups.hbs +++ b/app/assets/javascripts/admin/templates/backups.hbs @@ -6,7 +6,7 @@
- {{#if canRollback}} + {{#if model.canRollback}} {{d-button action="rollback" class="btn-rollback" label="admin.backups.operations.rollback.label" @@ -14,7 +14,7 @@ icon="ambulance" disabled=rollbackDisabled}} {{/if}} - {{#if isOperationRunning}} + {{#if model.isOperationRunning}} {{d-button action="cancelOperation" class="btn-danger" title="admin.backups.operations.cancel.title" diff --git a/app/assets/javascripts/admin/templates/backups_index.hbs b/app/assets/javascripts/admin/templates/backups_index.hbs index c6a6598be6..0b66fcaf73 100644 --- a/app/assets/javascripts/admin/templates/backups_index.hbs +++ b/app/assets/javascripts/admin/templates/backups_index.hbs @@ -6,9 +6,9 @@
{{resumable-upload target="/admin/backups/upload" success="uploadSuccess" error="uploadError" uploadText=uploadLabel title="admin.backups.upload.title"}} {{#if site.isReadOnly}} - {{d-button icon="eye" action="toggleReadOnlyMode" disabled=isOperationRunning title="admin.backups.read_only.disable.title" label="admin.backups.read_only.disable.label"}} + {{d-button icon="eye" action="toggleReadOnlyMode" disabled=model.isOperationRunning title="admin.backups.read_only.disable.title" label="admin.backups.read_only.disable.label"}} {{else}} - {{d-button icon="eye" action="toggleReadOnlyMode" disabled=isOperationRunning title="admin.backups.read_only.enable.title" label="admin.backups.read_only.enable.label"}} + {{d-button icon="eye" action="toggleReadOnlyMode" disabled=model.isOperationRunning title="admin.backups.read_only.enable.title" label="admin.backups.read_only.enable.label"}} {{/if}}
@@ -20,12 +20,12 @@
{{fa-icon "download"}}{{i18n 'admin.backups.operations.download.label'}} - {{#if isOperationRunning}} + {{#if model.isOperationRunning}} {{d-button icon="trash-o" action="destroyBackup" actionParam=backup class="btn-danger" disabled="true" title="admin.backups.operations.is_running"}} - {{d-button icon="play" action="startRestore" actionParam=backup disabled=restoreDisabled title=restoreTitle label="admin.backups.operations.restore.label"}} + {{d-button icon="play" action="startRestore" actionParam=backup disabled=model.restoreDisabled title=restoreTitle label="admin.backups.operations.restore.label"}} {{else}} {{d-button icon="trash-o" action="destroyBackup" actionParam=backup class="btn-danger" title="admin.backups.operations.destroy.title"}} - {{d-button icon="play" action="startRestore" actionParam=backup disabled=restoreDisabled title=restoreTitle label="admin.backups.operations.restore.label"}} + {{d-button icon="play" action="startRestore" actionParam=backup disabled=model.restoreDisabled title=restoreTitle label="admin.backups.operations.restore.label"}} {{/if}}
diff --git a/app/assets/javascripts/admin/templates/components/customize-link.hbs b/app/assets/javascripts/admin/templates/components/customize-link.hbs new file mode 100644 index 0000000000..dd3c4104c7 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/customize-link.hbs @@ -0,0 +1,5 @@ +
  • + + {{customization.description}} + +
  • diff --git a/app/assets/javascripts/admin/templates/components/group-member.hbs b/app/assets/javascripts/admin/templates/components/group-member.hbs new file mode 100644 index 0000000000..d476fdf033 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/group-member.hbs @@ -0,0 +1 @@ +{{avatar member imageSize="small"}} {{member.username}} {{#unless automatic}}{{fa-icon "times"}}{{/unless}} diff --git a/app/assets/javascripts/admin/templates/customize-css-html-index.hbs b/app/assets/javascripts/admin/templates/customize-css-html-index.hbs new file mode 100644 index 0000000000..781d063c7f --- /dev/null +++ b/app/assets/javascripts/admin/templates/customize-css-html-index.hbs @@ -0,0 +1 @@ +

    {{i18n 'admin.customize.about'}}

    diff --git a/app/assets/javascripts/admin/templates/customize-css-html-show.hbs b/app/assets/javascripts/admin/templates/customize-css-html-show.hbs new file mode 100644 index 0000000000..95b82ba97b --- /dev/null +++ b/app/assets/javascripts/admin/templates/customize-css-html-show.hbs @@ -0,0 +1,75 @@ +
    +
    + {{text-field class="style-name" value=model.name}} + {{fa-icon "download"}} {{i18n 'admin.export_json.button_text'}} + +
    + +
    + +
    + {{#if cssActive}}{{ace-editor content=model.stylesheet mode="scss"}}{{/if}} + {{#if headerActive}}{{ace-editor content=model.header mode="html"}}{{/if}} + {{#if topActive}}{{ace-editor content=model.top mode="html"}}{{/if}} + {{#if footerActive}}{{ace-editor content=model.footer mode="html"}}{{/if}} + {{#if headTagActive}}{{ace-editor content=model.head_tag mode="html"}}{{/if}} + {{#if bodyTagActive}}{{ace-editor content=model.body_tag mode="html"}}{{/if}} + {{#if embeddedCssActive}}{{ace-editor content=model.embedded_css mode="css"}}{{/if}} + {{#if mobileCssActive}}{{ace-editor content=model.mobile_stylesheet mode="scss"}}{{/if}} + {{#if mobileHeaderActive}}{{ace-editor content=model.mobile_header mode="html"}}{{/if}} + {{#if mobileTopActive}}{{ace-editor content=model.mobile_top mode="html"}}{{/if}} + {{#if mobileFooterActive}}{{ace-editor content=model.mobile_footer mode="html"}}{{/if}} +
    + + +
    +
    diff --git a/app/assets/javascripts/admin/templates/customize-css-html.hbs b/app/assets/javascripts/admin/templates/customize-css-html.hbs new file mode 100644 index 0000000000..73b8e22c9f --- /dev/null +++ b/app/assets/javascripts/admin/templates/customize-css-html.hbs @@ -0,0 +1,13 @@ +
    +

    {{i18n 'admin.customize.css_html.long_title'}}

    + + + {{d-button label="admin.customize.new" icon="plus" action="newCustomization" class="btn-primary"}} + {{d-button action="importModal" icon="upload" label="admin.customize.import"}} +
    + +{{outlet}} diff --git a/app/assets/javascripts/admin/templates/customize.hbs b/app/assets/javascripts/admin/templates/customize.hbs index c009909e1a..130b75e0ac 100644 --- a/app/assets/javascripts/admin/templates/customize.hbs +++ b/app/assets/javascripts/admin/templates/customize.hbs @@ -1,6 +1,6 @@ {{#admin-nav}} {{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}} - {{nav-item route='adminCustomize.css_html' label='admin.customize.css_html.title'}} + {{nav-item route='adminCustomizeCssHtml.index' label='admin.customize.css_html.title'}} {{nav-item route='adminSiteText' label='admin.site_text.title'}} {{nav-item route='adminUserFields' label='admin.user_fields.title'}} {{nav-item route='adminEmojis' label='admin.emoji.title'}} diff --git a/app/assets/javascripts/admin/templates/customize_css_html.hbs b/app/assets/javascripts/admin/templates/customize_css_html.hbs deleted file mode 100644 index f4dce62b09..0000000000 --- a/app/assets/javascripts/admin/templates/customize_css_html.hbs +++ /dev/null @@ -1,84 +0,0 @@ -
    -

    {{i18n 'admin.customize.css_html.long_title'}}

    - - - {{d-button action="importModal" icon="upload" label="admin.customize.import"}} -
    - -{{#if selectedItem}} -
    -
    - {{text-field class="style-name" value=selectedItem.name}} - {{fa-icon "download"}} {{i18n 'admin.export_json.button_text'}} - - - -
    - {{#if view.stylesheetActive}}{{ace-editor content=selectedItem.stylesheet mode="scss"}}{{/if}} - {{#if view.headerActive}}{{ace-editor content=selectedItem.header mode="html"}}{{/if}} - {{#if view.topActive}}{{ace-editor content=selectedItem.top mode="html"}}{{/if}} - {{#if view.footerActive}}{{ace-editor content=selectedItem.footer mode="html"}}{{/if}} - {{#if view.headTagActive}}{{ace-editor content=selectedItem.head_tag mode="html"}}{{/if}} - {{#if view.bodyTagActive}}{{ace-editor content=selectedItem.body_tag mode="html"}}{{/if}} - {{#if view.mobileStylesheetActive}}{{ace-editor content=selectedItem.mobile_stylesheet mode="scss"}}{{/if}} - {{#if view.mobileHeaderActive}}{{ace-editor content=selectedItem.mobile_header mode="html"}}{{/if}} - {{#if view.mobileTopActive}}{{ace-editor content=selectedItem.mobile_top mode="html"}}{{/if}} - {{#if view.mobileFooterActive}}{{ace-editor content=selectedItem.mobile_footer mode="html"}}{{/if}} -
    - -
    -
    -{{else}} -

    {{i18n 'admin.customize.about'}}

    -{{/if}} diff --git a/app/assets/javascripts/admin/templates/group.hbs b/app/assets/javascripts/admin/templates/group.hbs index d17167a66f..6260d12aab 100644 --- a/app/assets/javascripts/admin/templates/group.hbs +++ b/app/assets/javascripts/admin/templates/group.hbs @@ -1,31 +1,33 @@
    - {{#if automatic}} -

    {{name}}

    + {{#if model.automatic}} +

    {{model.name}}

    {{else}} - {{text-field name="name" value=name placeholderKey="admin.groups.name_placeholder"}} + {{text-field name="name" value=model.name placeholderKey="admin.groups.name_placeholder"}} {{/if}}
    - {{#if id}} + {{#if model.id}}
    - +
    {{fa-icon "fast-backward"}} {{currentPage}}/{{totalPages}} {{fa-icon "fast-forward"}}
    - {{each member in members itemView="group-member"}} + {{#each model.members as |member|}} + {{group-member member=member automatic=model.automatic removeAction="removeMember"}} + {{/each}}
    - {{#unless automatic}} + {{#unless model.automatic}}
    - {{user-selector usernames=usernames placeholderKey="admin.groups.selector_placeholder" id="user-selector"}} + {{user-selector usernames=model.usernames placeholderKey="admin.groups.selector_placeholder" id="user-selector"}}
    {{/unless}} @@ -33,15 +35,15 @@
    - {{#unless automatic}} + {{#unless model.automatic}}
    @@ -49,15 +51,15 @@
    - {{combo-box name="alias" valueAttribute="value" value=alias_level content=aliasLevelOptions}} + {{combo-box name="alias" valueAttribute="value" value=model.alias_level content=aliasLevelOptions}}
    - {{#unless automatic}} + {{#unless model.automatic}}
    - {{list-setting name="automatic_membership" settingValue=emailDomains}} + {{list-setting name="automatic_membership" settingValue=model.emailDomains}}
    @@ -66,13 +68,13 @@ - {{input value=title}} + {{input value=model.title}}
    {{/unless}}
    - {{#unless automatic}} + {{#unless model.automatic}} {{/unless}}
    diff --git a/app/assets/javascripts/admin/templates/group_member.hbs b/app/assets/javascripts/admin/templates/group_member.hbs deleted file mode 100644 index 1c344ba739..0000000000 --- a/app/assets/javascripts/admin/templates/group_member.hbs +++ /dev/null @@ -1 +0,0 @@ -{{avatar member imageSize="small"}} {{member.username}} {{#unless automatic}}{{fa-icon "times"}}{{/unless}} diff --git a/app/assets/javascripts/admin/templates/site-text-edit.hbs b/app/assets/javascripts/admin/templates/site-text-edit.hbs new file mode 100644 index 0000000000..438887f85f --- /dev/null +++ b/app/assets/javascripts/admin/templates/site-text-edit.hbs @@ -0,0 +1,26 @@ +

    {{model.title}}

    +

    {{model.description}}

    + +{{#if model.markdown}} + {{pagedown-editor value=model.value}} +{{/if}} +{{#if model.plainText}} + {{textarea value=model.value class="plain"}} +{{/if}} +{{#if model.html}} + {{ace-editor content=model.value mode="html"}} +{{/if}} +{{#if model.css}} + {{ace-editor content=model.value mode="css"}} +{{/if}} + +
    + + {{#if saved}}{{i18n 'saved'}}{{/if}} +
    diff --git a/app/assets/javascripts/admin/templates/site_text_index.hbs b/app/assets/javascripts/admin/templates/site-text-index.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/site_text_index.hbs rename to app/assets/javascripts/admin/templates/site-text-index.hbs diff --git a/app/assets/javascripts/admin/templates/site_text.hbs b/app/assets/javascripts/admin/templates/site-text.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/site_text.hbs rename to app/assets/javascripts/admin/templates/site-text.hbs diff --git a/app/assets/javascripts/admin/templates/site_text_edit.hbs b/app/assets/javascripts/admin/templates/site_text_edit.hbs deleted file mode 100644 index ac3d0e540f..0000000000 --- a/app/assets/javascripts/admin/templates/site_text_edit.hbs +++ /dev/null @@ -1,26 +0,0 @@ -

    {{title}}

    -

    {{description}}

    - -{{#if markdown}} - {{pagedown-editor value=value}} -{{/if}} -{{#if plainText}} - {{textarea value=value class="plain"}} -{{/if}} -{{#if html}} - {{ace-editor content=value mode="html"}} -{{/if}} -{{#if css}} - {{ace-editor content=value mode="css"}} -{{/if}} - -
    - - {{#if saved}}{{i18n 'saved'}}{{/if}} -
    diff --git a/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 b/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 index a7c2af453b..d4f03cd138 100644 --- a/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 +++ b/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 @@ -5,20 +5,20 @@ export default Discourse.View.extend({ _initialize: function() { this._reset(); }.on("init"), - _reset: function() { + _reset() { this.setProperties({ formattedLogs: "", index: 0 }); }, _updateFormattedLogs: Discourse.debounce(function() { - var logs = this.get("controller.model"); + const logs = this.get("controller.model"); if (logs.length === 0) { this._reset(); // reset the cached logs whenever the model is reset } else { // do the log formatting only once for HELLish performance - var formattedLogs = this.get("formattedLogs"); - for (var i = this.get("index"), length = logs.length; i < length; i++) { - var date = logs[i].get("timestamp"), - message = Handlebars.Utils.escapeExpression(logs[i].get("message")); + let formattedLogs = this.get("formattedLogs"); + for (let i = this.get("index"), length = logs.length; i < length; i++) { + const date = logs[i].get("timestamp"), + message = Handlebars.Utils.escapeExpression(logs[i].get("message")); formattedLogs += "[" + date + "] " + message + "\n"; } // update the formatted logs & cache index @@ -28,8 +28,8 @@ export default Discourse.View.extend({ } }, 150).observes("controller.model.@each"), - render: function(buffer) { - var formattedLogs = this.get("formattedLogs"); + render(buffer) { + const formattedLogs = this.get("formattedLogs"); if (formattedLogs && formattedLogs.length > 0) { buffer.push("
    ");
           buffer.push(formattedLogs);
    @@ -38,13 +38,13 @@ export default Discourse.View.extend({
           buffer.push("

    " + I18n.t("admin.backups.logs.none") + "

    "); } // add a loading indicator - if (this.get("controller.status.isOperationRunning")) { + if (this.get("controller.status.model.isOperationRunning")) { buffer.push(renderSpinner('small')); } }, _forceScrollToBottom: function() { - var $div = this.$()[0]; + const $div = this.$()[0]; $div.scrollTop = $div.scrollHeight; }.on("didInsertElement") diff --git a/app/assets/javascripts/admin/views/admin-customize.js.es6 b/app/assets/javascripts/admin/views/admin-customize.js.es6 new file mode 100644 index 0000000000..faca47026f --- /dev/null +++ b/app/assets/javascripts/admin/views/admin-customize.js.es6 @@ -0,0 +1,18 @@ +/*global Mousetrap:true */ + +export default Ember.View.extend({ + classNames: ['customize'], + + _init: function() { + var controller = this.get('controller'); + Mousetrap.bindGlobal('mod+s', function() { + controller.send("save"); + return false; + }); + }.on("didInsertElement"), + + _cleanUp: function() { + Mousetrap.unbindGlobal('mod+s'); + }.on("willDestroyElement") + +}); diff --git a/app/assets/javascripts/admin/views/admin_customize_view.js b/app/assets/javascripts/admin/views/admin_customize_view.js deleted file mode 100644 index 37ecfaceb1..0000000000 --- a/app/assets/javascripts/admin/views/admin_customize_view.js +++ /dev/null @@ -1,68 +0,0 @@ -/*global Mousetrap:true */ - -/** - A view to handle site customizations - - @class AdminCustomizeView - @extends Discourse.View - @namespace Discourse - @module Discourse -**/ -Discourse.AdminCustomizeView = Discourse.View.extend({ - templateName: 'admin/templates/customize', - classNames: ['customize'], - selected: 'stylesheet', - mobile: false, - - stylesheetActive: Em.computed.equal('selected', 'stylesheet'), - headerActive: Em.computed.equal('selected', 'header'), - topActive: Em.computed.equal('selected', 'top'), - footerActive: Em.computed.equal('selected', 'footer'), - headTagActive: Em.computed.equal('selected', 'head_tag'), - bodyTagActive: Em.computed.equal('selected', 'body_tag'), - - mobileStylesheetActive: Em.computed.equal('selected', 'mobile_stylesheet'), - mobileHeaderActive: Em.computed.equal('selected', 'mobile_header'), - mobileTopActive: Em.computed.equal('selected', 'mobile_top'), - mobileFooterActive: Em.computed.equal('selected', 'mobile_footer'), - - actions: { - toggleMobile: function() { - // auto-select best tab - var tab = this.get("selected"); - if (/_tag$/.test(tab)) { tab = "stylesheet"; } - if (this.get("mobile")) { tab = tab.replace("mobile_", ""); } - else { tab = "mobile_" + tab; } - this.set("selected", tab); - // toggle mobile - this.toggleProperty("mobile"); - }, - - select: function(tab) { - this.set('selected', tab); - }, - - toggleMaximize: function() { - this.set("maximized", !this.get("maximized")); - - Em.run.scheduleOnce('afterRender', this, function(){ - $('.ace-wrapper').each(function(){ - $(this).data("editor").resize(); - }); - }); - }, - }, - - _init: function() { - var controller = this.get('controller'); - Mousetrap.bindGlobal('mod+s', function() { - controller.send("save"); - return false; - }); - }.on("didInsertElement"), - - _cleanUp: function() { - Mousetrap.unbindGlobal('mod+s'); - }.on("willDestroyElement") - -}); diff --git a/app/assets/javascripts/admin/views/group-member.js.es6 b/app/assets/javascripts/admin/views/group-member.js.es6 deleted file mode 100644 index 7889fd59cf..0000000000 --- a/app/assets/javascripts/admin/views/group-member.js.es6 +++ /dev/null @@ -1,4 +0,0 @@ -export default Discourse.View.extend({ - classNames: ["item"], - templateName: "admin/templates/group_member" -}); diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index 2bddc91c2f..70a5af9e4a 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -140,3 +140,18 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { // TODO: Remove this, it is in for backwards compatibiltiy with plugins Discourse.HasCurrentUser = {}; + +function proxyDep(propName, moduleFunc, msg) { + if (Discourse.hasOwnProperty(propName)) { return; } + Object.defineProperty(Discourse, propName, { + get: function() { + msg = msg || "import the module"; + Ember.warn("DEPRECATION: `Discourse." + propName + "` is deprecated, " + msg + "."); + return moduleFunc(); + } + }); +} + +proxyDep('computed', function() { return require('discourse/lib/computed') }); +proxyDep('Formatter', function() { return require('discourse/lib/formatter') }); +proxyDep('PageTracker', function() { return require('discourse/lib/page-tracker').default }); diff --git a/app/assets/javascripts/discourse/adapters/rest.js.es6 b/app/assets/javascripts/discourse/adapters/rest.js.es6 index 5a64730aba..5e427a8658 100644 --- a/app/assets/javascripts/discourse/adapters/rest.js.es6 +++ b/app/assets/javascripts/discourse/adapters/rest.js.es6 @@ -1,4 +1,4 @@ -const ADMIN_MODELS = ['plugin']; +const ADMIN_MODELS = ['plugin', 'site-customization']; export function Result(payload, responseJson) { this.payload = payload; diff --git a/app/assets/javascripts/discourse/components/actions-summary.js.es6 b/app/assets/javascripts/discourse/components/actions-summary.js.es6 index 31c1459527..21c2076aa9 100644 --- a/app/assets/javascripts/discourse/components/actions-summary.js.es6 +++ b/app/assets/javascripts/discourse/components/actions-summary.js.es6 @@ -1,5 +1,6 @@ import StringBuffer from 'discourse/mixins/string-buffer'; import { iconHTML } from 'discourse/helpers/fa-icon'; +import { autoUpdatingRelativeAge } from 'discourse/lib/formatter'; export default Ember.Component.extend(StringBuffer, { tagName: 'section', @@ -57,7 +58,7 @@ export default Ember.Component.extend(StringBuffer, { buffer.push("
    " + iconHTML('fa-trash-o') + ' ' + Discourse.Utilities.tinyAvatar(post.get('postDeletedBy.avatar_template'), {title: post.get('postDeletedBy.username')}) + - Discourse.Formatter.autoUpdatingRelativeAge(new Date(post.get('postDeletedAt'))) + + autoUpdatingRelativeAge(new Date(post.get('postDeletedAt'))) + "
    "); } }, diff --git a/app/assets/javascripts/discourse/components/category-drop.js.es6 b/app/assets/javascripts/discourse/components/category-drop.js.es6 index bb63d0bbf9..696991853c 100644 --- a/app/assets/javascripts/discourse/components/category-drop.js.es6 +++ b/app/assets/javascripts/discourse/components/category-drop.js.es6 @@ -1,8 +1,9 @@ +import { setting } from 'discourse/lib/computed'; var get = Ember.get; export default Ember.Component.extend({ classNameBindings: ['category::no-category', 'categories:has-drop','categoryStyle'], - categoryStyle: Discourse.computed.setting('category_style'), + categoryStyle: setting('category_style'), tagName: 'li', diff --git a/app/assets/javascripts/discourse/components/date-picker.js.es6 b/app/assets/javascripts/discourse/components/date-picker.js.es6 new file mode 100644 index 0000000000..c3e92ad474 --- /dev/null +++ b/app/assets/javascripts/discourse/components/date-picker.js.es6 @@ -0,0 +1,30 @@ +/* global Pikaday:true */ +import loadScript from "discourse/lib/load-script"; + +export default Em.Component.extend({ + tagName: "input", + classNames: ["date-picker"], + _picker: null, + + _loadDatePicker: function() { + const self = this, + input = this.$()[0]; + + loadScript("/javascripts/pikaday.js").then(function() { + self._picker = new Pikaday({ + field: input, + format: "YYYY-MM-DD", + defaultDate: moment().add(1, "day").toDate(), + minDate: new Date(), + onSelect: function(date) { + self.set("value", moment(date).format("YYYY-MM-DD")); + }, + }); + }); + }.on("didInsertElement"), + + _destroy: function() { + this._picker = null; + }.on("willDestroyElement"), + +}); diff --git a/app/assets/javascripts/discourse/components/dropdown-button.js.es6 b/app/assets/javascripts/discourse/components/dropdown-button.js.es6 index 3747452e35..99d211bbe3 100644 --- a/app/assets/javascripts/discourse/components/dropdown-button.js.es6 +++ b/app/assets/javascripts/discourse/components/dropdown-button.js.es6 @@ -24,8 +24,11 @@ export default Ember.Component.extend(StringBuffer, { }.on('willDestroyElement'), renderString(buffer) { + const title = this.get('title'); + if (title) { + buffer.push("

    " + title + "

    "); + } - buffer.push("

    " + this.get('title') + "

    "); buffer.push(""); diff --git a/app/assets/javascripts/discourse/components/edit-category-settings.js.es6 b/app/assets/javascripts/discourse/components/edit-category-settings.js.es6 index 3bba6b1b24..049157609e 100644 --- a/app/assets/javascripts/discourse/components/edit-category-settings.js.es6 +++ b/app/assets/javascripts/discourse/components/edit-category-settings.js.es6 @@ -1,6 +1,7 @@ +import { setting } from 'discourse/lib/computed'; import { buildCategoryPanel } from 'discourse/components/edit-category-panel'; export default buildCategoryPanel('settings', { - emailInEnabled: Discourse.computed.setting('email_in'), - showPositionInput: Discourse.computed.setting('fixed_category_positions'), + emailInEnabled: setting('email_in'), + showPositionInput: setting('fixed_category_positions'), }); diff --git a/app/assets/javascripts/discourse/components/edit-category-tab.js.es6 b/app/assets/javascripts/discourse/components/edit-category-tab.js.es6 index 72c95192ac..9e47066067 100644 --- a/app/assets/javascripts/discourse/components/edit-category-tab.js.es6 +++ b/app/assets/javascripts/discourse/components/edit-category-tab.js.es6 @@ -1,3 +1,5 @@ +import { propertyEqual } from 'discourse/lib/computed'; + export default Em.Component.extend({ tagName: 'li', classNameBindings: ['active', 'tabClassName'], @@ -6,7 +8,7 @@ export default Em.Component.extend({ return 'edit-category-' + this.get('tab'); }.property('tab'), - active: Discourse.computed.propertyEqual('selectedTab', 'tab'), + active: propertyEqual('selectedTab', 'tab'), title: function() { return I18n.t('category.' + this.get('tab').replace('-', '_')); diff --git a/app/assets/javascripts/discourse/components/home-logo.js.es6 b/app/assets/javascripts/discourse/components/home-logo.js.es6 index 83ca9ec8da..d4b429ef03 100644 --- a/app/assets/javascripts/discourse/components/home-logo.js.es6 +++ b/app/assets/javascripts/discourse/components/home-logo.js.es6 @@ -1,3 +1,5 @@ +import { setting } from 'discourse/lib/computed'; + export default Ember.Component.extend({ classNames: ["title"], @@ -13,10 +15,10 @@ export default Ember.Component.extend({ return Discourse.Mobile.mobileView && !Ember.isBlank(this.get('mobileBigLogoUrl')); }.property(), - smallLogoUrl: Discourse.computed.setting('logo_small_url'), - bigLogoUrl: Discourse.computed.setting('logo_url'), - mobileBigLogoUrl: Discourse.computed.setting('mobile_logo_url'), - title: Discourse.computed.setting('title'), + smallLogoUrl: setting('logo_small_url'), + bigLogoUrl: setting('logo_url'), + mobileBigLogoUrl: setting('mobile_logo_url'), + title: setting('title'), click: function(e) { // if they want to open in a new tab, let it so diff --git a/app/assets/javascripts/discourse/components/notification-item.js.es6 b/app/assets/javascripts/discourse/components/notification-item.js.es6 index d4692458cb..f0add4c2f3 100644 --- a/app/assets/javascripts/discourse/components/notification-item.js.es6 +++ b/app/assets/javascripts/discourse/components/notification-item.js.es6 @@ -30,9 +30,9 @@ export default Ember.Component.extend({ } if (it.get('notification_type') === INVITED_TYPE) { - return Discourse.getURL('/my/invited'); + return Discourse.getURL('/users/' + it.get('data.display_username')); } - }.property("notification.data.{badge_id,badge_name}", "model.slug", "model.topic_id", "model.post_number"), + }.property("notification.data.{badge_id,badge_name,display_username}", "model.slug", "model.topic_id", "model.post_number"), description: function() { const badgeName = this.get("notification.data.badge_name"); diff --git a/app/assets/javascripts/discourse/components/poster-name.js.es6 b/app/assets/javascripts/discourse/components/poster-name.js.es6 index 3c5738cac4..5a2a7bd8ea 100644 --- a/app/assets/javascripts/discourse/components/poster-name.js.es6 +++ b/app/assets/javascripts/discourse/components/poster-name.js.es6 @@ -1,6 +1,8 @@ +import { setting } from 'discourse/lib/computed'; + const PosterNameComponent = Em.Component.extend({ classNames: ['names', 'trigger-user-card'], - displayNameOnPosts: Discourse.computed.setting('display_name_on_posts'), + displayNameOnPosts: setting('display_name_on_posts'), // sanitize name for comparison sanitizeName(name){ diff --git a/app/assets/javascripts/discourse/components/small-action.js.es6 b/app/assets/javascripts/discourse/components/small-action.js.es6 index 4457c6fb49..54ef41b9fa 100644 --- a/app/assets/javascripts/discourse/components/small-action.js.es6 +++ b/app/assets/javascripts/discourse/components/small-action.js.es6 @@ -1,3 +1,5 @@ +import { relativeAge } from 'discourse/lib/formatter'; + const icons = { 'closed.enabled': 'lock', 'closed.disabled': 'unlock-alt', @@ -19,7 +21,7 @@ export function actionDescription(actionCode, createdAt) { const ac = this.get(actionCode); if (ac) { const dt = new Date(this.get(createdAt)); - const when = Discourse.Formatter.relativeAge(dt, {format: 'medium-with-ago'}); + const when = relativeAge(dt, {format: 'medium-with-ago'}); return I18n.t(`action_codes.${ac}`, {when}).htmlSafe(); } }.property(actionCode, createdAt); diff --git a/app/assets/javascripts/discourse/components/stream-item.js.es6 b/app/assets/javascripts/discourse/components/stream-item.js.es6 index 9a29d87438..562ac59f03 100644 --- a/app/assets/javascripts/discourse/components/stream-item.js.es6 +++ b/app/assets/javascripts/discourse/components/stream-item.js.es6 @@ -1,8 +1,9 @@ +import { propertyEqual } from 'discourse/lib/computed'; import { actionDescription } from "discourse/components/small-action"; export default Ember.Component.extend({ classNameBindings: [":item", "item.hidden", "item.deleted", "moderatorAction"], - moderatorAction: Discourse.computed.propertyEqual("item.post_type", "site.post_types.moderator_action"), + moderatorAction: propertyEqual("item.post_type", "site.post_types.moderator_action"), actionDescription: actionDescription("item.action_code", "item.created_at"), actions: { diff --git a/app/assets/javascripts/discourse/components/user-field.js.es6 b/app/assets/javascripts/discourse/components/user-field.js.es6 index 625067622e..86caf3b5d6 100644 --- a/app/assets/javascripts/discourse/components/user-field.js.es6 +++ b/app/assets/javascripts/discourse/components/user-field.js.es6 @@ -1,6 +1,8 @@ +import { fmt } from 'discourse/lib/computed'; + export default Ember.Component.extend({ classNameBindings: [':user-field', 'field.field_type'], - layoutName: Discourse.computed.fmt('field.field_type', 'components/user-fields/%@'), + layoutName: fmt('field.field_type', 'components/user-fields/%@'), noneLabel: function() { if (!this.get('field.required')) { diff --git a/app/assets/javascripts/discourse/components/user-small.js.es6 b/app/assets/javascripts/discourse/components/user-small.js.es6 index b89c5d01a4..9dc0699381 100644 --- a/app/assets/javascripts/discourse/components/user-small.js.es6 +++ b/app/assets/javascripts/discourse/components/user-small.js.es6 @@ -1,7 +1,9 @@ +import { url } from 'discourse/lib/computed'; + export default Ember.Component.extend({ classNames: ['user-small'], - userPath: Discourse.computed.url('user.username', '/users/%@'), + userPath: url('user.username', '/users/%@'), name: function() { const name = this.get('user.name'); diff --git a/app/assets/javascripts/discourse/controllers/application.js.es6 b/app/assets/javascripts/discourse/controllers/application.js.es6 index 6017af3214..2f0b4cec5f 100644 --- a/app/assets/javascripts/discourse/controllers/application.js.es6 +++ b/app/assets/javascripts/discourse/controllers/application.js.es6 @@ -1,4 +1,5 @@ export default Ember.Controller.extend({ + showTop: true, showFooter: false, styleCategory: null, diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index c4f1ac21b4..f724a6cbce 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -1,3 +1,4 @@ +import { setting } from 'discourse/lib/computed'; import Presence from 'discourse/mixins/presence'; export default Ember.ObjectController.extend(Presence, { @@ -8,7 +9,7 @@ export default Ember.ObjectController.extend(Presence, { showEditReason: false, editReason: null, - maxTitleLength: Discourse.computed.setting('max_topic_title_length'), + maxTitleLength: setting('max_topic_title_length'), scopedCategoryId: null, similarTopics: null, similarTopicsMessage: null, diff --git a/app/assets/javascripts/discourse/controllers/create-account.js.es6 b/app/assets/javascripts/discourse/controllers/create-account.js.es6 index a668d4cc88..d8995ad6fe 100644 --- a/app/assets/javascripts/discourse/controllers/create-account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/create-account.js.es6 @@ -1,5 +1,6 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; import DiscourseController from 'discourse/controllers/controller'; +import { setting } from 'discourse/lib/computed'; export default DiscourseController.extend(ModalFunctionality, { needs: ['login'], @@ -16,10 +17,10 @@ export default DiscourseController.extend(ModalFunctionality, { userFields: null, hasAuthOptions: Em.computed.notEmpty('authOptions'), - canCreateLocal: Discourse.computed.setting('enable_local_logins'), + canCreateLocal: setting('enable_local_logins'), showCreateForm: Em.computed.or('hasAuthOptions', 'canCreateLocal'), - maxUsernameLength: Discourse.computed.setting('max_username_length'), - minUsernameLength: Discourse.computed.setting('min_username_length'), + maxUsernameLength: setting('max_username_length'), + minUsernameLength: setting('min_username_length'), resetForm() { // We wrap the fields in a structure so we can assign a value diff --git a/app/assets/javascripts/discourse/controllers/directory-item.js.es6 b/app/assets/javascripts/discourse/controllers/directory-item.js.es6 index 8858982408..2d09b3824f 100644 --- a/app/assets/javascripts/discourse/controllers/directory-item.js.es6 +++ b/app/assets/javascripts/discourse/controllers/directory-item.js.es6 @@ -1,3 +1,5 @@ +import { propertyEqual } from 'discourse/lib/computed'; + export default Ember.Controller.extend({ - me: Discourse.computed.propertyEqual('model.user.id', 'currentUser.id') + me: propertyEqual('model.user.id', 'currentUser.id') }); diff --git a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 index 71a316ef02..ac534b9618 100644 --- a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 @@ -1,6 +1,7 @@ import DiscoveryController from 'discourse/controllers/discovery'; import { queryParams } from 'discourse/controllers/discovery-sortable'; import BulkTopicSelection from 'discourse/mixins/bulk-topic-selection'; +import { endWith } from 'discourse/lib/computed'; const controllerOpts = { needs: ['discovery'], @@ -102,8 +103,8 @@ const controllerOpts = { hasTopics: Em.computed.gt('model.topics.length', 0), allLoaded: Em.computed.empty('model.more_topics_url'), - latest: Discourse.computed.endWith('model.filter', 'latest'), - new: Discourse.computed.endWith('model.filter', 'new'), + latest: endWith('model.filter', 'latest'), + new: endWith('model.filter', 'new'), top: Em.computed.notEmpty('period'), yearly: Em.computed.equal('period', 'yearly'), quarterly: Em.computed.equal('period', 'quarterly'), diff --git a/app/assets/javascripts/discourse/controllers/group.js.es6 b/app/assets/javascripts/discourse/controllers/group.js.es6 index b7c7c18a63..dcd408673d 100644 --- a/app/assets/javascripts/discourse/controllers/group.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group.js.es6 @@ -1,7 +1,4 @@ -import ObjectController from 'discourse/controllers/object'; - -// The basic controller for a group -export default ObjectController.extend({ +export default Ember.Controller.extend({ counts: null, showing: null, @@ -10,4 +7,3 @@ export default ObjectController.extend({ showingIndex: Em.computed.equal('showing', 'index'), showingMembers: Em.computed.equal('showing', 'members') }); - diff --git a/app/assets/javascripts/discourse/controllers/group/members.js.es6 b/app/assets/javascripts/discourse/controllers/group/members.js.es6 index ca218875fb..794b5856a7 100644 --- a/app/assets/javascripts/discourse/controllers/group/members.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group/members.js.es6 @@ -1,11 +1,13 @@ -export default Ember.ObjectController.extend({ +export default Ember.Controller.extend({ loading: false, + limit: null, + offset: null, actions: { loadMore() { if (this.get("loading")) { return; } // we've reached the end - if (this.get("model.members.length") >= this.get("user_count")) { return; } + if (this.get("model.members.length") >= this.get("model.user_count")) { return; } this.set("loading", true); diff --git a/app/assets/javascripts/discourse/controllers/invite.js.es6 b/app/assets/javascripts/discourse/controllers/invite.js.es6 index 4529672277..048f984658 100644 --- a/app/assets/javascripts/discourse/controllers/invite.js.es6 +++ b/app/assets/javascripts/discourse/controllers/invite.js.es6 @@ -14,7 +14,7 @@ export default ObjectController.extend(Presence, ModalFunctionality, { }.property(), disabled: function() { - if (this.get('saving')) return true; + if (this.get('model.saving')) return true; if (this.blank('emailOrUsername')) return true; const emailOrUsername = this.get('emailOrUsername').trim(); // when inviting to forum, email must be valid @@ -22,14 +22,14 @@ export default ObjectController.extend(Presence, ModalFunctionality, { // normal users (not admin) can't invite users to private topic via email if (!this.get('isAdmin') && this.get('isPrivateTopic') && Discourse.Utilities.emailValid(emailOrUsername)) return true; // when invting to private topic via email, group name must be specified - if (this.get('isPrivateTopic') && this.blank('groupNames') && Discourse.Utilities.emailValid(emailOrUsername)) return true; + if (this.get('isPrivateTopic') && this.blank('model.groupNames') && Discourse.Utilities.emailValid(emailOrUsername)) return true; if (this.get('model.details.can_invite_to')) return false; return false; - }.property('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'groupNames', 'saving'), + }.property('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'model.groupNames', 'model.saving'), buttonTitle: function() { - return this.get('saving') ? I18n.t('topic.inviting') : I18n.t('topic.invite_reply.action'); - }.property('saving'), + return this.get('model.saving') ? I18n.t('topic.inviting') : I18n.t('topic.invite_reply.action'); + }.property('model.saving'), // We are inviting to a topic if the model isn't the current user. // The current user would mean we are inviting to the forum in general. @@ -117,8 +117,8 @@ export default ObjectController.extend(Presence, ModalFunctionality, { // Reset the modal to allow a new user to be invited. reset() { - this.setProperties({ - emailOrUsername: null, + this.set('emailOrUsername', null); + this.get('model').setProperties({ groupNames: null, error: false, saving: false, @@ -131,13 +131,14 @@ export default ObjectController.extend(Presence, ModalFunctionality, { createInvite() { if (this.get('disabled')) { return; } - const groupNames = this.get('groupNames'), - userInvitedController = this.get('controllers.user-invited-show'); + const groupNames = this.get('model.groupNames'), + userInvitedController = this.get('controllers.user-invited-show'), + model = this.get('model'); - this.setProperties({ saving: true, error: false }); + model.setProperties({ saving: true, error: false }); return this.get('model').createInvite(this.get('emailOrUsername').trim(), groupNames).then(result => { - this.setProperties({ saving: false, finished: true }); + model.setProperties({ saving: false, finished: true }); if (!this.get('invitingToTopic')) { Discourse.Invite.findInvitedBy(this.currentUser, userInvitedController.get('filter')).then(invite_model => { userInvitedController.set('model', invite_model); @@ -146,7 +147,7 @@ export default ObjectController.extend(Presence, ModalFunctionality, { } else if (this.get('isMessage') && result && result.user) { this.get('model.details.allowed_users').pushObject(result.user); } - }).catch(() => this.setProperties({ saving: false, error: true })); + }).catch(() => model.setProperties({ saving: false, error: true })); } } diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6 index 641b6bc838..6d39640e5b 100644 --- a/app/assets/javascripts/discourse/controllers/login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/login.js.es6 @@ -1,6 +1,7 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; import DiscourseController from 'discourse/controllers/controller'; import showModal from 'discourse/lib/show-modal'; +import { setting } from 'discourse/lib/computed'; // This is happening outside of the app via popup const AuthErrors = @@ -13,7 +14,7 @@ export default DiscourseController.extend(ModalFunctionality, { loggingIn: false, loggedIn: false, - canLoginLocal: Discourse.computed.setting('enable_local_logins'), + canLoginLocal: setting('enable_local_logins'), loginRequired: Em.computed.alias('controllers.application.loginRequired'), resetForm: function() { diff --git a/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 b/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 index 4abaf1da27..c21ed81341 100644 --- a/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 +++ b/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 @@ -1,7 +1,8 @@ import NavigationDefaultController from 'discourse/controllers/navigation/default'; +import { setting } from 'discourse/lib/computed'; export default NavigationDefaultController.extend({ - subcategoryListSetting: Discourse.computed.setting('show_subcategory_list'), + subcategoryListSetting: setting('show_subcategory_list'), showingParentCategory: Em.computed.none('category.parentCategory'), showingSubcategoryList: Em.computed.and('subcategoryListSetting', 'showingParentCategory'), diff --git a/app/assets/javascripts/discourse/controllers/notifications.js.es6 b/app/assets/javascripts/discourse/controllers/notifications.js.es6 index f0ae8ad42e..e76d84e968 100644 --- a/app/assets/javascripts/discourse/controllers/notifications.js.es6 +++ b/app/assets/javascripts/discourse/controllers/notifications.js.es6 @@ -1,5 +1,7 @@ +import { url } from 'discourse/lib/computed'; + export default Ember.ArrayController.extend({ needs: ['header'], loadingNotifications: Em.computed.alias('controllers.header.loadingNotifications'), - myNotificationsUrl: Discourse.computed.url('/my/notifications') + myNotificationsUrl: url('/my/notifications') }); diff --git a/app/assets/javascripts/discourse/controllers/preferences.js.es6 b/app/assets/javascripts/discourse/controllers/preferences.js.es6 index 0cc11e05e9..cacbd4b9e1 100644 --- a/app/assets/javascripts/discourse/controllers/preferences.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences.js.es6 @@ -1,14 +1,15 @@ +import { setting } from 'discourse/lib/computed'; import ObjectController from 'discourse/controllers/object'; import CanCheckEmails from 'discourse/mixins/can-check-emails'; import { popupAjaxError } from 'discourse/lib/ajax-error'; export default ObjectController.extend(CanCheckEmails, { - allowAvatarUpload: Discourse.computed.setting('allow_uploaded_avatars'), - allowUserLocale: Discourse.computed.setting('allow_user_locale'), - ssoOverridesAvatar: Discourse.computed.setting('sso_overrides_avatar'), - allowBackgrounds: Discourse.computed.setting('allow_profile_backgrounds'), - editHistoryVisible: Discourse.computed.setting('edit_history_visible_to_public'), + allowAvatarUpload: setting('allow_uploaded_avatars'), + allowUserLocale: setting('allow_user_locale'), + ssoOverridesAvatar: setting('sso_overrides_avatar'), + allowBackgrounds: setting('allow_profile_backgrounds'), + editHistoryVisible: setting('edit_history_visible_to_public'), selectedCategories: function(){ return [].concat(this.get("model.watchedCategories"), @@ -40,7 +41,7 @@ export default ObjectController.extend(CanCheckEmails, { cannotDeleteAccount: Em.computed.not('can_delete_account'), deleteDisabled: Em.computed.or('saving', 'deleting', 'cannotDeleteAccount'), - canEditName: Discourse.computed.setting('enable_names'), + canEditName: setting('enable_names'), nameInstructions: function() { return I18n.t(Discourse.SiteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions'); diff --git a/app/assets/javascripts/discourse/controllers/preferences/email.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/email.js.es6 index 50ee7857d2..547a9adbd7 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/email.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/email.js.es6 @@ -1,13 +1,6 @@ +import { propertyEqual } from 'discourse/lib/computed'; import ObjectController from 'discourse/controllers/object'; -/** - This controller supports actions related to updating one's email address - - @class PreferencesEmailController - @extends ObjectController - @namespace Discourse - @module Discourse -**/ export default ObjectController.extend({ taken: false, saving: false, @@ -17,7 +10,7 @@ export default ObjectController.extend({ newEmailEmpty: Em.computed.empty('newEmail'), saveDisabled: Em.computed.or('saving', 'newEmailEmpty', 'taken', 'unchanged'), - unchanged: Discourse.computed.propertyEqual('newEmailLower', 'email'), + unchanged: propertyEqual('newEmailLower', 'email'), newEmailLower: function() { return this.get('newEmail').toLowerCase(); diff --git a/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 index ed6699ad4a..f74250976b 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 @@ -1,3 +1,4 @@ +import { setting, propertyEqual } from 'discourse/lib/computed'; import Presence from 'discourse/mixins/presence'; import ObjectController from 'discourse/controllers/object'; @@ -8,11 +9,11 @@ export default ObjectController.extend(Presence, { errorMessage: null, newUsername: null, - maxLength: Discourse.computed.setting('max_username_length'), - minLength: Discourse.computed.setting('min_username_length'), + maxLength: setting('max_username_length'), + minLength: setting('min_username_length'), newUsernameEmpty: Em.computed.empty('newUsername'), saveDisabled: Em.computed.or('saving', 'newUsernameEmpty', 'taken', 'unchanged', 'errorMessage'), - unchanged: Discourse.computed.propertyEqual('newUsername', 'username'), + unchanged: propertyEqual('newUsername', 'username'), checkTaken: function() { if( this.get('newUsername') && this.get('newUsername').length < this.get('minLength') ) { diff --git a/app/assets/javascripts/discourse/controllers/queued-post.js.es6 b/app/assets/javascripts/discourse/controllers/queued-post.js.es6 index 100b0b1586..a285537491 100644 --- a/app/assets/javascripts/discourse/controllers/queued-post.js.es6 +++ b/app/assets/javascripts/discourse/controllers/queued-post.js.es6 @@ -1,3 +1,4 @@ +import { propertyEqual } from 'discourse/lib/computed'; import BufferedContent from 'discourse/mixins/buffered-content'; import { popupAjaxError } from 'discourse/lib/ajax-error'; @@ -21,7 +22,7 @@ export default Ember.Controller.extend(BufferedContent, { post: Ember.computed.alias('model'), currentlyEditing: Ember.computed.alias('controllers.queued-posts.editing'), - editing: Discourse.computed.propertyEqual('model', 'currentlyEditing'), + editing: propertyEqual('model', 'currentlyEditing'), _confirmDelete: updateState('rejected', {deleteUser: true}), diff --git a/app/assets/javascripts/discourse/controllers/search.js.es6 b/app/assets/javascripts/discourse/controllers/search.js.es6 index 9c44f0b2a9..82f893f050 100644 --- a/app/assets/javascripts/discourse/controllers/search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/search.js.es6 @@ -1,14 +1,15 @@ import Presence from 'discourse/mixins/presence'; import searchForTerm from 'discourse/lib/search-for-term'; -var _dontSearch = false; +let _dontSearch = false; export default Em.Controller.extend(Presence, { + typeFilter: null, contextType: function(key, value){ if(arguments.length > 1) { // a bit hacky, consider cleaning this up, need to work through all observers though - var context = $.extend({}, this.get('searchContext')); + const context = $.extend({}, this.get('searchContext')); context.type = value; this.set('searchContext', context); } @@ -29,8 +30,8 @@ export default Em.Controller.extend(Presence, { return null; } - var url = '/search?q=' + encodeURIComponent(this.get('term')); - var searchContext = this.get('searchContext'); + let url = '/search?q=' + encodeURIComponent(this.get('term')); + const searchContext = this.get('searchContext'); if (this.get('searchContextEnabled') && searchContext) { url += encodeURIComponent(" " + searchContext.type + ":" + searchContext.id); @@ -41,14 +42,14 @@ export default Em.Controller.extend(Presence, { }.property('searchContext','term','searchContextEnabled'), fullSearchUrl: function(){ - var url = this.get('fullSearchUrlRelative'); + const url = this.get('fullSearchUrlRelative'); if (url) { return Discourse.getURL(url); } }.property('fullSearchUrlRelative'), searchContextDescription: function(){ - var ctx = this.get('searchContext'); + const ctx = this.get('searchContext'); if (ctx) { switch(Em.get(ctx, 'type')) { case 'topic': @@ -71,7 +72,7 @@ export default Em.Controller.extend(Presence, { // If we need to perform another search newSearchNeeded: function() { this.set('noResults', false); - var term = (this.get('term') || '').trim(); + const term = (this.get('term') || '').trim(); if (term.length >= Discourse.SiteSettings.min_search_term_length) { this.set('loading', true); @@ -82,23 +83,32 @@ export default Em.Controller.extend(Presence, { this.set('selectedIndex', 0); }.observes('term', 'typeFilter'), - searchTerm: function(term, typeFilter) { - var self = this; + searchTerm(term, typeFilter) { + const self = this; - var context; - if(this.get('searchContextEnabled')){ - context = this.get('searchContext'); + // for cancelling debounced search + if (this._cancelSearch){ + this._cancelSearch = null; + return; } - searchForTerm(term, { - typeFilter: typeFilter, - searchContext: context, + if (this._search) { + this._search.abort(); + } + + const searchContext = this.get('searchContextEnabled') ? this.get('searchContext') : null; + + this._search = searchForTerm(term, { + typeFilter, + searchContext, fullSearchUrl: this.get('fullSearchUrl') - }).then(function(results) { + }); + + this._search.then(function(results) { self.setProperties({ noResults: !results, content: results }); + }).finally(function() { self.set('loading', false); - }).catch(function() { - self.set('loading', false); + self._search = null; }); }, @@ -112,22 +122,36 @@ export default Em.Controller.extend(Presence, { }.observes('term'), actions: { - fullSearch: function() { - var url = this.get('fullSearchUrlRelative'); + fullSearch() { + const self = this; + + if (this._search) { + this._search.abort(); + } + + // maybe we are debounced and delayed + // stop that as well + this._cancelSearch = true; + Em.run.later(function(){ + self._cancelSearch = false; + }, 400); + + const url = this.get('fullSearchUrlRelative'); if (url) { Discourse.URL.routeTo(url); } }, - moreOfType: function(type) { + + moreOfType(type) { this.set('typeFilter', type); }, - cancelType: function() { + cancelType() { this.cancelTypeFilter(); } }, - cancelTypeFilter: function() { + cancelTypeFilter() { this.set('typeFilter', null); } }); diff --git a/app/assets/javascripts/discourse/controllers/share.js.es6 b/app/assets/javascripts/discourse/controllers/share.js.es6 index 2e662dc217..fa1c3ff390 100644 --- a/app/assets/javascripts/discourse/controllers/share.js.es6 +++ b/app/assets/javascripts/discourse/controllers/share.js.es6 @@ -1,11 +1,12 @@ import Sharing from 'discourse/lib/sharing'; +import { longDateNoYear } from 'discourse/lib/formatter'; export default Ember.Controller.extend({ needs: ['topic'], title: Ember.computed.alias('controllers.topic.model.title'), displayDate: function() { - return Discourse.Formatter.longDateNoYear(new Date(this.get('date'))); + return longDateNoYear(new Date(this.get('date'))); }.property('date'), // Close the share controller diff --git a/app/assets/javascripts/discourse/controllers/site-map.js.es6 b/app/assets/javascripts/discourse/controllers/site-map.js.es6 index af9ccd08c4..61192eb575 100644 --- a/app/assets/javascripts/discourse/controllers/site-map.js.es6 +++ b/app/assets/javascripts/discourse/controllers/site-map.js.es6 @@ -1,3 +1,5 @@ +import { url } from 'discourse/lib/computed'; + export default Ember.ArrayController.extend({ needs: ['application', 'header'], @@ -8,7 +10,7 @@ export default Ember.ArrayController.extend({ return Discourse.SiteSettings.faq_url ? Discourse.SiteSettings.faq_url : Discourse.getURL('/faq'); }.property(), - badgesUrl: Discourse.computed.url('/badges'), + badgesUrl: url('/badges'), showKeyboardShortcuts: function(){ return !Discourse.Mobile.mobileView && !this.capabilities.touch; diff --git a/app/assets/javascripts/discourse/controllers/topic-unsubscribe.js.es6 b/app/assets/javascripts/discourse/controllers/topic-unsubscribe.js.es6 new file mode 100644 index 0000000000..a2611ebdcb --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/topic-unsubscribe.js.es6 @@ -0,0 +1,7 @@ +export default Ember.Controller.extend({ + + stopNotificiationsText: function() { + return I18n.t("topic.unsubscribe.stop_notifications", { title: this.get("model.fancyTitle") }); + }.property("model.fancyTitle"), + +}); diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 7dbce6dbb4..5b9e0ae778 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -3,6 +3,7 @@ import BufferedContent from 'discourse/mixins/buffered-content'; import SelectedPostsCount from 'discourse/mixins/selected-posts-count'; import { spinnerHTML } from 'discourse/helpers/loading-spinner'; import Topic from 'discourse/models/topic'; +import { setting } from 'discourse/lib/computed'; export default ObjectController.extend(SelectedPostsCount, BufferedContent, { multiSelect: false, @@ -18,7 +19,7 @@ export default ObjectController.extend(SelectedPostsCount, BufferedContent, { firstPostExpanded: false, retrying: false, - maxTitleLength: Discourse.computed.setting('max_topic_title_length'), + maxTitleLength: setting('max_topic_title_length'), contextChanged: function() { this.set('controllers.search.searchContext', this.get('model.searchContext')); @@ -285,8 +286,8 @@ export default ObjectController.extend(SelectedPostsCount, BufferedContent, { self.rollbackBuffer(); self.set('editingTopic', false); }).catch(function(error) { - if (error && error.responseText) { - bootbox.alert($.parseJSON(error.responseText).errors[0]); + if (error && error.jqXHR && error.jqXHR.responseText) { + bootbox.alert($.parseJSON(error.jqXHR.responseText).errors[0]); } else { bootbox.alert(I18n.t('generic_error')); } diff --git a/app/assets/javascripts/discourse/controllers/upload-customization.js.es6 b/app/assets/javascripts/discourse/controllers/upload-customization.js.es6 index 09158f5375..d7a980a0b4 100644 --- a/app/assets/javascripts/discourse/controllers/upload-customization.js.es6 +++ b/app/assets/javascripts/discourse/controllers/upload-customization.js.es6 @@ -2,25 +2,19 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; export default Ember.Controller.extend(ModalFunctionality, { notReady: Em.computed.not('ready'), - - needs: ['admin-customize-css-html'], - - title: "hi", + needs: ['adminCustomizeCssHtml'], ready: function() { - let parsed; try { - parsed = JSON.parse(this.get('customizationFile')); + const parsed = JSON.parse(this.get('customizationFile')); + return !!parsed["site_customization"]; } catch (e) { return false; } - - return !!parsed["site_customization"]; }.property('customizationFile'), actions: { createCustomization: function() { - const self = this; const object = JSON.parse(this.get('customizationFile')).site_customization; // Slight fixup before creating object @@ -28,24 +22,8 @@ export default Ember.Controller.extend(ModalFunctionality, { delete object.id; delete object.key; - const customization = Discourse.SiteCustomization.create(object); - - this.set('loading', true); - customization.save().then(function(customization) { - self.send('closeModal'); - self.set('loading', false); - - const parentController = self.get('controllers.admin-customize-css-html'); - parentController.pushObject(customization); - parentController.set('selectedItem', customization); - }).catch(function(xhr) { - self.set('loading', false); - if (xhr.responseJSON) { - bootbox.alert(xhr.responseJSON.errors.join("
    ")); - } else { - bootbox.alert(I18n.t('generic_error')); - } - }); + const controller = this.get('controllers.adminCustomizeCssHtml'); + controller.send('newCustomization', object); } } diff --git a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 index edd2d56943..be9d21dafb 100644 --- a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 +++ b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 @@ -1,5 +1,6 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; import DiscourseController from 'discourse/controllers/controller'; +import { setting } from 'discourse/lib/computed'; export default DiscourseController.extend(ModalFunctionality, { remote: Em.computed.not("local"), @@ -13,7 +14,7 @@ export default DiscourseController.extend(ModalFunctionality, { }); }.on('init'), - maxSize: Discourse.computed.setting('max_attachment_size_kb'), + maxSize: setting('max_attachment_size_kb'), allowLocal: Em.computed.gt('maxSize', 0), actions: { diff --git a/app/assets/javascripts/discourse/controllers/user-card.js.es6 b/app/assets/javascripts/discourse/controllers/user-card.js.es6 index 2dc7dde29f..3279137dbf 100644 --- a/app/assets/javascripts/discourse/controllers/user-card.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-card.js.es6 @@ -1,3 +1,5 @@ +import { propertyNotEqual, setting } from 'discourse/lib/computed'; + export default Ember.Controller.extend({ needs: ['topic', 'application'], visible: false, @@ -16,10 +18,10 @@ export default Ember.Controller.extend({ viewingTopic: Em.computed.match('controllers.application.currentPath', /^topic\./), viewingAdmin: Em.computed.match('controllers.application.currentPath', /^admin\./), showFilter: Em.computed.and('viewingTopic', 'postStream.hasNoFilters', 'enoughPostsForFiltering'), - showName: Discourse.computed.propertyNotEqual('user.name', 'user.username'), + showName: propertyNotEqual('user.name', 'user.username'), hasUserFilters: Em.computed.gt('postStream.userFilters.length', 0), isSuspended: Em.computed.notEmpty('user.suspend_reason'), - showBadges: Discourse.computed.setting('enable_badges'), + showBadges: setting('enable_badges'), showMoreBadges: Em.computed.gt('moreBadgesCount', 0), showDelete: Em.computed.and("viewingAdmin", "showName", "user.canBeDeleted"), diff --git a/app/assets/javascripts/discourse/controllers/user-posts.js.es6 b/app/assets/javascripts/discourse/controllers/user-posts.js.es6 index dd2f58f695..e06143db48 100644 --- a/app/assets/javascripts/discourse/controllers/user-posts.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-posts.js.es6 @@ -1,7 +1,7 @@ -export default Ember.ObjectController.extend({ +export default Ember.Controller.extend({ needs: ["application"], _showFooter: function() { - this.set("controllers.application.showFooter", !this.get("canLoadMore")); - }.observes("canLoadMore") + this.set("controllers.application.showFooter", !this.get("model.canLoadMore")); + }.observes("model.canLoadMore") }); diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6 index 60fd3fc060..419e6f8a2f 100644 --- a/app/assets/javascripts/discourse/controllers/user.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user.js.es6 @@ -14,12 +14,6 @@ export default ObjectController.extend(CanCheckEmails, { collapsedInfo: Em.computed.not('indexStream'), - websiteName: function() { - var website = this.get('model.website'); - if (Em.isEmpty(website)) { return; } - return website.split("/")[2]; - }.property('model.website'), - linkWebsite: Em.computed.not('model.isBasic'), removeNoFollow: function() { diff --git a/app/assets/javascripts/discourse/helpers/application_helpers.js b/app/assets/javascripts/discourse/helpers/application.js.es6 similarity index 64% rename from app/assets/javascripts/discourse/helpers/application_helpers.js rename to app/assets/javascripts/discourse/helpers/application.js.es6 index 291e598b14..35c4f64936 100644 --- a/app/assets/javascripts/discourse/helpers/application_helpers.js +++ b/app/assets/javascripts/discourse/helpers/application.js.es6 @@ -1,17 +1,17 @@ -var safe = Handlebars.SafeString; +import registerUnbound from 'discourse/helpers/register-unbound'; +import avatarTemplate from 'discourse/lib/avatar-template'; +import { longDate, autoUpdatingRelativeAge, number } from 'discourse/lib/formatter'; -// TODO: Remove me when ES6ified -var registerUnbound = require('discourse/helpers/register-unbound', null, null, true).default; -var avatarTemplate = require('discourse/lib/avatar-template', null, null, true).default; +const safe = Handlebars.SafeString; Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) { if (Em.isEmpty(user)) { return new safe("
    "); } - var username = Em.get(user, 'username'); + const username = Em.get(user, 'username'); if (arguments.length < 4) { uploadId = Em.get(user, 'uploaded_avatar_id'); } - var avatar = Em.get(user, 'avatar_template') || avatarTemplate(username, uploadId); + const avatar = Em.get(user, 'avatar_template') || avatarTemplate(username, uploadId); return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: avatar })); }, 'username', 'uploaded_avatar_id', 'avatar_template'); @@ -24,30 +24,30 @@ Em.Handlebars.helper('bound-avatar-template', function(avatarTemplate, size) { }); registerUnbound('raw-date', function(dt) { - return Discourse.Formatter.longDate(new Date(dt)); + return longDate(new Date(dt)); }); registerUnbound('age-with-tooltip', function(dt) { - return new safe(Discourse.Formatter.autoUpdatingRelativeAge(new Date(dt), {title: true})); + return new safe(autoUpdatingRelativeAge(new Date(dt), {title: true})); }); registerUnbound('number', function(orig, params) { orig = parseInt(orig, 10); if (isNaN(orig)) { orig = 0; } - var title = orig; + let title = orig; if (params.numberKey) { title = I18n.t(params.numberKey, { number: orig }); } - var classNames = 'number'; + let classNames = 'number'; if (params['class']) { classNames += ' ' + params['class']; } - var result = "#reply-control .wmd-preview img:not(.thumbnail), .cooked img:not(.thumbnail) {' + style + '}').appendTo('head'); + } +}; diff --git a/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 b/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 index 1899879943..791e1983e6 100644 --- a/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 +++ b/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 @@ -1,13 +1,14 @@ import { cleanDOM } from 'discourse/routes/discourse'; +import PageTracker from 'discourse/lib/page-tracker'; export default { name: "page-tracking", after: 'register-discourse-location', - initialize: function(container) { + initialize(container) { // Tell our AJAX system to track a page transition - var router = container.lookup('router:main'); + const router = container.lookup('router:main'); router.on('willTransition', function() { Discourse.viewTrackingRequired(); }); @@ -16,7 +17,7 @@ export default { Em.run.scheduleOnce('afterRender', Ember.Route, cleanDOM); }); - var pageTracker = Discourse.PageTracker.current(); + const pageTracker = PageTracker.current(); pageTracker.start(); // Out of the box, Discourse tries to track google analytics diff --git a/app/assets/javascripts/discourse/initializers/relative-ages.js.es6 b/app/assets/javascripts/discourse/initializers/relative-ages.js.es6 index 263f8d1981..aec67ad3fb 100644 --- a/app/assets/javascripts/discourse/initializers/relative-ages.js.es6 +++ b/app/assets/javascripts/discourse/initializers/relative-ages.js.es6 @@ -1,11 +1,11 @@ -/** - Updates the relative ages of dates on the screen. -**/ +import { updateRelativeAge } from 'discourse/lib/formatter'; + +// Updates the relative ages of dates on the screen. export default { name: "relative-ages", initialize: function() { setInterval(function(){ - Discourse.Formatter.updateRelativeAge($('.relative-date')); + updateRelativeAge($('.relative-date')); }, 60 * 1000); } }; diff --git a/app/assets/javascripts/discourse/lib/computed.js b/app/assets/javascripts/discourse/lib/computed.js deleted file mode 100644 index 9cabb58990..0000000000 --- a/app/assets/javascripts/discourse/lib/computed.js +++ /dev/null @@ -1,128 +0,0 @@ -Discourse.computed = { - - /** - Returns whether two properties are equal to each other. - - @method propertyEqual - @params {String} p1 the first property - @params {String} p2 the second property - @return {Function} computedProperty function - **/ - propertyEqual: function(p1, p2) { - return Em.computed(function() { - return this.get(p1) === this.get(p2); - }).property(p1, p2); - }, - - /** - Returns whether two properties are not equal to each other. - - @method propertyNotEqual - @params {String} p1 the first property - @params {String} p2 the second property - @return {Function} computedProperty function - **/ - propertyNotEqual: function(p1, p2) { - return Em.computed(function() { - return this.get(p1) !== this.get(p2); - }).property(p1, p2); - }, - - /** - Returns i18n version of a string based on a property. - - @method i18n - @params {String} properties* to format - @params {String} format the i18n format string - @return {Function} computedProperty function - **/ - i18n: function() { - var args = Array.prototype.slice.call(arguments, 0); - var format = args.pop(); - var computed = Em.computed(function() { - var self = this; - return I18n.t(format.fmt.apply(format, args.map(function (a) { - return self.get(a); - }))); - }); - return computed.property.apply(computed, args); - }, - - /** - Uses an Ember String `fmt` call to format a string. See: - http://emberjs.com/api/classes/Em.String.html#method_fmt - - @method fmt - @params {String} properties* to format - @params {String} format the format string - @return {Function} computedProperty function - **/ - fmt: function() { - var args = Array.prototype.slice.call(arguments, 0); - var format = args.pop(); - var computed = Em.computed(function() { - var self = this; - return format.fmt.apply(format, args.map(function (a) { - return self.get(a); - })); - }); - return computed.property.apply(computed, args); - }, - - /** - Creates a URL using Discourse.getURL. It takes a fmt string just like - fmt does. - - @method url - @params {String} properties* to format - @params {String} format the format string for the URL - @return {Function} computedProperty function returning a URL - **/ - url: function() { - var args = Array.prototype.slice.call(arguments, 0); - var format = args.pop(); - var computed = Em.computed(function() { - var self = this; - return Discourse.getURL(format.fmt.apply(format, args.map(function (a) { - return self.get(a); - }))); - }); - return computed.property.apply(computed, args); - }, - - /** - Returns whether properties end with a string - - @method endWith - @params {String} properties* to check - @params {String} substring the substring - @return {Function} computedProperty function - **/ - endWith: function() { - var args = Array.prototype.slice.call(arguments, 0); - var substring = args.pop(); - var computed = Em.computed(function() { - var self = this; - return _.all(args.map(function(a) { return self.get(a); }), function(s) { - var position = s.length - substring.length, - lastIndex = s.lastIndexOf(substring); - return lastIndex !== -1 && lastIndex === position; - }); - }); - return computed.property.apply(computed, args); - }, - - /** - Creates a property from a SiteSetting. In the future the plan is for them to - be able to update when changed. - - @method setting - @param {String} name of site setting - **/ - setting: function(name) { - return Em.computed(function() { - return Discourse.SiteSettings[name]; - }).property(); - } - -}; diff --git a/app/assets/javascripts/discourse/lib/computed.js.es6 b/app/assets/javascripts/discourse/lib/computed.js.es6 new file mode 100644 index 0000000000..b1b7186b0e --- /dev/null +++ b/app/assets/javascripts/discourse/lib/computed.js.es6 @@ -0,0 +1,124 @@ +/** + Returns whether two properties are equal to each other. + + @method propertyEqual + @params {String} p1 the first property + @params {String} p2 the second property + @return {Function} computedProperty function +**/ +export function propertyEqual(p1, p2) { + return Em.computed(function() { + return this.get(p1) === this.get(p2); + }).property(p1, p2); +} + +/** + Returns whether two properties are not equal to each other. + + @method propertyNotEqual + @params {String} p1 the first property + @params {String} p2 the second property + @return {Function} computedProperty function +**/ +export function propertyNotEqual(p1, p2) { + return Em.computed(function() { + return this.get(p1) !== this.get(p2); + }).property(p1, p2); +} + +/** + Returns i18n version of a string based on a property. + + @method i18n + @params {String} properties* to format + @params {String} format the i18n format string + @return {Function} computedProperty function +**/ +export function i18n() { + const args = Array.prototype.slice.call(arguments, 0); + const format = args.pop(); + const computed = Em.computed(function() { + const self = this; + return I18n.t(format.fmt.apply(format, args.map(function (a) { + return self.get(a); + }))); + }); + return computed.property.apply(computed, args); +} + +/** + Uses an Ember String `fmt` call to format a string. See: + http://emberjs.com/api/classes/Em.String.html#method_fmt + + @method fmt + @params {String} properties* to format + @params {String} format the format string + @return {Function} computedProperty function +**/ +export function fmt() { + const args = Array.prototype.slice.call(arguments, 0); + const format = args.pop(); + const computed = Em.computed(function() { + const self = this; + return format.fmt.apply(format, args.map(function (a) { + return self.get(a); + })); + }); + return computed.property.apply(computed, args); +} + +/** + Creates a URL using Discourse.getURL. It takes a fmt string just like + fmt does. + + @method url + @params {String} properties* to format + @params {String} format the format string for the URL + @return {Function} computedProperty function returning a URL +**/ +export function url() { + const args = Array.prototype.slice.call(arguments, 0); + const format = args.pop(); + const computed = Em.computed(function() { + const self = this; + return Discourse.getURL(format.fmt.apply(format, args.map(function (a) { + return self.get(a); + }))); + }); + return computed.property.apply(computed, args); +} + +/** + Returns whether properties end with a string + + @method endWith + @params {String} properties* to check + @params {String} substring the substring + @return {Function} computedProperty function +**/ +export function endWith() { + const args = Array.prototype.slice.call(arguments, 0); + const substring = args.pop(); + const computed = Em.computed(function() { + const self = this; + return _.all(args.map(function(a) { return self.get(a); }), function(s) { + const position = s.length - substring.length, + lastIndex = s.lastIndexOf(substring); + return lastIndex !== -1 && lastIndex === position; + }); + }); + return computed.property.apply(computed, args); +} + +/** + Creates a property from a SiteSetting. In the future the plan is for them to + be able to update when changed. + + @method setting + @param {String} name of site setting +**/ +export function setting(name) { + return Em.computed(function() { + return Discourse.SiteSettings[name]; + }).property(); +} diff --git a/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 b/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 index ce67ff9dfd..5bdcf40d86 100644 --- a/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 +++ b/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 @@ -1,3 +1,4 @@ +import PageTracker from 'discourse/lib/page-tracker'; let primaryTab = false; let liveEnabled = false; @@ -79,7 +80,7 @@ function setupNotifications() { document.addEventListener("scroll", resetIdle); } window.addEventListener("mouseover", resetIdle); - Discourse.PageTracker.on("change", resetIdle); + PageTracker.on("change", resetIdle); } function resetIdle() { diff --git a/app/assets/javascripts/discourse/lib/eyeline.js b/app/assets/javascripts/discourse/lib/eyeline.js.es6 similarity index 58% rename from app/assets/javascripts/discourse/lib/eyeline.js rename to app/assets/javascripts/discourse/lib/eyeline.js.es6 index ded17866ec..8d40841afd 100644 --- a/app/assets/javascripts/discourse/lib/eyeline.js +++ b/app/assets/javascripts/discourse/lib/eyeline.js.es6 @@ -1,38 +1,29 @@ -/** - Track visible elemnts on the screen. - - @class Eyeline - @namespace Discourse - @module Discourse - @uses RSVP.EventTarget -**/ -Discourse.Eyeline = function Eyeline(selector) { +// Track visible elemnts on the screen. +const Eyeline = function Eyeline(selector) { this.selector = selector; }; -/** - Call this whenever you want to consider what is being seen by the browser +Eyeline.prototype.update = function() { + if (Ember.testing) { return; } - @method update -**/ -Discourse.Eyeline.prototype.update = function() { - var docViewTop = $(window).scrollTop(), - windowHeight = $(window).height(), - docViewBottom = docViewTop + windowHeight, - $elements = $(this.selector), - atBottom = false, - bottomOffset = $elements.last().offset(), - self = this; + const docViewTop = $(window).scrollTop(), + windowHeight = $(window).height(), + docViewBottom = docViewTop + windowHeight, + $elements = $(this.selector), + bottomOffset = $elements.last().offset(), + self = this; + let atBottom = false; if (bottomOffset) { atBottom = (bottomOffset.top <= docViewBottom) && (bottomOffset.top >= docViewTop); } return $elements.each(function(i, elem) { - var $elem = $(elem), - elemTop = $elem.offset().top, - elemBottom = elemTop + $elem.height(), - markSeen = false; + const $elem = $(elem), + elemTop = $elem.offset().top, + elemBottom = elemTop + $elem.height(); + + let markSeen = false; // Make sure the element is visible if (!$elem.is(':visible')) return true; @@ -67,18 +58,15 @@ Discourse.Eyeline.prototype.update = function() { }; -/** - Call this when we know aren't loading any more elements. Mark the rest as seen - - @method flushRest -**/ -Discourse.Eyeline.prototype.flushRest = function() { - var self = this; +// Call this when we know aren't loading any more elements. Mark the rest as seen +Eyeline.prototype.flushRest = function() { + if (Ember.testing) { return; } + const self = this; $(this.selector).each(function(i, elem) { return self.trigger('saw', { detail: $(elem) }); }); }; -RSVP.EventTarget.mixin(Discourse.Eyeline.prototype); - +RSVP.EventTarget.mixin(Eyeline.prototype); +export default Eyeline; diff --git a/app/assets/javascripts/discourse/lib/formatter.js b/app/assets/javascripts/discourse/lib/formatter.js.es6 similarity index 72% rename from app/assets/javascripts/discourse/lib/formatter.js rename to app/assets/javascripts/discourse/lib/formatter.js.es6 index 1b3db5baad..a2cdaefb40 100644 --- a/app/assets/javascripts/discourse/lib/formatter.js +++ b/app/assets/javascripts/discourse/lib/formatter.js.es6 @@ -1,9 +1,5 @@ /* global BreakString:true */ -var updateRelativeAge, autoUpdatingRelativeAge, relativeAge, relativeAgeTiny, - relativeAgeMedium, relativeAgeMediumSpan, longDate, longDateNoYear, toTitleCase, - shortDate, shortDateNoYear, tinyDateYear, relativeAgeTinyShowsYear; - /* * memoize.js * by @philogb and @addyosmani @@ -14,15 +10,15 @@ var updateRelativeAge, autoUpdatingRelativeAge, relativeAge, relativeAgeTiny, * * modified with cap by Sam */ -var cappedMemoize = function ( fn, max ) { +function cappedMemoize(fn, max) { fn.maxMemoize = max; fn.memoizeLength = 0; return function () { - var args = Array.prototype.slice.call(arguments), - hash = "", - i = args.length; - var currentArg = null; + const args = Array.prototype.slice.call(arguments); + let hash = ""; + let i = args.length; + let currentArg = null; while (i--) { currentArg = args[i]; hash += (currentArg === new Object(currentArg)) ? @@ -39,44 +35,45 @@ var cappedMemoize = function ( fn, max ) { fn.memoizeLength = 0; fn.memoize = {}; } - var result = fn.apply(this, args); + const result = fn.apply(this, args); fn.memoize[hash] = result; return result; } }; -}; +} -var breakUp = cappedMemoize(function(str, hint){ +const breakUp = cappedMemoize(function(str, hint){ return new BreakString(str).break(hint); }, 100); +export { breakUp }; -shortDate = function(date){ +export function shortDate(date){ return moment(date).format(I18n.t("dates.medium.date_year")); -}; +} -shortDateNoYear = function(date) { +function shortDateNoYear(date) { return moment(date).format(I18n.t("dates.tiny.date_month")); -}; +} -tinyDateYear = function(date) { +function tinyDateYear(date) { return moment(date).format(I18n.t("dates.tiny.date_year")); -}; +} // http://stackoverflow.com/questions/196972/convert-string-to-title-case-with-javascript // TODO: locale support ? -toTitleCase = function toTitleCase(str) { +export function toTitleCase(str) { return str.replace(/\w\S*/g, function(txt){ return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); }); -}; +} -longDate = function(dt) { +export function longDate(dt) { if (!dt) return; return moment(dt).longDate(); -}; +} // suppress year, if current year -longDateNoYear = function(dt) { +export function longDateNoYear(dt) { if (!dt) return; if ((new Date()).getFullYear() !== dt.getFullYear()) { @@ -84,26 +81,25 @@ longDateNoYear = function(dt) { } else { return moment(dt).format(I18n.t("dates.long_date_without_year")); } -}; +} -updateRelativeAge = function(elems) { +export function updateRelativeAge(elems) { // jQuery .each elems.each(function(){ - var $this = $(this); + const $this = $(this); $this.html(relativeAge(new Date($this.data('time')), {format: $this.data('format'), wrapInSpan: false})); }); -}; +} -autoUpdatingRelativeAge = function(date,options) { +export function autoUpdatingRelativeAge(date,options) { if (!date) return ""; if (+date === +new Date(0)) return ""; options = options || {}; - var format = options.format || "tiny"; + let format = options.format || "tiny"; - var append = ""; - - if(format === 'medium') { + let append = ""; + if (format === 'medium') { append = " date"; if(options.leaveAgo) { format = 'medium-with-ago'; @@ -111,7 +107,7 @@ autoUpdatingRelativeAge = function(date,options) { options.wrapInSpan = false; } - var relAge = relativeAge(date, options); + const relAge = relativeAge(date, options); if (format === 'tiny' && relativeAgeTinyShowsYear(relAge)) { append += " with-year"; @@ -122,16 +118,16 @@ autoUpdatingRelativeAge = function(date,options) { } return "" + relAge + ""; -}; +} -relativeAgeTiny = function(date){ - var format = "tiny"; - var distance = Math.round((new Date() - date) / 1000); - var distanceInMinutes = Math.round(distance / 60.0); +function relativeAgeTiny(date){ + const format = "tiny"; + const distance = Math.round((new Date() - date) / 1000); + const distanceInMinutes = Math.round(distance / 60.0); - var formatted; - var t = function(key,opts){ + let formatted; + const t = function(key,opts){ return I18n.t("dates." + format + "." + key, opts); }; @@ -168,22 +164,21 @@ relativeAgeTiny = function(date){ } return formatted; -}; +} /* * Returns true if the given tiny date string includes the year. * Useful for checking if the string isn't so tiny. */ -relativeAgeTinyShowsYear = function(relativeAgeString) { +function relativeAgeTinyShowsYear(relativeAgeString) { return relativeAgeString.match(/'[\d]{2}$/); -}; +} -relativeAgeMediumSpan = function(distance, leaveAgo) { - var formatted, distanceInMinutes; +function relativeAgeMediumSpan(distance, leaveAgo) { + let formatted; + const distanceInMinutes = Math.round(distance / 60.0); - distanceInMinutes = Math.round(distance / 60.0); - - var t = function(key, opts){ + const t = function(key, opts){ return I18n.t("dates.medium" + (leaveAgo?"_with_ago":"") + "." + key, opts); }; @@ -205,24 +200,22 @@ relativeAgeMediumSpan = function(distance, leaveAgo) { break; } return formatted || '&mdash'; -}; +} -relativeAgeMedium = function(date, options){ - var displayDate, fiveDaysAgo, oneMinuteAgo, fullReadable, leaveAgo; - var wrapInSpan = options.wrapInSpan !== false; - - leaveAgo = options.leaveAgo; - var distance = Math.round((new Date() - date) / 1000); +function relativeAgeMedium(date, options) { + const wrapInSpan = options.wrapInSpan !== false; + const leaveAgo = options.leaveAgo; + const distance = Math.round((new Date() - date) / 1000); if (!date) { return "—"; } - fullReadable = longDate(date); - displayDate = ""; - fiveDaysAgo = 432000; - oneMinuteAgo = 60; + const fullReadable = longDate(date); + const fiveDaysAgo = 432000; + const oneMinuteAgo = 60; + let displayDate = ""; if (distance < oneMinuteAgo) { displayDate = I18n.t("now"); } else if (distance > fiveDaysAgo) { @@ -239,12 +232,12 @@ relativeAgeMedium = function(date, options){ } else { return displayDate; } -}; +} // mostly lifted from rails with a few amendments -relativeAge = function(date, options) { +export function relativeAge(date, options) { options = options || {}; - var format = options.format || "tiny"; + const format = options.format || "tiny"; if(format === "tiny") { return relativeAgeTiny(date, options); @@ -255,10 +248,10 @@ relativeAge = function(date, options) { } return "UNKNOWN FORMAT"; -}; +} -var number = function(val) { - var formattedNumber; +export function number(val) { + let formattedNumber; val = parseInt(val, 10); if (isNaN(val)) val = 0; @@ -272,17 +265,5 @@ var number = function(val) { return I18n.t("number.short.thousands", {number: formattedNumber}); } return val.toString(); -}; +} -Discourse.Formatter = { - longDate: longDate, - longDateNoYear: longDateNoYear, - relativeAge: relativeAge, - autoUpdatingRelativeAge: autoUpdatingRelativeAge, - updateRelativeAge: updateRelativeAge, - toTitleCase: toTitleCase, - shortDate: shortDate, - breakUp: breakUp, - cappedMemoize: cappedMemoize, - number: number -}; diff --git a/app/assets/javascripts/discourse/lib/key_value_store.js b/app/assets/javascripts/discourse/lib/key_value_store.js index 17d17c76a3..ecb1651eb9 100644 --- a/app/assets/javascripts/discourse/lib/key_value_store.js +++ b/app/assets/javascripts/discourse/lib/key_value_store.js @@ -10,9 +10,13 @@ var safeLocalStorage; try { - safeLocalStorage = localStorage; + safeLocalStorage = localStorage; + if (localStorage["disableLocalStorage"] === "true") { + safeLocalStorage = null; + } } catch(e){ // cookies disabled, we don't care + safeLocalStorage = null; } Discourse.KeyValueStore = { diff --git a/app/assets/javascripts/discourse/lib/page_tracker.js b/app/assets/javascripts/discourse/lib/page-tracker.js.es6 similarity index 77% rename from app/assets/javascripts/discourse/lib/page_tracker.js rename to app/assets/javascripts/discourse/lib/page-tracker.js.es6 index 842f2d9b11..9a7dcc9c9d 100644 --- a/app/assets/javascripts/discourse/lib/page_tracker.js +++ b/app/assets/javascripts/discourse/lib/page-tracker.js.es6 @@ -1,3 +1,5 @@ +import Singleton from 'discourse/mixins/singleton'; + /** Called whenever the "page" changes. This allows us to set up analytics and other tracking. @@ -5,12 +7,12 @@ To get notified when the page changes, you can install a hook like so: ```javascript - Discourse.PageTracker.current().on('change', function(url, title) { + PageTracker.current().on('change', function(url, title) { console.log('the page changed to: ' + url + ' and title ' + title); }); ``` **/ -Discourse.PageTracker = Ember.Object.extend(Ember.Evented, { +const PageTracker = Ember.Object.extend(Ember.Evented, { start: function() { if (this.get('started')) { return; } @@ -30,4 +32,6 @@ Discourse.PageTracker = Ember.Object.extend(Ember.Evented, { this.set('started', true); } }); -Discourse.PageTracker.reopenClass(Discourse.Singleton); +PageTracker.reopenClass(Singleton); + +export default PageTracker; diff --git a/app/assets/javascripts/discourse/lib/screen_track.js b/app/assets/javascripts/discourse/lib/screen-track.js.es6 similarity index 72% rename from app/assets/javascripts/discourse/lib/screen_track.js rename to app/assets/javascripts/discourse/lib/screen-track.js.es6 index 595a5321ce..3d55418cfb 100644 --- a/app/assets/javascripts/discourse/lib/screen_track.js +++ b/app/assets/javascripts/discourse/lib/screen-track.js.es6 @@ -1,23 +1,18 @@ -/** - We use this class to track how long posts in a topic are on the screen. +// We use this class to track how long posts in a topic are on the screen. - @class ScreenTrack - @extends Ember.Object - @namespace Discourse - @module Discourse -**/ +import Singleton from 'discourse/mixins/singleton'; -var PAUSE_UNLESS_SCROLLED = 1000 * 60 * 3, - MAX_TRACKING_TIME = 1000 * 60 * 6; +const PAUSE_UNLESS_SCROLLED = 1000 * 60 * 3, + MAX_TRACKING_TIME = 1000 * 60 * 6; -Discourse.ScreenTrack = Ember.Object.extend({ +const ScreenTrack = Ember.Object.extend({ - init: function() { + init() { this.reset(); }, - start: function(topicId, topicController) { - var currentTopicId = this.get('topicId'); + start(topicId, topicController) { + const currentTopicId = this.get('topicId'); if (currentTopicId && (currentTopicId !== topicId)) { this.tick(); this.flush(); @@ -27,7 +22,7 @@ Discourse.ScreenTrack = Ember.Object.extend({ // Create an interval timer if we don't have one. if (!this.get('interval')) { - var self = this; + const self = this; this.set('interval', setInterval(function () { self.tick(); }, 1000)); @@ -39,7 +34,7 @@ Discourse.ScreenTrack = Ember.Object.extend({ this.set('topicController', topicController); }, - stop: function() { + stop() { if(!this.get('topicId')) { // already stopped no need to "extra stop" return; @@ -56,19 +51,19 @@ Discourse.ScreenTrack = Ember.Object.extend({ } }, - track: function(elementId, postNumber) { + track(elementId, postNumber) { this.get('timings')["#" + elementId] = { time: 0, postNumber: postNumber }; }, - stopTracking: function(elementId) { + stopTracking(elementId) { delete this.get('timings')['#' + elementId]; }, // Reset our timers - reset: function() { + reset() { this.setProperties({ lastTick: new Date().getTime(), lastScrolled: new Date().getTime(), @@ -80,17 +75,17 @@ Discourse.ScreenTrack = Ember.Object.extend({ }); }, - scrolled: function() { + scrolled() { this.set('lastScrolled', new Date().getTime()); }, - flush: function() { + flush() { if (this.get('cancelled')) { return; } // We don't log anything unless we're logged in if (!Discourse.User.current()) return; - var newTimings = {}, + const newTimings = {}, totalTimings = this.get('totalTimings'), self = this; @@ -105,14 +100,14 @@ Discourse.ScreenTrack = Ember.Object.extend({ timing.time = 0; }); - var topicId = parseInt(this.get('topicId'), 10), - highestSeen = 0; + const topicId = parseInt(this.get('topicId'), 10); + let highestSeen = 0; _.each(newTimings, function(time,postNumber) { highestSeen = Math.max(highestSeen, parseInt(postNumber, 10)); }); - var highestSeenByTopic = Discourse.Session.currentProp('highestSeenByTopic'); + const highestSeenByTopic = Discourse.Session.currentProp('highestSeenByTopic'); if ((highestSeenByTopic[topicId] || 0) < highestSeen) { highestSeenByTopic[topicId] = highestSeen; } @@ -132,9 +127,9 @@ Discourse.ScreenTrack = Ember.Object.extend({ 'X-SILENCE-LOGGER': 'true' } }).then(function(){ - var controller = self.get('topicController'); + const controller = self.get('topicController'); if(controller){ - var postNumbers = Object.keys(newTimings).map(function(v){ + const postNumbers = Object.keys(newTimings).map(function(v){ return parseInt(v,10); }); controller.readPosts(topicId, postNumbers); @@ -146,23 +141,23 @@ Discourse.ScreenTrack = Ember.Object.extend({ this.set('lastFlush', 0); }, - tick: function() { + tick() { // If the user hasn't scrolled the browser in a long time, stop tracking time read - var sinceScrolled = new Date().getTime() - this.get('lastScrolled'); + const sinceScrolled = new Date().getTime() - this.get('lastScrolled'); if (sinceScrolled > PAUSE_UNLESS_SCROLLED) { return; } - var diff = new Date().getTime() - this.get('lastTick'); + const diff = new Date().getTime() - this.get('lastTick'); this.set('lastFlush', this.get('lastFlush') + diff); this.set('lastTick', new Date().getTime()); - var totalTimings = this.get('totalTimings'), timings = this.get('timings'); - var nextFlush = Discourse.SiteSettings.flush_timings_secs * 1000; + const totalTimings = this.get('totalTimings'), timings = this.get('timings'); + const nextFlush = Discourse.SiteSettings.flush_timings_secs * 1000; // rush new post numbers - var rush = _.any(_.filter(timings, function(t){return t.time>0;}), function(t){ + const rush = _.any(_.filter(timings, function(t){return t.time>0;}), function(t){ return !totalTimings[t.postNumber]; }); @@ -174,15 +169,15 @@ Discourse.ScreenTrack = Ember.Object.extend({ if (!Discourse.get("hasFocus")) return; this.set('topicTime', this.get('topicTime') + diff); - var docViewTop = $(window).scrollTop() + $('header').height(), + const docViewTop = $(window).scrollTop() + $('header').height(), docViewBottom = docViewTop + $(window).height(); // TODO: Eyeline has a smarter more accurate function here. It's bad to do jQuery // in a model like component, so we should refactor this out later. _.each(this.get('timings'),function(timing,id) { - var $element = $(id); + const $element = $(id); if ($element.length === 1) { - var elemTop = $element.offset().top, + const elemTop = $element.offset().top, elemBottom = elemTop + $element.height(); // If part of the element is on the screen, increase the counter @@ -195,5 +190,5 @@ Discourse.ScreenTrack = Ember.Object.extend({ }); -Discourse.ScreenTrack.reopenClass(Discourse.Singleton); - +ScreenTrack.reopenClass(Singleton); +export default ScreenTrack; diff --git a/app/assets/javascripts/discourse/lib/search-for-term.js.es6 b/app/assets/javascripts/discourse/lib/search-for-term.js.es6 index e5348a6a13..2d029fdf9e 100644 --- a/app/assets/javascripts/discourse/lib/search-for-term.js.es6 +++ b/app/assets/javascripts/discourse/lib/search-for-term.js.es6 @@ -77,9 +77,13 @@ function searchForTerm(term, opts) { }; } - return Discourse.ajax('/search/query', { data: data }).then(function(results){ + var promise = Discourse.ajax('/search/query', { data: data }); + + promise.then(function(results){ return translateResults(results, opts); }); + + return promise; } export default searchForTerm; diff --git a/app/assets/javascripts/discourse/lib/utilities.js b/app/assets/javascripts/discourse/lib/utilities.js index 6d08ab9347..92c66cd53c 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js +++ b/app/assets/javascripts/discourse/lib/utilities.js @@ -179,7 +179,7 @@ Discourse.Utilities = { // check file size var fileSizeKB = file.size / 1024; - var maxSizeKB = Discourse.SiteSettings['max_' + type + '_size_kb']; + var maxSizeKB = 10 * 1024; // 10MB if (fileSizeKB > maxSizeKB) { bootbox.alert(I18n.t('post.errors.file_too_large', { max_size_kb: maxSizeKB })); return false; @@ -243,7 +243,7 @@ Discourse.Utilities = { // entity too large, usually returned from the web server case 413: - var maxSizeKB = Discourse.SiteSettings.max_image_size_kb; + var maxSizeKB = 10 * 1024; // 10 MB bootbox.alert(I18n.t('post.errors.file_too_large', { max_size_kb: maxSizeKB })); return; diff --git a/app/assets/javascripts/discourse/mixins/ajax.js b/app/assets/javascripts/discourse/mixins/ajax.js index 6817150d51..4ca8c9a131 100644 --- a/app/assets/javascripts/discourse/mixins/ajax.js +++ b/app/assets/javascripts/discourse/mixins/ajax.js @@ -19,6 +19,7 @@ Discourse.Ajax = Em.Mixin.create({ **/ ajax: function() { var url, args; + var ajax; if (arguments.length === 1) { if (typeof arguments[0] === "string") { @@ -95,22 +96,32 @@ Discourse.Ajax = Em.Mixin.create({ args.cache = false; } - $.ajax(Discourse.getURL(url), args); + ajax = $.ajax(Discourse.getURL(url), args); }; + var promise; + // For cached pages we strip out CSRF tokens, need to round trip to server prior to sending the // request (bypass for GET, not needed) if(args.type && args.type.toUpperCase() !== 'GET' && !Discourse.Session.currentProp('csrfToken')){ - return new Ember.RSVP.Promise(function(resolve, reject){ - $.ajax(Discourse.getURL('/session/csrf'), {cache: false}) + promise = new Ember.RSVP.Promise(function(resolve, reject){ + ajax = $.ajax(Discourse.getURL('/session/csrf'), {cache: false}) .success(function(result){ Discourse.Session.currentProp('csrfToken', result.csrf); performAjax(resolve, reject); }); }); } else { - return new Ember.RSVP.Promise(performAjax); + promise = new Ember.RSVP.Promise(performAjax); } + + promise.abort = function(){ + if (ajax) { + ajax.abort(); + } + }; + + return promise; } }); diff --git a/app/assets/javascripts/discourse/mixins/can-check-emails.js.es6 b/app/assets/javascripts/discourse/mixins/can-check-emails.js.es6 index 22742d7451..dc802c4d04 100644 --- a/app/assets/javascripts/discourse/mixins/can-check-emails.js.es6 +++ b/app/assets/javascripts/discourse/mixins/can-check-emails.js.es6 @@ -1,6 +1,8 @@ +import { propertyEqual, setting } from 'discourse/lib/computed'; + export default Ember.Mixin.create({ - isOwnEmail: Discourse.computed.propertyEqual("model.id", "currentUser.id"), - showEmailOnProfile: Discourse.computed.setting("show_email_on_profile"), + isOwnEmail: propertyEqual("model.id", "currentUser.id"), + showEmailOnProfile: setting("show_email_on_profile"), canStaffCheckEmails: Em.computed.and("showEmailOnProfile", "currentUser.staff"), canAdminCheckEmails: Em.computed.alias("currentUser.admin"), canCheckEmails: Em.computed.or("isOwnEmail", "canStaffCheckEmails", "canAdminCheckEmails"), diff --git a/app/assets/javascripts/discourse/mixins/load-more.js.es6 b/app/assets/javascripts/discourse/mixins/load-more.js.es6 index 0726b13bd7..d0e63e4ba4 100644 --- a/app/assets/javascripts/discourse/mixins/load-more.js.es6 +++ b/app/assets/javascripts/discourse/mixins/load-more.js.es6 @@ -1,13 +1,16 @@ -// Provides the ability to load more items for a view which is scrolled to the bottom. -export default Em.Mixin.create(Ember.ViewTargetActionSupport, Discourse.Scrolling, { +import Eyeline from 'discourse/lib/eyeline'; +import Scrolling from 'discourse/mixins/scrolling'; - scrolled: function() { +// Provides the ability to load more items for a view which is scrolled to the bottom. +export default Ember.Mixin.create(Ember.ViewTargetActionSupport, Scrolling, { + + scrolled() { const eyeline = this.get('eyeline'); if (eyeline) { eyeline.update(); } }, _bindEyeline: function() { - const eyeline = new Discourse.Eyeline(this.get('eyelineSelector') + ":last"); + const eyeline = new Eyeline(this.get('eyelineSelector') + ":last"); this.set('eyeline', eyeline); eyeline.on('sawBottom', () => this.send('loadMore')); this.bindScrolling(); diff --git a/app/assets/javascripts/discourse/mixins/scrolling.js b/app/assets/javascripts/discourse/mixins/scrolling.js deleted file mode 100644 index c9473c155e..0000000000 --- a/app/assets/javascripts/discourse/mixins/scrolling.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - This mixin adds support for being notified every time the browser window - is scrolled. - - @class Scrolling - @extends Ember.Mixin - @namespace Discourse - @module Discourse -**/ - -Discourse.Scrolling = Em.Mixin.create({ - - /** - Begin watching for scroll events. By default they will be called at max every 100ms. - call with {debounce: N} for a diff time - - @method bindScrolling - */ - bindScrolling: function(opts) { - opts = opts || {debounce: 100}; - - // So we can not call the scrolled event while transitioning - var router = Discourse.__container__.lookup('router:main').router; - - var self = this, - onScrollMethod = function() { - if (router.activeTransition) { return; } - return Em.run.scheduleOnce('afterRender', self, 'scrolled'); - }; - - if (opts.debounce) { - onScrollMethod = Discourse.debounce(onScrollMethod, opts.debounce); - } - - Discourse.ScrollingDOMMethods.bindOnScroll(onScrollMethod, opts.name); - Em.run.scheduleOnce('afterRender', onScrollMethod); - }, - - /** - Stop watching for scroll events. - - @method unbindScrolling - */ - unbindScrolling: function(name) { - Discourse.ScrollingDOMMethods.unbindOnScroll(name); - } - -}); - - -/** - This object provides the DOM methods we need for our Mixin to bind to scrolling - methods in the browser. By removing them from the Mixin we can test them - easier. - - @class ScrollingDOMMethods - @module Discourse -**/ -Discourse.ScrollingDOMMethods = { - - bindOnScroll: function(onScrollMethod, name) { - name = name || 'default'; - $(document).bind('touchmove.discourse-' + name, onScrollMethod); - $(window).bind('scroll.discourse-' + name, onScrollMethod); - }, - - unbindOnScroll: function(name) { - name = name || 'default'; - $(window).unbind('scroll.discourse-' + name); - $(document).unbind('touchmove.discourse-' + name); - } - -}; diff --git a/app/assets/javascripts/discourse/mixins/scrolling.js.es6 b/app/assets/javascripts/discourse/mixins/scrolling.js.es6 new file mode 100644 index 0000000000..459bbd7119 --- /dev/null +++ b/app/assets/javascripts/discourse/mixins/scrolling.js.es6 @@ -0,0 +1,50 @@ +/** + This object provides the DOM methods we need for our Mixin to bind to scrolling + methods in the browser. By removing them from the Mixin we can test them + easier. +**/ +const ScrollingDOMMethods = { + bindOnScroll: function(onScrollMethod, name) { + name = name || 'default'; + $(document).bind('touchmove.discourse-' + name, onScrollMethod); + $(window).bind('scroll.discourse-' + name, onScrollMethod); + }, + + unbindOnScroll: function(name) { + name = name || 'default'; + $(window).unbind('scroll.discourse-' + name); + $(document).unbind('touchmove.discourse-' + name); + } +}; + +const Scrolling = Ember.Mixin.create({ + + // Begin watching for scroll events. By default they will be called at max every 100ms. + // call with {debounce: N} for a diff time + bindScrolling: function(opts) { + opts = opts || {debounce: 100}; + + // So we can not call the scrolled event while transitioning + const router = Discourse.__container__.lookup('router:main').router; + + const self = this; + var onScrollMethod = function() { + if (router.activeTransition) { return; } + return Em.run.scheduleOnce('afterRender', self, 'scrolled'); + }; + + if (opts.debounce) { + onScrollMethod = Discourse.debounce(onScrollMethod, opts.debounce); + } + + ScrollingDOMMethods.bindOnScroll(onScrollMethod, opts.name); + Em.run.scheduleOnce('afterRender', onScrollMethod); + }, + + unbindScrolling: function(name) { + ScrollingDOMMethods.unbindOnScroll(name); + } +}); + +export { ScrollingDOMMethods }; +export default Scrolling; diff --git a/app/assets/javascripts/discourse/mixins/singleton.js b/app/assets/javascripts/discourse/mixins/singleton.js.es6 similarity index 61% rename from app/assets/javascripts/discourse/mixins/singleton.js rename to app/assets/javascripts/discourse/mixins/singleton.js.es6 index 73ce7a33e7..3bd27813e5 100644 --- a/app/assets/javascripts/discourse/mixins/singleton.js +++ b/app/assets/javascripts/discourse/mixins/singleton.js.es6 @@ -9,7 +9,7 @@ // Define your class and apply the Mixin User = Ember.Object.extend({}); - User.reopenClass(Discourse.Singleton); + User.reopenClass(Singleton); // Retrieve the current instance: var instance = User.current(); @@ -35,7 +35,7 @@ // Define your class and apply the Mixin Foot = Ember.Object.extend({}); - Foot.reopenClass(Discourse.Singleton, { + Foot.reopenClass(Singleton, { createCurrent: function() { return Foot.create({toes: 5}); } @@ -44,51 +44,28 @@ console.log(Foot.currentProp('toes')); // 5 ``` - - @class Discourse.Singleton - @extends Ember.Mixin - @namespace Discourse - @module Discourse **/ -Discourse.Singleton = Em.Mixin.create({ +const Singleton = Ember.Mixin.create({ - /** - Returns the current singleton instance of the class. - - @method current - @returns {Ember.Object} the instance of the singleton - **/ - current: function() { + current() { if (!this._current) { this._current = this.createCurrent(); } - return this._current; }, - /** How the singleton instance is created. This can be overridden with logic for creating (or even returning null) your instance. By default it just calls `create` with an empty object. - - @method createCurrent - @returns {Ember.Object} the instance that will be your singleton **/ - createCurrent: function() { + createCurrent() { return this.create({}); }, - /** - Returns or sets a property on the singleton instance. - - @method currentProp - @param {String} property the property we want to get or set - @param {String} value the optional value to set the property to - @returns the value of the property - **/ - currentProp: function(property, value) { + // Returns OR sets a property on the singleton instance. + currentProp(property, value) { var instance = this.current(); if (!instance) { return; } @@ -100,14 +77,9 @@ Discourse.Singleton = Em.Mixin.create({ } }, - /** - Resets the current singleton. Useful in testing. - - @method resetCurrent - **/ - resetCurrent: function(val) { + resetCurrent(val) { this._current = val; } }); - +export default Singleton; diff --git a/app/assets/javascripts/discourse/models/admin_post.js b/app/assets/javascripts/discourse/models/admin-post.js.es6 similarity index 73% rename from app/assets/javascripts/discourse/models/admin_post.js rename to app/assets/javascripts/discourse/models/admin-post.js.es6 index 8db9dfec69..4504fcbe24 100644 --- a/app/assets/javascripts/discourse/models/admin_post.js +++ b/app/assets/javascripts/discourse/models/admin-post.js.es6 @@ -1,15 +1,9 @@ -/** - A data model for flagged/deleted posts. +import Post from 'discourse/models/post'; - @class AdminPost - @extends Discourse.Post - @namespace Discourse - @module Discourse -**/ -Discourse.AdminPost = Discourse.Post.extend({ +export default Post.extend({ _attachCategory: function () { - var categoryId = this.get("category_id"); + const categoryId = this.get("category_id"); if (categoryId) { this.set("category", Discourse.Category.findById(categoryId)); } diff --git a/app/assets/javascripts/discourse/models/archetype.js b/app/assets/javascripts/discourse/models/archetype.js deleted file mode 100644 index cfbc88c418..0000000000 --- a/app/assets/javascripts/discourse/models/archetype.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - A data model for archetypes such as polls, tasks, etc. - - @class Archetype - @extends Discourse.Model - @namespace Discourse - @module Discourse -**/ -Discourse.Archetype = Discourse.Model.extend({ - - hasOptions: Em.computed.gt('options.length', 0), - - site: function() { - return Discourse.Site.current(); - }.property(), - - isDefault: Discourse.computed.propertyEqual('id', 'site.default_archetype'), - notDefault: Em.computed.not('isDefault') - -}); - - diff --git a/app/assets/javascripts/discourse/models/archetype.js.es6 b/app/assets/javascripts/discourse/models/archetype.js.es6 new file mode 100644 index 0000000000..3931858f75 --- /dev/null +++ b/app/assets/javascripts/discourse/models/archetype.js.es6 @@ -0,0 +1,8 @@ +import { propertyEqual } from 'discourse/lib/computed'; +import RestModel from 'discourse/models/rest'; + +export default RestModel.extend({ + hasOptions: Em.computed.gt('options.length', 0), + isDefault: propertyEqual('id', 'site.default_archetype'), + notDefault: Em.computed.not('isDefault') +}); diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index edde5df3c6..e58f62362e 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -101,12 +101,13 @@ const Composer = RestModel.extend({ actionTitle: function() { const topic = this.get('topic'); - let postLink, topicLink; + let postLink, topicLink, usernameLink; if (topic) { const postNumber = this.get('post.post_number'); postLink = "" + I18n.t("post.post_number", { number: postNumber }) + ""; topicLink = " " + (Handlebars.Utils.escapeExpression(topic.get('title'))) + ""; + usernameLink = "" + this.get('post.username') + ""; } let postDescription; @@ -116,7 +117,8 @@ const Composer = RestModel.extend({ postDescription = I18n.t('post.' + this.get('action'), { link: postLink, replyAvatar: Discourse.Utilities.tinyAvatar(post.get('avatar_template')), - username: this.get('post.username') + username: this.get('post.username'), + usernameLink }); if (!Discourse.Mobile.mobileView) { @@ -491,15 +493,17 @@ const Composer = RestModel.extend({ this.set('composeState', CLOSED); + var rollback = throwAjaxError(function(){ + post.set('cooked', oldCooked); + self.set('composeState', OPEN); + }); + return promise.then(function() { return post.save(props).then(function(result) { self.clearState(); return result; - }).catch(throwAjaxError(function() { - post.set('cooked', oldCooked); - self.set('composeState', OPEN); - })); - }); + }).catch(rollback); + }).catch(rollback); }, serialize(serializer, dest) { diff --git a/app/assets/javascripts/discourse/models/group.js.es6 b/app/assets/javascripts/discourse/models/group.js.es6 index 984941e89f..b79db164f4 100644 --- a/app/assets/javascripts/discourse/models/group.js.es6 +++ b/app/assets/javascripts/discourse/models/group.js.es6 @@ -50,14 +50,7 @@ const Group = Discourse.Model.extend({ type: "PUT", data: { usernames: usernames } }).then(function() { - // reload member list self.findMembers(); - }).catch(function(error) { - if (error && error.responseText) { - bootbox.alert($.parseJSON(error.responseText).errors[0]); - } else { - bootbox.alert(I18n.t('generic_error')); - } }); }, diff --git a/app/assets/javascripts/discourse/models/nav-item.js.es6 b/app/assets/javascripts/discourse/models/nav-item.js.es6 index def6a3430a..d4e5ef228b 100644 --- a/app/assets/javascripts/discourse/models/nav-item.js.es6 +++ b/app/assets/javascripts/discourse/models/nav-item.js.es6 @@ -1,11 +1,4 @@ -/** - A data model representing a navigation item on the list views - - @class NavItem - @extends Discourse.Model - @namespace Discourse - @module Discourse -**/ +import { toTitleCase } from 'discourse/lib/formatter'; const NavItem = Discourse.Model.extend({ @@ -22,7 +15,7 @@ const NavItem = Discourse.Model.extend({ if (categoryName) { name = 'category'; - extra.categoryName = Discourse.Formatter.toTitleCase(categoryName); + extra.categoryName = toTitleCase(categoryName); } return I18n.t("filters." + name.replace("/", ".") + ".title", extra); }.property('categoryName', 'name', 'count'), diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index a678573077..7f6ccf38d8 100644 --- a/app/assets/javascripts/discourse/models/post.js.es6 +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -1,6 +1,7 @@ import RestModel from 'discourse/models/rest'; import { popupAjaxError } from 'discourse/lib/ajax-error'; import ActionSummary from 'discourse/models/action-summary'; +import { url, fmt, propertyEqual } from 'discourse/lib/computed'; const Post = RestModel.extend({ @@ -57,7 +58,7 @@ const Post = RestModel.extend({ return (this.get('post_number') === 1) ? url + "/1" : url; }.property('post_number', 'url'), - usernameUrl: Discourse.computed.url('username', '/users/%@'), + usernameUrl: url('username', '/users/%@'), showUserReplyTab: function() { return this.get('reply_to_user') && ( @@ -66,9 +67,9 @@ const Post = RestModel.extend({ ); }.property('reply_to_user', 'reply_to_post_number', 'post_number'), - topicOwner: Discourse.computed.propertyEqual('topic.details.created_by.id', 'user_id'), + topicOwner: propertyEqual('topic.details.created_by.id', 'user_id'), hasHistory: Em.computed.gt('version', 1), - postElementId: Discourse.computed.fmt('post_number', 'post_%@'), + postElementId: fmt('post_number', 'post_%@'), canViewRawEmail: function() { return this.get("user_id") === Discourse.User.currentProp("id") || Discourse.User.currentProp('staff'); diff --git a/app/assets/javascripts/discourse/models/session.js.es6 b/app/assets/javascripts/discourse/models/session.js.es6 index 894dc6b2fd..f875493393 100644 --- a/app/assets/javascripts/discourse/models/session.js.es6 +++ b/app/assets/javascripts/discourse/models/session.js.es6 @@ -1,11 +1,13 @@ +import RestModel from 'discourse/models/rest'; +import Singleton from 'discourse/mixins/singleton'; + // A data model representing current session data. You can put transient // data here you might want later. It is not stored or serialized anywhere. -var Session = Discourse.Model.extend({ +const Session = RestModel.extend({ init: function() { this.set('highestSeenByTopic', {}); } }); -Session.reopenClass(Discourse.Singleton); - +Session.reopenClass(Singleton); export default Session; diff --git a/app/assets/javascripts/discourse/models/site.js.es6 b/app/assets/javascripts/discourse/models/site.js.es6 index e3466eada0..226e1ed811 100644 --- a/app/assets/javascripts/discourse/models/site.js.es6 +++ b/app/assets/javascripts/discourse/models/site.js.es6 @@ -1,4 +1,6 @@ +import Archetype from 'discourse/models/archetype'; import PostActionType from 'discourse/models/post-action-type'; +import Singleton from 'discourse/mixins/singleton'; const Site = Discourse.Model.extend({ @@ -85,11 +87,11 @@ const Site = Discourse.Model.extend({ } }); -Site.reopenClass(Discourse.Singleton, { +Site.reopenClass(Singleton, { // The current singleton will retrieve its attributes from the `PreloadStore`. createCurrent() { - return Discourse.Site.create(PreloadStore.get('site')); + return Site.create(PreloadStore.get('site')); }, create() { @@ -137,7 +139,8 @@ Site.reopenClass(Discourse.Singleton, { if (result.archetypes) { result.archetypes = _.map(result.archetypes,function(a) { - return Discourse.Archetype.create(a); + a.site = result; + return Archetype.create(a); }); } diff --git a/app/assets/javascripts/discourse/models/store.js.es6 b/app/assets/javascripts/discourse/models/store.js.es6 index cda112cefb..a9aa86f294 100644 --- a/app/assets/javascripts/discourse/models/store.js.es6 +++ b/app/assets/javascripts/discourse/models/store.js.es6 @@ -116,6 +116,12 @@ export default Ember.Object.extend({ }, destroyRecord(type, record) { + // If the record is new, don't perform an Ajax call + if (record.get('isNew')) { + removeMap(type, record.get('id')); + return Ember.RSVP.Promise.resolve(true); + } + return this.adapterFor(type).destroyRecord(this, type, record).then(function(result) { removeMap(type, record.get('id')); return result; diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index 2455087411..0fab68fa10 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -1,5 +1,7 @@ import { flushMap } from 'discourse/models/store'; import RestModel from 'discourse/models/rest'; +import { propertyEqual } from 'discourse/lib/computed'; +import { longDate } from 'discourse/lib/formatter'; const Topic = RestModel.extend({ message: null, @@ -20,8 +22,8 @@ const Topic = RestModel.extend({ }.property('bumped_at', 'createdAt'), bumpedAtTitle: function() { - return I18n.t('first_post') + ": " + Discourse.Formatter.longDate(this.get('createdAt')) + "\n" + - I18n.t('last_post') + ": " + Discourse.Formatter.longDate(this.get('bumpedAt')); + return I18n.t('first_post') + ": " + longDate(this.get('createdAt')) + "\n" + + I18n.t('last_post') + ": " + longDate(this.get('bumpedAt')); }.property('bumpedAt'), createdAt: function() { @@ -364,7 +366,7 @@ const Topic = RestModel.extend({ return( e && e.substr(e.length - 8,8) === '…' ); }.property('excerpt'), - readLastPost: Discourse.computed.propertyEqual('last_read_post_number', 'highest_post_number'), + readLastPost: propertyEqual('last_read_post_number', 'highest_post_number'), canClearPin: Em.computed.and('pinned', 'readLastPost') }); diff --git a/app/assets/javascripts/discourse/models/trust-level.js.es6 b/app/assets/javascripts/discourse/models/trust-level.js.es6 new file mode 100644 index 0000000000..f51eda37e3 --- /dev/null +++ b/app/assets/javascripts/discourse/models/trust-level.js.es6 @@ -0,0 +1,6 @@ +import RestModel from 'discourse/models/rest'; +import { fmt } from 'discourse/lib/computed'; + +export default RestModel.extend({ + detailedName: fmt('id', 'name', '%@ - %@') +}); diff --git a/app/assets/javascripts/discourse/models/trust_level.js b/app/assets/javascripts/discourse/models/trust_level.js deleted file mode 100644 index 8694010119..0000000000 --- a/app/assets/javascripts/discourse/models/trust_level.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - Represents a user's trust level in the system - - @class TrustLevel - @extends Discourse.Model - @namespace Discourse - @module Discourse -**/ -Discourse.TrustLevel = Discourse.Model.extend({ - detailedName: Discourse.computed.fmt('id', 'name', '%@ - %@') -}); diff --git a/app/assets/javascripts/discourse/models/user-action-stat.js.es6 b/app/assets/javascripts/discourse/models/user-action-stat.js.es6 new file mode 100644 index 0000000000..073c303247 --- /dev/null +++ b/app/assets/javascripts/discourse/models/user-action-stat.js.es6 @@ -0,0 +1,21 @@ +import RestModel from 'discourse/models/rest'; +import UserAction from 'discourse/models/user-action'; +import { i18n } from 'discourse/lib/computed'; + +export default RestModel.extend({ + + isPM: function() { + const actionType = this.get('action_type'); + return actionType === UserAction.TYPES.messages_sent || + actionType === UserAction.TYPES.messages_received; + }.property('action_type'), + + description: i18n('action_type', 'user_action_groups.%@'), + + isResponse: function() { + const actionType = this.get('action_type'); + return actionType === UserAction.TYPES.replies || + actionType === UserAction.TYPES.quotes; + }.property('action_type') + +}); diff --git a/app/assets/javascripts/discourse/models/user_action.js b/app/assets/javascripts/discourse/models/user-action.js.es6 similarity index 81% rename from app/assets/javascripts/discourse/models/user_action.js rename to app/assets/javascripts/discourse/models/user-action.js.es6 index 2c5060258d..2d273c43e1 100644 --- a/app/assets/javascripts/discourse/models/user_action.js +++ b/app/assets/javascripts/discourse/models/user-action.js.es6 @@ -1,39 +1,37 @@ -var UserActionTypes = { - likes_given: 1, - likes_received: 2, - bookmarks: 3, - topics: 4, - posts: 5, - replies: 6, - mentions: 7, - quotes: 9, - edits: 11, - messages_sent: 12, - messages_received: 13, - pending: 14 - }, - InvertedActionTypes = {}; +import RestModel from 'discourse/models/rest'; +import { url } from 'discourse/lib/computed'; + +const UserActionTypes = { + likes_given: 1, + likes_received: 2, + bookmarks: 3, + topics: 4, + posts: 5, + replies: 6, + mentions: 7, + quotes: 9, + edits: 11, + messages_sent: 12, + messages_received: 13, + pending: 14 +}; +const InvertedActionTypes = {}; _.each(UserActionTypes, function (k, v) { InvertedActionTypes[k] = v; }); -Discourse.UserAction = Discourse.Model.extend({ +const UserAction = RestModel.extend({ _attachCategory: function() { - var categoryId = this.get('category_id'); + const categoryId = this.get('category_id'); if (categoryId) { this.set('category', Discourse.Category.findById(categoryId)); } }.on('init'), - /** - Return an i18n key we will use for the description text of a user action. - - @property descriptionKey - **/ descriptionKey: function() { - var action = this.get('action_type'); + const action = this.get('action_type'); if (action === null || Discourse.UserAction.TO_SHOW.indexOf(action) >= 0) { if (this.get('isPM')) { return this.get('sameUser') ? 'sent_by_you' : 'sent_by_user'; @@ -74,13 +72,13 @@ Discourse.UserAction = Discourse.Model.extend({ presentName: Em.computed.any('name', 'username'), targetDisplayName: Em.computed.any('target_name', 'target_username'), actingDisplayName: Em.computed.any('acting_name', 'acting_username'), - targetUserUrl: Discourse.computed.url('target_username', '/users/%@'), + targetUserUrl: url('target_username', '/users/%@'), usernameLower: function() { return this.get('username').toLowerCase(); }.property('username'), - userUrl: Discourse.computed.url('usernameLower', '/users/%@'), + userUrl: url('usernameLower', '/users/%@'), postUrl: function() { return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('post_number')); @@ -102,7 +100,7 @@ Discourse.UserAction = Discourse.Model.extend({ removableBookmark: Em.computed.and('bookmarkType', 'sameUser'), addChild: function(action) { - var groups = this.get("childGroups"); + let groups = this.get("childGroups"); if (!groups) { groups = { likes: Discourse.UserActionGroup.create({ icon: "fa fa-heart" }), @@ -113,7 +111,7 @@ Discourse.UserAction = Discourse.Model.extend({ } this.set("childGroups", groups); - var bucket = (function() { + const bucket = (function() { switch (action.action_type) { case UserActionTypes.likes_given: case UserActionTypes.likes_received: @@ -124,15 +122,15 @@ Discourse.UserAction = Discourse.Model.extend({ return "bookmarks"; } })(); - var current = groups[bucket]; + const current = groups[bucket]; if (current) { current.push(action); } }, children: function() { - var g = this.get("childGroups"); - var rval = []; + const g = this.get("childGroups"); + let rval = []; if (g) { rval = [g.likes, g.stars, g.edits, g.bookmarks].filter(function(i) { return i.get("items") && i.get("items").length > 0; @@ -154,20 +152,20 @@ Discourse.UserAction = Discourse.Model.extend({ } }); -Discourse.UserAction.reopenClass({ +UserAction.reopenClass({ collapseStream: function(stream) { - var uniq = {}, - collapsed = [], - pos = 0; + const uniq = {}; + const collapsed = []; + let pos = 0; stream.forEach(function(item) { - var key = "" + item.topic_id + "-" + item.post_number; - var found = uniq[key]; + const key = "" + item.topic_id + "-" + item.post_number; + const found = uniq[key]; if (found === void 0) { - var current; + let current; if (Discourse.UserAction.TO_COLLAPSE.indexOf(item.action_type) >= 0) { - current = Discourse.UserAction.create(item); + current = UserAction.create(item); item.switchToActing(); current.addChild(item); } else { @@ -177,7 +175,7 @@ Discourse.UserAction.reopenClass({ collapsed[pos] = current; pos += 1; } else { - if (Discourse.UserAction.TO_COLLAPSE.indexOf(item.action_type) >= 0) { + if (UserAction.TO_COLLAPSE.indexOf(item.action_type) >= 0) { item.switchToActing(); collapsed[found].addChild(item); } else { @@ -208,3 +206,5 @@ Discourse.UserAction.reopenClass({ ] }); + +export default UserAction; diff --git a/app/assets/javascripts/discourse/models/user_posts_stream.js b/app/assets/javascripts/discourse/models/user-posts-stream.js.es6 similarity index 67% rename from app/assets/javascripts/discourse/models/user_posts_stream.js rename to app/assets/javascripts/discourse/models/user-posts-stream.js.es6 index e00646cc12..8d7a750d8b 100644 --- a/app/assets/javascripts/discourse/models/user_posts_stream.js +++ b/app/assets/javascripts/discourse/models/user-posts-stream.js.es6 @@ -1,12 +1,7 @@ -/** - Represents a user's stream +import { url } from 'discourse/lib/computed'; +import AdminPost from 'discourse/models/admin-post'; - @class UserPostsStream - @extends Discourse.Model - @namespace Discourse - @module Discourse -**/ -Discourse.UserPostsStream = Discourse.Model.extend({ +export default Discourse.Model.extend({ loaded: false, _initialize: function () { @@ -17,9 +12,9 @@ Discourse.UserPostsStream = Discourse.Model.extend({ }); }.on("init"), - url: Discourse.computed.url("user.username_lower", "filter", "itemsLoaded", "/posts/%@/%@?offset=%@"), + url: url("user.username_lower", "filter", "itemsLoaded", "/posts/%@/%@?offset=%@"), - filterBy: function (filter) { + filterBy(filter) { if (this.get("loaded") && this.get("filter") === filter) { return Ember.RSVP.resolve(); } this.setProperties({ @@ -32,15 +27,15 @@ Discourse.UserPostsStream = Discourse.Model.extend({ return this.findItems(); }, - findItems: function () { - var self = this; + findItems() { + const self = this; if (this.get("loading") || !this.get("canLoadMore")) { return Ember.RSVP.reject(); } this.set("loading", true); return Discourse.ajax(this.get("url"), { cache: false }).then(function (result) { if (result) { - var posts = result.map(function (post) { return Discourse.AdminPost.create(post); }); + const posts = result.map(function (post) { return AdminPost.create(post); }); self.get("content").pushObjects(posts); self.setProperties({ loaded: true, diff --git a/app/assets/javascripts/discourse/models/user-stream.js.es6 b/app/assets/javascripts/discourse/models/user-stream.js.es6 index 1870d484d6..71d1bba89c 100644 --- a/app/assets/javascripts/discourse/models/user-stream.js.es6 +++ b/app/assets/javascripts/discourse/models/user-stream.js.es6 @@ -1,3 +1,4 @@ +import { url } from 'discourse/lib/computed'; import RestModel from 'discourse/models/rest'; export default RestModel.extend({ @@ -22,7 +23,7 @@ export default RestModel.extend({ return filter; }.property('filter'), - baseUrl: Discourse.computed.url('itemsLoaded', 'user.username_lower', '/user_actions.json?offset=%@&username=%@'), + baseUrl: url('itemsLoaded', 'user.username_lower', '/user_actions.json?offset=%@&username=%@'), filterBy(filter) { this.setProperties({ filter, itemsLoaded: 0, content: [], lastLoadedUrl: null }); diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 8e9c8e5350..279073957c 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -1,5 +1,10 @@ +import { url } from 'discourse/lib/computed'; import RestModel from 'discourse/models/rest'; import avatarTemplate from 'discourse/lib/avatar-template'; +import UserStream from 'discourse/models/user-stream'; +import UserPostsStream from 'discourse/models/user-posts-stream'; +import Singleton from 'discourse/mixins/singleton'; +import { longDate } from 'discourse/lib/formatter'; const User = RestModel.extend({ @@ -10,24 +15,12 @@ const User = RestModel.extend({ hasNotPosted: Em.computed.not("hasPosted"), canBeDeleted: Em.computed.and("can_be_deleted", "hasNotPosted"), - /** - The user's stream - - @property stream - @type {Discourse.UserStream} - **/ stream: function() { - return Discourse.UserStream.create({ user: this }); + return UserStream.create({ user: this }); }.property(), - /** - The user's posts stream - - @property postsStream - @type {Discourse.UserPostsStream} - **/ postsStream: function() { - return Discourse.UserPostsStream.create({ user: this }); + return UserPostsStream.create({ user: this }); }.property(), /** @@ -63,7 +56,7 @@ const User = RestModel.extend({ /** This user's profile background(in CSS). - @property websiteName + @property profileBackground @type {String} **/ profileBackground: function() { @@ -89,7 +82,7 @@ const User = RestModel.extend({ @property adminPath @type {String} **/ - adminPath: Discourse.computed.url('username_lower', "/admin/users/%@"), + adminPath: url('username_lower', "/admin/users/%@"), /** This user's username in lowercase. @@ -123,7 +116,7 @@ const User = RestModel.extend({ }.property('suspended_till'), suspendedTillDate: function() { - return Discourse.Formatter.longDate(this.get('suspended_till')); + return longDate(this.get('suspended_till')); }.property('suspended_till'), /** @@ -434,15 +427,15 @@ const User = RestModel.extend({ }); -User.reopenClass(Discourse.Singleton, { +User.reopenClass(Singleton, { // Find a `Discourse.User` for a given username. findByUsername: function(username, options) { - const user = Discourse.User.create({username: username}); + const user = User.create({username: username}); return user.findDetails(options); }, - // TODO: Use app.register and junk Discourse.Singleton + // TODO: Use app.register and junk Singleton createCurrent: function() { var userJson = PreloadStore.get('currentUser'); if (userJson) { diff --git a/app/assets/javascripts/discourse/models/user_action_stat.js b/app/assets/javascripts/discourse/models/user_action_stat.js deleted file mode 100644 index 942f22e960..0000000000 --- a/app/assets/javascripts/discourse/models/user_action_stat.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - A data model representing a statistic on a UserAction - - @class UserActionStat - @extends Discourse.Model - @namespace Discourse - @module Discourse -**/ -Discourse.UserActionStat = Discourse.Model.extend({ - - isPM: function() { - var actionType = this.get('action_type'); - return actionType === Discourse.UserAction.TYPES.messages_sent || - actionType === Discourse.UserAction.TYPES.messages_received; - }.property('action_type'), - - description: Discourse.computed.i18n('action_type', 'user_action_groups.%@'), - - isResponse: function() { - var actionType = this.get('action_type'); - return actionType === Discourse.UserAction.TYPES.replies || - actionType === Discourse.UserAction.TYPES.quotes; - }.property('action_type') - -}); 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 779822ee39..152aabadde 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -10,6 +10,7 @@ export default function() { this.route('fromParamsNear', { path: '/:nearPost' }); }); this.resource('topicBySlug', { path: '/t/:slug' }); + this.route('topicUnsubscribe', { path: '/t/:slug/:id/unsubscribe' }); this.resource('discovery', { path: '/' }, function() { // top diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6 index f0af569b02..a923dd1797 100644 --- a/app/assets/javascripts/discourse/routes/application.js.es6 +++ b/app/assets/javascripts/discourse/routes/application.js.es6 @@ -1,3 +1,4 @@ +import { setting } from 'discourse/lib/computed'; import showModal from 'discourse/lib/show-modal'; import OpenComposer from "discourse/mixins/open-composer"; @@ -13,7 +14,7 @@ function unlessReadOnly(method) { const ApplicationRoute = Discourse.Route.extend(OpenComposer, { - siteTitle: Discourse.computed.setting('title'), + siteTitle: setting('title'), actions: { _collectTitleTokens(tokens) { @@ -74,7 +75,7 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, { } exceptionController.setProperties({ lastTransition: transition, thrown: err }); - this.transitionTo('exception'); + this.intermediateTransitionTo('exception'); }, showLogin: unlessReadOnly('handleShowLogin'), 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 1f5dcd7621..69cd2e5c22 100644 --- a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 @@ -1,3 +1,4 @@ +import ScreenTrack from 'discourse/lib/screen-track'; import { queryParams } from 'discourse/controllers/discovery-sortable'; // A helper to build a topic route for a filter @@ -82,7 +83,7 @@ export default function(filter, extras) { model(data, transition) { // attempt to stop early cause we need this to be called before .sync - Discourse.ScreenTrack.current().stop(); + ScreenTrack.current().stop(); const findOpts = filterQueryParams(transition.queryParams), extras = { cached: this.isPoppedState(transition) }; diff --git a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 index bfb8468cd7..6de3d07c54 100644 --- a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 @@ -2,32 +2,32 @@ export default Discourse.Route.extend({ // Avoid default model hook - model: function(p) { return p; }, + model(params) { return params; }, - setupController: function(controller, params) { + setupController(controller, params) { params = params || {}; params.track_visit = true; - var topic = this.modelFor('topic'), - postStream = topic.get('postStream'); - var topicController = this.controllerFor('topic'), - topicProgressController = this.controllerFor('topic-progress'), - composerController = this.controllerFor('composer'); + const self = this, + topic = this.modelFor('topic'), + postStream = topic.get('postStream'), + topicController = this.controllerFor('topic'), + topicProgressController = this.controllerFor('topic-progress'), + composerController = this.controllerFor('composer'); // I sincerely hope no topic gets this many posts if (params.nearPost === "last") { params.nearPost = 999999999; } - var self = this; params.forceLoad = true; - postStream.refresh(params).then(function () { + postStream.refresh(params).then(function () { // TODO we are seeing errors where closest post is null and this is exploding // we need better handling and logging for this condition. // The post we requested might not exist. Let's find the closest post - var closestPost = postStream.closestPostForPostNumber(params.nearPost || 1), - closest = closestPost.get('post_number'), - progress = postStream.progressIndexOfPost(closestPost); + const closestPost = postStream.closestPostForPostNumber(params.nearPost || 1), + closest = closestPost.get('post_number'), + progress = postStream.progressIndexOfPost(closestPost); topicController.setProperties({ 'model.currentPost': closest, @@ -43,6 +43,7 @@ export default Discourse.Route.extend({ Ember.run.scheduleOnce('afterRender', function() { self.appEvents.trigger('post:highlight', closest); }); + Discourse.URL.jumpToPost(closest); if (topic.present('draft')) { diff --git a/app/assets/javascripts/discourse/routes/topic-unsubscribe.js.es6 b/app/assets/javascripts/discourse/routes/topic-unsubscribe.js.es6 new file mode 100644 index 0000000000..10e77c47b4 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/topic-unsubscribe.js.es6 @@ -0,0 +1,23 @@ +import PostStream from "discourse/models/post-stream"; + +export default Discourse.Route.extend({ + model(params) { + const topic = this.store.createRecord("topic", { id: params.id }); + return PostStream.loadTopicView(params.id).then(json => { + topic.updateFromJson(json); + return topic; + }); + }, + + afterModel(topic) { + // hide the notification reason text + topic.set("details.notificationReasonText", null); + }, + + actions: { + didTransition() { + this.controllerFor("application").set("showFooter", true); + return true; + } + } +}); diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 7d5f253341..6d730f058d 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -1,3 +1,5 @@ +import ScreenTrack from 'discourse/lib/screen-track'; + let isTransitioning = false, scheduledReplace = null, lastScrollPos = null; @@ -185,7 +187,7 @@ const TopicRoute = Discourse.Route.extend({ topicController.set('multiSelect', false); topicController.unsubscribe(); this.controllerFor('composer').set('topic', null); - Discourse.ScreenTrack.current().stop(); + ScreenTrack.current().stop(); const headerController = this.controllerFor('header'); if (headerController) { @@ -226,7 +228,7 @@ const TopicRoute = Discourse.Route.extend({ this.controllerFor('topic-progress').set('model', model); // We reset screen tracking every time a topic is entered - Discourse.ScreenTrack.current().start(model.get('id'), controller); + ScreenTrack.current().start(model.get('id'), controller); } }); diff --git a/app/assets/javascripts/discourse/templates/application.hbs b/app/assets/javascripts/discourse/templates/application.hbs index 7eb87f79e3..9d92d2063f 100644 --- a/app/assets/javascripts/discourse/templates/application.hbs +++ b/app/assets/javascripts/discourse/templates/application.hbs @@ -2,7 +2,9 @@
    - {{custom-html "top"}} + {{#if showTop}} + {{custom-html "top"}} + {{/if}} {{global-notice}}
    {{outlet}} diff --git a/app/assets/javascripts/discourse/templates/badges/show.hbs b/app/assets/javascripts/discourse/templates/badges/show.hbs index ed72176c52..42a13208c2 100644 --- a/app/assets/javascripts/discourse/templates/badges/show.hbs +++ b/app/assets/javascripts/discourse/templates/badges/show.hbs @@ -36,7 +36,7 @@ {{/link-to}} {{#if ub.post_number}} - {{ub.topic.title}} + {{{ub.topic.fancyTitle}}} {{/if}}
    {{/each}} diff --git a/app/assets/javascripts/discourse/templates/header.hbs b/app/assets/javascripts/discourse/templates/header.hbs index 3a8bd239ed..42b2323f6a 100644 --- a/app/assets/javascripts/discourse/templates/header.hbs +++ b/app/assets/javascripts/discourse/templates/header.hbs @@ -12,6 +12,9 @@ {{/unless}} {{#if view.renderDropdowns}} + + {{plugin-outlet "header-before-dropdowns"}} + {{render "search"}} {{render "notifications" notifications}} diff --git a/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs index 82f0e59d79..7a02b2a778 100644 --- a/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs @@ -38,7 +38,7 @@

    {{footerMessage}} - {{#if can_create_topic}}{{i18n 'topic.suggest_create_topic'}}{{/if}} + {{#if model.can_create_topic}}{{i18n 'topic.suggest_create_topic'}}{{/if}}

    {{else}} {{#if top}} diff --git a/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs b/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs index f52e48ba43..972acfa16a 100644 --- a/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs @@ -14,6 +14,7 @@ {{category-link content.category}} {{/unless}} + {{plugin-outlet "topic-list-tags"}}
    {{raw "list/posts-count-column" topic=content tagName="div"}} diff --git a/app/assets/javascripts/discourse/templates/modal/feature-topic.hbs b/app/assets/javascripts/discourse/templates/modal/feature-topic.hbs index 07b6430795..d6360b86de 100644 --- a/app/assets/javascripts/discourse/templates/modal/feature-topic.hbs +++ b/app/assets/javascripts/discourse/templates/modal/feature-topic.hbs @@ -35,7 +35,7 @@

    {{{pinMessage}}} {{fa-icon "clock-o"}} - {{input type="date" value=model.pinnedInCategoryUntil}} + {{date-picker value=model.pinnedInCategoryUntil}}

    {{d-button action="pin" icon="thumb-tack" label="topic.feature.pin" class="btn-primary" disabled=pinDisabled}} @@ -56,7 +56,7 @@

    {{i18n "topic.feature_topic.pin_globally"}} {{fa-icon "clock-o"}} - {{input type="date" value=model.pinnedGloballyUntil}} + {{date-picker value=model.pinnedGloballyUntil}}

    {{d-button action="pinGlobally" icon="thumb-tack" label="topic.feature.pin_globally" class="btn-primary" disabled=pinGloballyDisabled}} diff --git a/app/assets/javascripts/discourse/templates/modal/invite.hbs b/app/assets/javascripts/discourse/templates/modal/invite.hbs index 5984aa86ab..cba7a89b4e 100644 --- a/app/assets/javascripts/discourse/templates/modal/invite.hbs +++ b/app/assets/javascripts/discourse/templates/modal/invite.hbs @@ -1,11 +1,11 @@