diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000000..975bbe566c --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,22 @@ +languages: + Ruby: true + JavaScript: true + Python: false + PHP: false + +exclude_paths: + - "app/assets/javascripts/defer/*" + - "app/assets/javascripts/discourse/lib/Markdown.Editor.js" + - "app/assets/javascripts/ember-addons/*" + - "lib/autospec/*" + - "lib/es6_module_transpiler/*" + - "lib/highlight_js/*" + - "lib/import/*" + - "lib/javascripts/*" + - "lib/tasks/*" + - "lib/*.js" + - "public/*" + - "script/*" + - "spec/*" + - "test/*" + - "vendor/*" diff --git a/.jshintrc b/.jshintrc index dab59943b4..55cc59d193 100644 --- a/.jshintrc +++ b/.jshintrc @@ -35,6 +35,7 @@ "exists", "visible", "invisible", + "asyncRender", "selectDropdown", "asyncTestDiscourse", "fixture", diff --git a/Gemfile b/Gemfile index 3f7a2446ad..bab99a6024 100644 --- a/Gemfile +++ b/Gemfile @@ -51,14 +51,14 @@ gem 'message_bus' gem 'rails_multisite', path: 'vendor/gems/rails_multisite' gem 'redcarpet', require: false -gem 'eventmachine' gem 'fast_xs' gem 'fast_xor' # while we sort out https://github.com/sdsykes/fastimage/pull/46 gem 'fastimage_discourse', require: 'fastimage' -gem 'fog', '1.26.0', require: false +gem 'aws-sdk', require: false +gem 'excon', require: false gem 'unf', require: false gem 'email_reply_parser' @@ -96,7 +96,7 @@ gem 'sass' gem 'sidekiq' # for sidekiq web -gem 'sinatra', require: nil +gem 'sinatra', require: false gem 'therubyracer' gem 'thin', require: false @@ -124,7 +124,7 @@ group :test, :development do gem 'certified', require: false # later appears to break Fabricate(:topic, category: category) gem 'fabrication', '2.9.8', require: false - gem 'qunit-rails' + gem 'discourse-qunit-rails', require: 'qunit-rails' gem 'mocha', require: false gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false diff --git a/Gemfile.lock b/Gemfile.lock index fd65ba4879..d97b1e14dd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,7 +6,6 @@ PATH GEM remote: https://rubygems.org/ specs: - CFPropertyList (2.2.8) actionmailer (4.1.10) actionpack (= 4.1.10) actionview (= 4.1.10) @@ -41,6 +40,14 @@ GEM activerecord (>= 2.3.0) rake (~> 10.4.2, >= 10.4.2) arel (5.0.1.20140414130214) + aws-sdk (2.0.45) + aws-sdk-resources (= 2.0.45) + aws-sdk-core (2.0.45) + builder (~> 3.0) + jmespath (~> 1.0) + multi_json (~> 1.0) + aws-sdk-resources (2.0.45) + aws-sdk-core (= 2.0.45) babel-source (4.6.6) babel-transpiler (0.6.0) babel-source (>= 4.0, < 5) @@ -64,6 +71,8 @@ GEM daemons (1.2.2) debug_inspector (0.0.2) diff-lcs (1.2.5) + discourse-qunit-rails (0.0.8) + railties docile (1.1.5) dotenv (1.0.2) email_reply_parser (0.5.8) @@ -82,7 +91,7 @@ GEM ember-source (1.11.3.1) erubis (2.7.0) eventmachine (1.0.7) - excon (0.44.4) + excon (0.45.3) execjs (2.5.2) exifr (1.1.3) fabrication (2.9.8) @@ -99,79 +108,11 @@ GEM fast_xs (0.8.0) fastimage_discourse (1.6.6) ffi (1.9.6) - fission (0.5.0) - CFPropertyList (~> 2.2) flamegraph (0.1.0) fast_stack - fog (1.26.0) - fog-atmos - fog-brightbox (~> 0.4) - fog-core (~> 1.27, >= 1.27.1) - fog-ecloud - fog-json - fog-profitbricks - fog-radosgw (>= 0.0.2) - fog-sakuracloud (>= 0.0.4) - fog-softlayer - fog-storm_on_demand - fog-terremark - fog-vmfusion - fog-voxel - fog-xml (~> 0.1.1) - ipaddress (~> 0.5) - nokogiri (~> 1.5, >= 1.5.11) - fog-atmos (0.1.0) - fog-core - fog-xml - fog-brightbox (0.7.1) - fog-core (~> 1.22) - fog-json - inflecto (~> 0.0.2) - fog-core (1.27.2) - builder - excon (~> 0.38) - formatador (~> 0.2) - mime-types - net-scp (~> 1.1) - net-ssh (>= 2.1.3) - fog-ecloud (0.0.2) - fog-core - fog-xml - fog-json (1.0.0) - multi_json (~> 1.0) - fog-profitbricks (0.0.1) - fog-core - fog-xml - nokogiri - fog-radosgw (0.0.3) - fog-core (>= 1.21.0) - fog-json - fog-xml (>= 0.0.1) - fog-sakuracloud (0.1.1) - fog-core - fog-json - fog-softlayer (0.3.26) - fog-core - fog-json - fog-storm_on_demand (0.1.0) - fog-core - fog-json - fog-terremark (0.0.3) - fog-core - fog-xml - fog-vmfusion (0.0.1) - fission - fog-core - fog-voxel (0.0.2) - fog-core - fog-xml - fog-xml (0.1.1) - fog-core - nokogiri (~> 1.5, >= 1.5.11) foreman (0.77.0) dotenv (~> 1.0.2) thor (~> 0.19.1) - formatador (0.2.5) fspath (2.0.6) gctools (0.2.3) given_core (3.5.4) @@ -193,8 +134,8 @@ GEM progress (~> 3.0.0) image_size (1.1.5) in_threads (1.2.2) - inflecto (0.0.2) - ipaddress (0.8.0) + jmespath (1.0.2) + multi_json (~> 1.0) jquery-rails (3.1.2) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) @@ -229,9 +170,6 @@ GEM multi_xml (0.5.5) multipart-post (2.0.0) mustache (0.99.8) - net-scp (1.2.1) - net-ssh (>= 2.6.5) - net-ssh (2.9.2) netrc (0.10.3) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) @@ -270,7 +208,7 @@ GEM omniauth-twitter (1.0.1) multi_json (~> 1.3) omniauth-oauth (~> 1.0) - onebox (1.5.18) + onebox (1.5.19) moneta (~> 0.7) multi_json (~> 1.7) mustache (~> 0.99) @@ -291,8 +229,6 @@ GEM pry (>= 0.9.10) puma (2.11.1) rack (>= 1.1, < 2.0) - qunit-rails (0.0.7) - railties r2 (0.2.5) rack (1.5.3) rack-mini-profiler (0.9.3) @@ -459,15 +395,17 @@ DEPENDENCIES actionpack-action_caching active_model_serializers (~> 0.8.3) annotate + aws-sdk babel-transpiler barber better_errors binding_of_caller certified + discourse-qunit-rails email_reply_parser ember-rails ember-source (= 1.11.3.1) - eventmachine + excon fabrication (= 2.9.8) fakeweb (~> 1.3.0) fast_blank @@ -475,7 +413,6 @@ DEPENDENCIES fast_xs fastimage_discourse flamegraph - fog (= 1.26.0) foreman gctools handlebars-source (= 2.0.0) @@ -510,7 +447,6 @@ DEPENDENCIES pry-nav pry-rails puma - qunit-rails r2 (~> 0.2.5) rack-mini-profiler rack-protection diff --git a/app/assets/javascripts/admin/components/ace-editor.js.es6 b/app/assets/javascripts/admin/components/ace-editor.js.es6 index 5687959605..d1813bcf35 100644 --- a/app/assets/javascripts/admin/components/ace-editor.js.es6 +++ b/app/assets/javascripts/admin/components/ace-editor.js.es6 @@ -1,5 +1,4 @@ -/*global ace:true */ - +/* global ace:true */ import loadScript from 'discourse/lib/load-script'; export default Ember.Component.extend({ @@ -33,19 +32,21 @@ export default Ember.Component.extend({ const self = this; loadScript("/javascripts/ace/ace.js", { scriptTag: true }).then(function() { - const editor = ace.edit(self.$('.ace')[0]); + ace.require(['ace/ace'], function(loadedAce) { + const editor = loadedAce.edit(self.$('.ace')[0]); - editor.setTheme("ace/theme/chrome"); - editor.setShowPrintMargin(false); - editor.getSession().setMode("ace/mode/" + (self.get('mode'))); - editor.on('change', function() { - self._skipContentChangeEvent = true; - self.set('content', editor.getSession().getValue()); - self._skipContentChangeEvent = false; + editor.setTheme("ace/theme/chrome"); + editor.setShowPrintMargin(false); + editor.getSession().setMode("ace/mode/" + self.get('mode')); + editor.on('change', function() { + self._skipContentChangeEvent = true; + self.set('content', editor.getSession().getValue()); + self._skipContentChangeEvent = false; + }); + + self.$().data('editor', editor); + self._editor = editor; }); - - self.$().data('editor', editor); - self._editor = editor; }); }.on('didInsertElement') diff --git a/app/assets/javascripts/admin/components/admin-report-counts.js.es6 b/app/assets/javascripts/admin/components/admin-report-counts.js.es6 index 28e45b10ec..25bf94db21 100644 --- a/app/assets/javascripts/admin/components/admin-report-counts.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report-counts.js.es6 @@ -1,3 +1,3 @@ export default Ember.Component.extend({ - tagName: 'tbody' + tagName: 'tr' }); diff --git a/app/assets/javascripts/admin/components/admin-report-per-day-counts.js.es6 b/app/assets/javascripts/admin/components/admin-report-per-day-counts.js.es6 new file mode 100644 index 0000000000..25bf94db21 --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-report-per-day-counts.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Component.extend({ + tagName: 'tr' +}); diff --git a/app/assets/javascripts/admin/components/admin-report-trust-level-counts.js.es6 b/app/assets/javascripts/admin/components/admin-report-trust-level-counts.js.es6 new file mode 100644 index 0000000000..25bf94db21 --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-report-trust-level-counts.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Component.extend({ + tagName: 'tr' +}); diff --git a/app/assets/javascripts/admin/components/site-setting.js.es6 b/app/assets/javascripts/admin/components/site-setting.js.es6 index 4703950d71..f8e3cc6b2c 100644 --- a/app/assets/javascripts/admin/components/site-setting.js.es6 +++ b/app/assets/javascripts/admin/components/site-setting.js.es6 @@ -1,9 +1,10 @@ import BufferedContent from 'discourse/mixins/buffered-content'; +import ScrollTop from 'discourse/mixins/scroll-top'; import SiteSetting from 'admin/models/site-setting'; const CustomTypes = ['bool', 'enum', 'list', 'url_list']; -export default Ember.Component.extend(BufferedContent, Discourse.ScrollTop, { +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'), @@ -65,8 +66,8 @@ export default Ember.Component.extend(BufferedContent, Discourse.ScrollTop, { self.set('validationMessage', null); self.commitBuffer(); }).catch(function(e) { - if (e.responseJSON && e.responseJSON.errors) { - self.set('validationMessage', e.responseJSON.errors[0]); + if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) { + self.set('validationMessage', e.jqXHR.responseJSON.errors[0]); } else { self.set('validationMessage', I18n.t('generic_error')); } 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 f955e2217a..ac648462b2 100644 --- a/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 @@ -1,3 +1,4 @@ +import { popupAjaxError } from 'discourse/lib/ajax-error'; import BufferedContent from 'discourse/mixins/buffered-content'; export default Ember.ObjectController.extend(BufferedContent, { @@ -64,11 +65,9 @@ export default Ember.ObjectController.extend(BufferedContent, { self.set('savingStatus', I18n.t('saved')); } - }).catch(function(error) { - self.set('savingStatus', I18n.t('failed')); - self.send('saveError', error); - }).finally(function() { + }).catch(popupAjaxError).finally(function() { self.set('saving', false); + self.set('savingStatus', ''); }); } }, diff --git a/app/assets/javascripts/admin/controllers/admin-emojis.js.es6 b/app/assets/javascripts/admin/controllers/admin-emojis.js.es6 index 5921c4f21d..619d08c36f 100644 --- a/app/assets/javascripts/admin/controllers/admin-emojis.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-emojis.js.es6 @@ -2,19 +2,24 @@ export default Ember.ArrayController.extend({ sortProperties: ["name"], actions: { - emojiUploaded: function (emoji) { - this.pushObject(emoji); + emojiUploaded(emoji) { + this.pushObject(Em.Object.create(emoji)); }, - destroy: function(emoji) { - var self = this; - return bootbox.confirm(I18n.t("admin.emoji.delete_confirm", { name: emoji.name }), I18n.t("no_value"), I18n.t("yes_value"), function (destroy) { - if (destroy) { - return Discourse.ajax("/admin/customize/emojis/" + emoji.name, { type: "DELETE" }).then(function() { - self.removeObject(emoji); - }); + destroy(emoji) { + const self = this; + return bootbox.confirm( + I18n.t("admin.emoji.delete_confirm", { name: emoji.get("name") }), + I18n.t("no_value"), + I18n.t("yes_value"), + function(destroy) { + if (destroy) { + return Discourse.ajax("/admin/customize/emojis/" + emoji.get("name"), { type: "DELETE" }).then(function() { + self.removeObject(emoji); + }); + } } - }); + ); } } }); diff --git a/app/assets/javascripts/admin/controllers/admin-group.js.es6 b/app/assets/javascripts/admin/controllers/admin-group.js.es6 index d0332e8207..2256714084 100644 --- a/app/assets/javascripts/admin/controllers/admin-group.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-group.js.es6 @@ -1,3 +1,5 @@ +import { popupAjaxError } from 'discourse/lib/ajax-error'; + export default Em.ObjectController.extend({ needs: ['adminGroupsType'], disableSave: false, @@ -73,7 +75,7 @@ export default Em.ObjectController.extend({ let promise = group.get("id") ? group.save() : group.create().then(() => groupsController.addObject(group)); promise.then(() => this.transitionToRoute("adminGroup", group)) - .catch(e => bootbox.alert($.parseJSON(e.responseText).errors)) + .catch(popupAjaxError) .finally(() => this.set('disableSave', false)); }, diff --git a/app/assets/javascripts/admin/controllers/admin-user-fields.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-fields.js.es6 index 6ccf0810cf..0eb041c55b 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-fields.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-fields.js.es6 @@ -2,7 +2,7 @@ import UserField from 'admin/models/user-field'; export default Ember.ArrayController.extend({ fieldTypes: null, - createDisabled: Em.computed.gte('model.length', 3), + createDisabled: Em.computed.gte('model.length', 20), userFieldsDescription: function() { return I18n.t('admin.user_fields.description'); 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 88d775b1b8..b544aafc63 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 @@ -9,7 +9,7 @@ export default ObjectController.extend(CanCheckEmails, { showApproval: Discourse.computed.setting('must_approve_users'), showBadges: Discourse.computed.setting('enable_badges'), - primaryGroupDirty: Discourse.computed.propertyNotEqual('originalPrimaryGroupId', 'primary_group_id'), + primaryGroupDirty: Discourse.computed.propertyNotEqual('originalPrimaryGroupId', 'model.primary_group_id'), automaticGroups: function() { return this.get("model.automaticGroups").map((g) => g.name).join(", "); @@ -17,7 +17,7 @@ export default ObjectController.extend(CanCheckEmails, { userFields: function() { const siteUserFields = this.site.get('user_fields'), - userFields = this.get('user_fields'); + userFields = this.get('model.user_fields'); if (!Ember.isEmpty(siteUserFields)) { return siteUserFields.map(function(uf) { @@ -26,7 +26,7 @@ export default ObjectController.extend(CanCheckEmails, { }); } return []; - }.property('user_fields.@each'), + }.property('model.user_fields.@each'), actions: { toggleTitleEdit() { @@ -67,16 +67,16 @@ export default ObjectController.extend(CanCheckEmails, { return Discourse.ajax("/admin/users/" + this.get('id') + "/primary_group", { type: 'PUT', - data: {primary_group_id: this.get('primary_group_id')} + data: {primary_group_id: this.get('model.primary_group_id')} }).then(function () { - self.set('originalPrimaryGroupId', self.get('primary_group_id')); + self.set('originalPrimaryGroupId', self.get('model.primary_group_id')); }).catch(function() { bootbox.alert(I18n.t('generic_error')); }); }, resetPrimaryGroup() { - this.set('primary_group_id', this.get('originalPrimaryGroupId')); + this.set('model.primary_group_id', this.get('originalPrimaryGroupId')); }, regenerateApiKey() { diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index 674ea5e6a3..8416f53585 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -1,3 +1,5 @@ +import { popupAjaxError } from 'discourse/lib/ajax-error'; + const AdminUser = Discourse.User.extend({ customGroups: Em.computed.filter("groups", (g) => !g.automatic && Discourse.Group.create(g)), @@ -90,14 +92,7 @@ const AdminUser = Discourse.User.extend({ can_grant_admin: false, can_revoke_admin: true }); - }).catch(function(e) { - let error; - if (e.responseJSON && e.responseJSON.error) { - error = e.responseJSON.error; - } - error = error || I18n.t('admin.user.grant_admin_failed', { error: "http: " + e.status + " - " + e.body }); - bootbox.alert(error); - }); + }).catch(popupAjaxError); }, revokeModeration() { @@ -110,7 +105,7 @@ const AdminUser = Discourse.User.extend({ can_grant_moderation: true, can_revoke_moderation: false }); - }); + }).catch(popupAjaxError); }, grantModeration() { @@ -123,14 +118,7 @@ const AdminUser = Discourse.User.extend({ can_grant_moderation: false, can_revoke_moderation: true }); - }).catch(function(e) { - let error; - if (e.responseJSON && e.responseJSON.error) { - error = e.responseJSON.error; - } - error = error || I18n.t('admin.user.grant_moderation_failed', { error: "http: " + e.status + " - " + e.body }); - bootbox.alert(error); - }); + }).catch(popupAjaxError); }, refreshBrowsers() { @@ -156,10 +144,6 @@ const AdminUser = Discourse.User.extend({ this.set('originalTrustLevel', this.get('trust_level')); }, - trustLevels: function() { - return Discourse.Site.currentProp('trustLevels'); - }.property(), - dirty: Discourse.computed.propertyNotEqual('originalTrustLevel', 'trustLevel.id'), saveTrustLevel() { @@ -243,7 +227,7 @@ const AdminUser = Discourse.User.extend({ type: 'POST', data: { username_or_email: this.get('username') } }).then(function() { - document.location = "/"; + document.location = Discourse.getURL("/"); }).catch(function(e) { if (e.status === 404) { bootbox.alert(I18n.t('admin.impersonate.not_found')); @@ -321,9 +305,9 @@ const AdminUser = Discourse.User.extend({ }).then(function(data) { if (data.success) { if (data.username) { - document.location = "/admin/users/" + data.username; + document.location = Discourse.getURL("/admin/users/" + data.username); } else { - document.location = "/admin/users/list/active"; + document.location = Discourse.getURL("/admin/users/list/active"); } } else { bootbox.alert(I18n.t("admin.user.anonymize_failed")); @@ -386,7 +370,7 @@ const AdminUser = Discourse.User.extend({ if (/^\/admin\/users\/list\//.test(location)) { document.location = location; } else { - document.location = "/admin/users/list/active"; + document.location = Discourse.getURL("/admin/users/list/active"); } } else { bootbox.alert(I18n.t("admin.user.delete_failed")); diff --git a/app/assets/javascripts/admin/templates/components/admin-report-counts.hbs b/app/assets/javascripts/admin/templates/components/admin-report-counts.hbs index e91598e5da..d86e5ed5da 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report-counts.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report-counts.hbs @@ -1,13 +1,11 @@ - - - {{#if report.icon}} - {{fa-icon report.icon}} - {{/if}} - {{report.title}} - - {{report.todayCount}} - {{report.yesterdayCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}} - {{report.lastSevenDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}} - {{report.lastThirtyDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}} - {{report.total}} - + + {{#if report.icon}} + {{fa-icon report.icon}} + {{/if}} + {{report.title}} + +{{report.todayCount}} +{{report.yesterdayCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}} +{{report.lastSevenDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}} +{{report.lastThirtyDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}} +{{report.total}} diff --git a/app/assets/javascripts/admin/templates/components/admin-report-per-day-counts.hbs b/app/assets/javascripts/admin/templates/components/admin-report-per-day-counts.hbs new file mode 100644 index 0000000000..e16bd42f50 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/admin-report-per-day-counts.hbs @@ -0,0 +1,6 @@ +{{report.title}} +{{report.todayCount}} +{{report.yesterdayCount}} +{{report.sevenDaysAgoCount}} +{{report.thirtyDaysAgoCount}} + diff --git a/app/assets/javascripts/admin/templates/components/admin-report-trust-level-counts.hbs b/app/assets/javascripts/admin/templates/components/admin-report-trust-level-counts.hbs new file mode 100644 index 0000000000..93df84f2bc --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/admin-report-trust-level-counts.hbs @@ -0,0 +1,6 @@ +{{report.title}} +{{#link-to 'adminUsersList.show' 'newuser'}}{{value-at-tl report.data level="0"}}{{/link-to}} +{{#link-to 'adminUsersList.show' 'basic'}}{{value-at-tl report.data level="1"}}{{/link-to}} +{{#link-to 'adminUsersList.show' 'regular'}}{{value-at-tl report.data level="2"}}{{/link-to}} +{{#link-to 'adminUsersList.show' 'leader'}}{{value-at-tl report.data level="3"}}{{/link-to}} +{{#link-to 'adminUsersList.show' 'elder'}}{{value-at-tl report.data level="4"}}{{/link-to}} diff --git a/app/assets/javascripts/admin/templates/dashboard.hbs b/app/assets/javascripts/admin/templates/dashboard.hbs index f73a52b5cd..32ce15f9c2 100644 --- a/app/assets/javascripts/admin/templates/dashboard.hbs +++ b/app/assets/javascripts/admin/templates/dashboard.hbs @@ -15,9 +15,11 @@ 4 - {{#unless loading}} - {{ render 'admin/templates/reports/trust_levels_report' users_by_trust_level tagName="tbody"}} - {{/unless}} + + {{#unless loading}} + {{admin-report-trust-level-counts report=users_by_trust_level}} + {{/unless}} + @@ -50,16 +52,18 @@ {{i18n 'admin.dashboard.reports.all'}} - {{#unless loading}} - {{ render 'admin/templates/reports/per_day_counts_report' visits tagName="tbody"}} - {{admin-report-counts report=signups}} - {{admin-report-counts report=topics}} - {{admin-report-counts report=posts}} - {{admin-report-counts report=likes}} - {{admin-report-counts report=flags}} - {{admin-report-counts report=bookmarks}} - {{admin-report-counts report=emails}} - {{/unless}} + + {{#unless loading}} + {{admin-report-per-day-counts report=visits}} + {{admin-report-counts report=signups}} + {{admin-report-counts report=topics}} + {{admin-report-counts report=posts}} + {{admin-report-counts report=likes}} + {{admin-report-counts report=flags}} + {{admin-report-counts report=bookmarks}} + {{admin-report-counts report=emails}} + {{/unless}} + @@ -75,12 +79,14 @@ {{i18n 'admin.dashboard.reports.all'}} - {{#unless loading}} - {{admin-report-counts report=page_view_anon_reqs}} - {{admin-report-counts report=page_view_logged_in_reqs}} - {{admin-report-counts report=page_view_crawler_reqs}} - {{admin-report-counts report=page_view_total_reqs}} - {{/unless}} + + {{#unless loading}} + {{admin-report-counts report=page_view_anon_reqs}} + {{admin-report-counts report=page_view_logged_in_reqs}} + {{admin-report-counts report=page_view_crawler_reqs}} + {{admin-report-counts report=page_view_total_reqs}} + {{/unless}} + @@ -97,13 +103,15 @@ {{i18n 'admin.dashboard.reports.all'}} - {{#unless loading}} - {{admin-report-counts report=user_to_user_private_messages}} - {{admin-report-counts report=system_private_messages}} - {{admin-report-counts report=notify_moderators_private_messages}} - {{admin-report-counts report=notify_user_private_messages}} - {{admin-report-counts report=moderator_warning_private_messages}} - {{/unless}} + + {{#unless loading}} + {{admin-report-counts report=user_to_user_private_messages}} + {{admin-report-counts report=system_private_messages}} + {{admin-report-counts report=notify_moderators_private_messages}} + {{admin-report-counts report=notify_user_private_messages}} + {{admin-report-counts report=moderator_warning_private_messages}} + {{/unless}} + @@ -118,14 +126,14 @@ - {{#unless loading}} + {{#unless loading}} {{i18n 'admin.dashboard.uploads'}} {{disk_space.uploads_used}} ({{i18n 'admin.dashboard.space_free' size=disk_space.uploads_free}}) {{i18n 'admin.dashboard.backups'}} {{disk_space.backups_used}} ({{i18n 'admin.dashboard.space_free' size=disk_space.backups_free}}) - {{/unless}} + {{/unless}} @@ -144,20 +152,22 @@ {{i18n 'admin.dashboard.reports.all'}} - {{#unless loading}} - {{admin-report-counts report=http_2xx_reqs}} - {{admin-report-counts report=http_3xx_reqs}} - {{admin-report-counts report=http_4xx_reqs}} - {{admin-report-counts report=http_5xx_reqs}} - {{admin-report-counts report=http_background_reqs}} - {{admin-report-counts report=http_total_reqs}} - {{/unless}} + + {{#unless loading}} + {{admin-report-counts report=http_2xx_reqs}} + {{admin-report-counts report=http_3xx_reqs}} + {{admin-report-counts report=http_4xx_reqs}} + {{admin-report-counts report=http_5xx_reqs}} + {{admin-report-counts report=http_background_reqs}} + {{admin-report-counts report=http_total_reqs}} + {{/unless}} + {{else}} -
- {{i18n 'admin.dashboard.show_traffic_report'}} -
+
+ {{i18n 'admin.dashboard.show_traffic_report'}} +
{{/if}} {{/unless}} diff --git a/app/assets/javascripts/admin/templates/plugins-index.hbs b/app/assets/javascripts/admin/templates/plugins-index.hbs index 1686e34b9f..1bbdf5e7d9 100644 --- a/app/assets/javascripts/admin/templates/plugins-index.hbs +++ b/app/assets/javascripts/admin/templates/plugins-index.hbs @@ -10,13 +10,13 @@
+ + + + + + - - - - - - {{#each plugin in controller}} - - - - - - - diff --git a/app/assets/javascripts/admin/templates/reports/trust_levels_report.hbs b/app/assets/javascripts/admin/templates/reports/trust_levels_report.hbs deleted file mode 100644 index cedde2f3ad..0000000000 --- a/app/assets/javascripts/admin/templates/reports/trust_levels_report.hbs +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/app/assets/javascripts/admin/templates/user_index.hbs b/app/assets/javascripts/admin/templates/user_index.hbs index b522067b0c..9d2c510b31 100644 --- a/app/assets/javascripts/admin/templates/user_index.hbs +++ b/app/assets/javascripts/admin/templates/user_index.hbs @@ -1,12 +1,12 @@ -
+
- {{#if active}} + {{#if model.active}} {{#link-to 'user' model class="btn"}} {{fa-icon "user"}} {{i18n 'admin.user.show_public_profile'}} {{/link-to}} - {{#if can_impersonate}} + {{#if model.can_impersonate}} {{/if}} @@ -50,8 +50,8 @@
{{i18n 'user.associated_accounts'}}
- {{#if associated_accounts}} - {{associated_accounts}} + {{#if model.associated_accounts}} + {{model.associated_accounts}} {{else}} {{/if}} @@ -68,9 +68,9 @@
{{i18n 'user.title.title'}}
{{#if editingTitle}} - {{text-field value=title autofocus="autofocus"}} + {{text-field value=model.title autofocus="autofocus"}} {{else}} - {{title}}  + {{model.title}}  {{/if}}
@@ -85,23 +85,23 @@
{{i18n 'user.ip_address.title'}}
-
{{ip_address}}
+
{{model.ip_address}}
{{#if currentUser.staff}} - {{ip-lookup ip=ip_address userId=id}} + {{ip-lookup ip=model.ip_address userId=model.id}} {{/if}}
{{i18n 'user.registration_ip_address.title'}}
-
{{registration_ip_address}}
+
{{model.registration_ip_address}}
{{#if currentUser.staff}} - {{ip-lookup ip=registration_ip_address userId=id}} + {{ip-lookup ip=model.registration_ip_address userId=model.id}} {{/if}}
@@ -110,10 +110,10 @@
{{i18n 'admin.badges.title'}}
- {{i18n 'badges.badge_count' count=badge_count}} + {{i18n 'badges.badge_count' count=model.badge_count}}
- {{#link-to 'adminUser.badges' this class="btn"}}{{fa-icon "certificate"}}{{i18n 'admin.badges.edit_badges'}}{{/link-to}} + {{#link-to 'adminUser.badges' model class="btn"}}{{fa-icon "certificate"}}{{i18n 'admin.badges.edit_badges'}}{{/link-to}}
{{/if}} @@ -169,26 +169,26 @@
{{i18n 'admin.users.active'}}
- {{#if active}} + {{#if model.active}} {{i18n 'yes_value'}} {{else}} {{i18n 'no_value'}} {{/if}}
- {{#if active}} - {{#if can_deactivate}} + {{#if model.active}} + {{#if model.can_deactivate}} {{i18n 'admin.user.deactivate_explanation'}} {{/if}} {{else}} - {{#if can_send_activation_email}} + {{#if model.can_send_activation_email}} {{/if}} - {{#if can_activate}} + {{#if model.can_activate}} {{/if}} - {{#if can_grant_admin}} + {{#if model.can_grant_admin}} {{/if}} - {{#if can_grant_moderation}} + {{#if model.can_grant_moderation}} @@ -273,17 +273,17 @@ {{/if}} {{/if}} - {{#if tl3Requirements}} - {{#link-to 'adminUser.tl3Requirements' this class="btn"}}{{i18n 'admin.user.trust_level_3_requirements'}}{{/link-to}} + {{#if model.tl3Requirements}} + {{#link-to 'adminUser.tl3Requirements' model class="btn"}}{{i18n 'admin.user.trust_level_3_requirements'}}{{/link-to}} {{/if}}
-
+
{{i18n 'admin.user.suspended'}}
-
{{isSuspended}}
+
{{model.isSuspended}}
- {{#if isSuspended}} + {{#if model.isSuspended}}
- {{#if isSuspended}} + {{#if model.isSuspended}}
{{i18n 'admin.user.suspended_by'}}
@@ -316,11 +316,11 @@
{{/if}} -
+
{{i18n 'admin.user.blocked'}}
-
{{blocked}}
+
{{model.blocked}}
- {{#if blocked}} + {{#if model.blocked}} {{/if}} {{else}} - {{deleteAllPostsExplanation}} + {{model.deleteAllPostsExplanation}} {{/if}}
{{i18n 'admin.user.posts_read_count'}}
-
{{posts_read_count}}
+
{{model.posts_read_count}}
{{i18n 'admin.user.warnings_received_count'}}
-
{{warnings_received_count}}
+
{{model.warnings_received_count}}
{{i18n 'admin.user.flags_given_received_count'}}
-
{{flags_given_count}} / {{flags_received_count}}
+
{{model.flags_given_count}} / {{model.flags_received_count}}
{{i18n 'admin.user.private_topics_count'}}
-
{{private_topics_count}}
+
{{model.private_topics_count}}
{{i18n 'admin.user.time_read'}}
-
{{{time_read}}}
+
{{{model.time_read}}}
{{i18n 'user.invited.days_visited'}}
-
{{{days_visited}}}
+
{{{model.days_visited}}}
-{{#if single_sign_on_record}} +{{#if model.single_sign_on_record}}

{{i18n 'admin.user.sso.title'}}

- {{#with single_sign_on_record}} + {{#with model.single_sign_on_record}}
{{i18n 'admin.user.sso.external_id'}}
{{external_id}}
@@ -455,28 +455,28 @@

- {{#unless anonymizeForbidden}} + {{#unless model.anonymizeForbidden}} {{d-button label="admin.user.anonymize" icon="exclamation-triangle" class="btn-danger" - disabled=anonymizeForbidden + disabled=model.anonymizeForbidden action="anonymize"}} {{/unless}} - {{#unless deleteForbidden}} + {{#unless model.deleteForbidden}} {{d-button label="admin.user.delete" icon="exclamation-triangle" class="btn-danger" - disabled=deleteForbidden + disabled=model.deleteForbidden action="destroy"}} {{/unless}}
- {{#if deleteExplanation}} + {{#if model.deleteExplanation}}

- {{fa-icon "exclamation-triangle"}} {{deleteExplanation}} + {{fa-icon "exclamation-triangle"}} {{model.deleteExplanation}}
{{/if}}
diff --git a/app/assets/javascripts/admin/views/admin-badges-index.js.es6 b/app/assets/javascripts/admin/views/admin-badges-index.js.es6 index 2bd2497f15..f0b0ceb85d 100644 --- a/app/assets/javascripts/admin/views/admin-badges-index.js.es6 +++ b/app/assets/javascripts/admin/views/admin-badges-index.js.es6 @@ -1 +1,3 @@ -export default Ember.View.extend(Discourse.ScrollTop); +import ScrollTop from 'discourse/mixins/scroll-top'; + +export default Ember.View.extend(ScrollTop); diff --git a/app/assets/javascripts/admin/views/admin-badges-show.js.es6 b/app/assets/javascripts/admin/views/admin-badges-show.js.es6 index b49d0fad67..55f7e0b64d 100644 --- a/app/assets/javascripts/admin/views/admin-badges-show.js.es6 +++ b/app/assets/javascripts/admin/views/admin-badges-show.js.es6 @@ -1,4 +1,6 @@ -export default Ember.View.extend(Discourse.ScrollTop, { +import ScrollTop from 'discourse/mixins/scroll-top'; + +export default Ember.View.extend(ScrollTop, { _scrollOnModelChange: function() { this._scrollTop(); }.observes('controller.model.id') diff --git a/app/assets/javascripts/discourse/components/avatar-uploader.js.es6 b/app/assets/javascripts/discourse/components/avatar-uploader.js.es6 index f00f674280..69b2f94fe1 100644 --- a/app/assets/javascripts/discourse/components/avatar-uploader.js.es6 +++ b/app/assets/javascripts/discourse/components/avatar-uploader.js.es6 @@ -1,36 +1,21 @@ -import UploadMixin from 'discourse/mixins/upload'; +import UploadMixin from "discourse/mixins/upload"; export default Em.Component.extend(UploadMixin, { - type: 'avatar', - tagName: 'span', + type: "avatar", + tagName: "span", imageIsNotASquare: false, - uploadUrl: Discourse.computed.url('username', '/users/%@/preferences/user_image'), - uploadButtonText: function() { - return this.get("uploading") ? - I18n.t("uploading") : - I18n.t("user.change_avatar.upload_picture"); + return this.get("uploading") ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture"); }.property("uploading"), - uploadDone(data) { - // display a warning whenever the image is not a square - this.set("imageIsNotASquare", data.result.width !== data.result.height); - - // in order to be as much responsive as possible, we're cheating a bit here - // indeed, the server gives us back the url to the file we've just uploaded - // often, this file is not a square, so we need to crop it properly - // this will also capture the first frame of animated avatars when they're not allowed - Discourse.Utilities.cropAvatar(data.result.url, data.files[0].type).then(avatarTemplate => { - this.set("uploadedAvatarTemplate", avatarTemplate); - - // indicates the users is using an uploaded avatar (must happen after cropping, otherwise - // we will attempt to load an invalid avatar and cache a redirect to old one, uploadedAvatarTemplate - // trumps over custom avatar upload id) - this.set("custom_avatar_upload_id", data.result.upload_id); + uploadDone(upload) { + this.setProperties({ + imageIsNotASquare: upload.width !== upload.height, + uploadedAvatarTemplate: upload.url, + custom_avatar_upload_id: upload.id, }); - // the upload is now done this.sendAction("done"); } }); diff --git a/app/assets/javascripts/discourse/components/color-picker.js.es6 b/app/assets/javascripts/discourse/components/color-picker.js.es6 index 519bf2aa96..d1acf7d811 100644 --- a/app/assets/javascripts/discourse/components/color-picker.js.es6 +++ b/app/assets/javascripts/discourse/components/color-picker.js.es6 @@ -17,7 +17,7 @@ export default DiscourseContainerView.extend({ tagName: 'button', attributeBindings: ['style', 'title'], classNames: ['colorpicker'].concat( isUsed ? ['used-color'] : ['unused-color'] ), - style: 'background-color: #' + color + ';', + style: ('background-color: #' + color + ';').htmlSafe(), title: isUsed ? I18n.t("category.already_used") : null, click: function() { self.set("value", color); diff --git a/app/assets/javascripts/discourse/components/emoji-uploader.js.es6 b/app/assets/javascripts/discourse/components/emoji-uploader.js.es6 index 59f65b3d55..b43bea0410 100644 --- a/app/assets/javascripts/discourse/components/emoji-uploader.js.es6 +++ b/app/assets/javascripts/discourse/components/emoji-uploader.js.es6 @@ -1,4 +1,4 @@ -import UploadMixin from 'discourse/mixins/upload'; +import UploadMixin from "discourse/mixins/upload"; export default Em.Component.extend(UploadMixin, { type: "emoji", @@ -11,9 +11,9 @@ export default Em.Component.extend(UploadMixin, { return Ember.isBlank(this.get("name")) ? {} : { name: this.get("name") }; }.property("name"), - uploadDone: function (data) { + uploadDone(upload) { this.set("name", null); - this.sendAction("done", data.result); + this.sendAction("done", upload); } }); diff --git a/app/assets/javascripts/discourse/components/image-uploader.js.es6 b/app/assets/javascripts/discourse/components/image-uploader.js.es6 index 4173886ebf..4ca6bfdab2 100644 --- a/app/assets/javascripts/discourse/components/image-uploader.js.es6 +++ b/app/assets/javascripts/discourse/components/image-uploader.js.es6 @@ -1,31 +1,21 @@ -import UploadMixin from 'discourse/mixins/upload'; +import UploadMixin from "discourse/mixins/upload"; export default Em.Component.extend(UploadMixin, { + classNames: ["image-uploader"], backgroundStyle: function() { - var imageUrl = this.get('imageUrl'); + const imageUrl = this.get("imageUrl"); if (Em.isNone(imageUrl)) { return; } + return ("background-image: url(" + imageUrl + ")").htmlSafe(); + }.property("imageUrl"), - return "background-image: url(" + imageUrl + ")"; - }.property('imageUrl'), - - uploadDone: function(data) { - this.set('imageUrl', data.result.url); + uploadDone(upload) { + this.set("imageUrl", upload.url); }, actions: { - trash: function() { - this.set('imageUrl', null); - - // Do we want to signal the delete to the server right away? - if (this.get('instantDelete')) { - Discourse.ajax(this.get('uploadUrl'), { - type: 'DELETE', - data: { image_type: this.get('type') } - }).then(null, function() { - bootbox.alert(I18n.t('generic_error')); - }); - } + trash() { + this.set("imageUrl", null); } } }); diff --git a/app/assets/javascripts/discourse/components/post-gutter.js.es6 b/app/assets/javascripts/discourse/components/post-gutter.js.es6 index d40fcbad44..dbc8c6c052 100644 --- a/app/assets/javascripts/discourse/components/post-gutter.js.es6 +++ b/app/assets/javascripts/discourse/components/post-gutter.js.es6 @@ -62,7 +62,7 @@ export default Em.Component.extend(StringBuffer, { buffer.push(''); } - if ((links.length <= MAX_SHOWN || !collapsed) && this.get('canReplyAsNewTopic')) { + if (this.get('canReplyAsNewTopic')) { buffer.push("" + iconHTML('plus') + I18n.t('post.reply_as_new_topic') + ""); } }, diff --git a/app/assets/javascripts/discourse/components/text-overflow.js.es6 b/app/assets/javascripts/discourse/components/text-overflow.js.es6 index 99b24c525a..73a4637bfd 100644 --- a/app/assets/javascripts/discourse/components/text-overflow.js.es6 +++ b/app/assets/javascripts/discourse/components/text-overflow.js.es6 @@ -1,10 +1,12 @@ export default Ember.Component.extend({ _parse: function() { - this.$().find('hr').remove(); - this.$().ellipsis(); + Ember.run.next(null, () => { + this.$().find('hr').remove(); + this.$().ellipsis(); + }); }.on('didInsertElement'), - render: function(buffer) { + render(buffer) { buffer.push(this.get('text')); } }); diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index f3da08e9b5..357828a718 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -191,8 +191,9 @@ export default Ember.ObjectController.extend(Presence, { // for now handle a very narrow use case // if we are replying to a topic AND not on the topic pop the window up if (!force && composer.get('replyingToTopic')) { - const topic = this.get('model.topic'); - if (!topic || topic.get('id') !== composer.get('topic.id')) + + const currentTopic = this.get('controllers.topic.model'); + if (!currentTopic || currentTopic.get('id') !== composer.get('topic.id')) { const message = I18n.t("composer.posting_not_on_topic"); @@ -202,12 +203,12 @@ export default Ember.ObjectController.extend(Presence, { "link": true }]; - if (topic) { + if (currentTopic) { buttons.push({ - "label": I18n.t("composer.reply_here") + "
" + Handlebars.Utils.escapeExpression(topic.get('title')) + "
", + "label": I18n.t("composer.reply_here") + "
" + Handlebars.Utils.escapeExpression(currentTopic.get('title')) + "
", "class": "btn btn-reply-here", "callback": function() { - composer.set('topic', topic); + composer.set('topic', currentTopic); composer.set('post', null); self.save(true); } diff --git a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 b/app/assets/javascripts/discourse/controllers/edit-category.js.es6 index 09f6d06555..52e0e45bc2 100644 --- a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-category.js.es6 @@ -5,7 +5,10 @@ import { categoryBadgeHTML } from 'discourse/helpers/category-link'; // Modal for editing / creating a category export default ObjectController.extend(ModalFunctionality, { foregroundColors: ['FFFFFF', '000000'], - categoryUploadUrl: '/category/uploads', + editingPermissions: false, + selectedTab: null, + saving: false, + deleting: false, parentCategories: function() { return Discourse.Category.list().filter(function (c) { @@ -15,31 +18,31 @@ export default ObjectController.extend(ModalFunctionality, { // We can change the parent if there are no children subCategories: function() { - if (Em.isEmpty(this.get('id'))) { return null; } - return Discourse.Category.list().filterBy('parent_category_id', this.get('id')); + if (Em.isEmpty(this.get('model.id'))) { return null; } + return Discourse.Category.list().filterBy('parent_category_id', this.get('model.id')); }.property('model.id'), - canSelectParentCategory: Em.computed.not('isUncategorizedCategory'), + canSelectParentCategory: Em.computed.not('model.isUncategorizedCategory'), - onShow: function() { + onShow() { this.changeSize(); this.titleChanged(); }, changeSize: function() { - if (this.present('description')) { + if (this.present('model.description')) { this.set('controllers.modal.modalClass', 'edit-category-modal full'); } else { this.set('controllers.modal.modalClass', 'edit-category-modal small'); } - }.observes('description'), + }.observes('model.description'), title: function() { - if (this.get('id')) { + if (this.get('model.id')) { return I18n.t("category.edit_long") + " : " + this.get('model.name'); } return I18n.t("category.create") + (this.get('model.name') ? (" : " + this.get('model.name')) : ''); - }.property('id', 'model.name'), + }.property('model.id', 'model.name'), titleChanged: function() { this.set('controllers.modal.title', this.get('title')); @@ -47,10 +50,10 @@ export default ObjectController.extend(ModalFunctionality, { disabled: function() { if (this.get('saving') || this.get('deleting')) return true; - if (!this.get('name')) return true; - if (!this.get('color')) return true; + if (!this.get('model.name')) return true; + if (!this.get('model.color')) return true; return false; - }.property('saving', 'name', 'color', 'deleting'), + }.property('saving', 'model.name', 'model.color', 'deleting'), emailInEnabled: Discourse.computed.setting('email_in'), @@ -59,80 +62,82 @@ export default ObjectController.extend(ModalFunctionality, { }.property('disabled', 'saving', 'deleting'), colorStyle: function() { - return "background-color: #" + (this.get('color')) + "; color: #" + (this.get('text_color')) + ";"; - }.property('color', 'text_color'), + return "background-color: #" + this.get('model.color') + "; color: #" + this.get('model.text_color') + ";"; + }.property('model.color', 'model.text_color'), categoryBadgePreview: function() { - var c = Discourse.Category.create({ - name: this.get('categoryName'), - color: this.get('color'), - text_color: this.get('text_color'), - parent_category_id: parseInt(this.get('parent_category_id'),10), - read_restricted: this.get('model.read_restricted') + const model = this.get('model'); + const c = Discourse.Category.create({ + name: model.get('categoryName'), + color: model.get('color'), + text_color: model.get('text_color'), + parent_category_id: parseInt(model.get('parent_category_id'),10), + read_restricted: model.get('read_restricted') }); return categoryBadgeHTML(c, {link: false}); - }.property('parent_category_id', 'categoryName', 'color', 'text_color'), + }.property('model.parent_category_id', 'model.categoryName', 'model.color', 'model.text_color'), // background colors are available as a pipe-separated string backgroundColors: function() { - var categories = Discourse.Category.list(); + const categories = Discourse.Category.list(); return Discourse.SiteSettings.category_colors.split("|").map(function(i) { return i.toUpperCase(); }).concat( categories.map(function(c) { return c.color.toUpperCase(); }) ).uniq(); }.property('Discourse.SiteSettings.category_colors'), usedBackgroundColors: function() { - var categories = Discourse.Category.list(); + const categories = Discourse.Category.list(); - var currentCat = this.get('model'); + const currentCat = this.get('model'); return categories.map(function(c) { // If editing a category, don't include its color: return (currentCat.get('id') && currentCat.get('color').toUpperCase() === c.color.toUpperCase()) ? null : c.color.toUpperCase(); }, this).compact(); - }.property('id', 'color'), + }.property('model.id', 'model.color'), categoryName: function() { - var name = this.get('name') || ""; + const name = this.get('name') || ""; return name.trim().length > 0 ? name : I18n.t("preview"); }.property('name'), buttonTitle: function() { if (this.get('saving')) return I18n.t("saving"); - if (this.get('isUncategorizedCategory')) return I18n.t("save"); - return (this.get('id') ? I18n.t("category.save") : I18n.t("category.create")); - }.property('saving', 'id'), + if (this.get('model.isUncategorizedCategory')) return I18n.t("save"); + return (this.get('model.id') ? I18n.t("category.save") : I18n.t("category.create")); + }.property('saving', 'model.id'), deleteButtonTitle: function() { return I18n.t('category.delete'); }.property(), showDescription: function() { - return !this.get('isUncategorizedCategory') && this.get('id'); - }.property('isUncategorizedCategory', 'id'), + return !this.get('model.isUncategorizedCategory') && this.get('model.id'); + }.property('model.isUncategorizedCategory', 'model.id'), showPositionInput: Discourse.computed.setting('fixed_category_positions'), actions: { - showCategoryTopic: function() { + showCategoryTopic() { this.send('closeModal'); - Discourse.URL.routeTo(this.get('topic_url')); + Discourse.URL.routeTo(this.get('model.topic_url')); return false; }, - editPermissions: function(){ + editPermissions() { this.set('editingPermissions', true); }, - addPermission: function(group, permission_id){ - this.get('model').addPermission({group_name: group + "", permission: Discourse.PermissionType.create({id: permission_id})}); + addPermission(group, id) { + this.get('model').addPermission({group_name: group + "", + permission: Discourse.PermissionType.create({id})}); }, - removePermission: function(permission){ + removePermission(permission) { this.get('model').removePermission(permission); }, - saveCategory: function() { - var self = this, + saveCategory() { + const self = this, model = this.get('model'), parentCategory = Discourse.Category.list().findBy('id', parseInt(model.get('parent_category_id'), 10)); @@ -155,8 +160,8 @@ export default ObjectController.extend(ModalFunctionality, { }); }, - deleteCategory: function() { - var self = this; + deleteCategory() { + const self = this; this.set('deleting', true); this.send('hideModal'); diff --git a/app/assets/javascripts/discourse/controllers/group.js.es6 b/app/assets/javascripts/discourse/controllers/group.js.es6 index 8355eff56d..b7c7c18a63 100644 --- a/app/assets/javascripts/discourse/controllers/group.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group.js.es6 @@ -3,6 +3,7 @@ import ObjectController from 'discourse/controllers/object'; // The basic controller for a group export default ObjectController.extend({ counts: null, + showing: null, // It would be nice if bootstrap marked action lists as selected when their links // were 'active' not the `li` tags. diff --git a/app/assets/javascripts/discourse/controllers/group/members.js.es6 b/app/assets/javascripts/discourse/controllers/group/members.js.es6 index bff5ee7a28..881a614ca3 100644 --- a/app/assets/javascripts/discourse/controllers/group/members.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group/members.js.es6 @@ -5,15 +5,13 @@ export default Ember.ObjectController.extend({ loadMore() { if (this.get("loading")) { return; } // we've reached the end - if (this.get("members.length") >= this.get("user_count")) { return; } + if (this.get("model.members.length") >= this.get("user_count")) { return; } this.set("loading", true); - const self = this; - - Discourse.Group.loadMembers(this.get("name"), this.get("members.length"), this.get("limit")).then(function (result) { - self.get("members").addObjects(result.members.map(member => Discourse.User.create(member))); - self.setProperties({ + Discourse.Group.loadMembers(this.get("name"), this.get("model.members.length"), this.get("limit")).then(result => { + this.get("model.members").addObjects(result.members.map(member => Discourse.User.create(member))); + this.setProperties({ loading: false, user_count: result.meta.total, limit: result.meta.limit, diff --git a/app/assets/javascripts/discourse/controllers/group/post.js.es6 b/app/assets/javascripts/discourse/controllers/group/post.js.es6 deleted file mode 100644 index fedecb6f5c..0000000000 --- a/app/assets/javascripts/discourse/controllers/group/post.js.es6 +++ /dev/null @@ -1,21 +0,0 @@ -export default Em.ObjectController.extend({ - - byName: function() { - var result = "", - longName = this.get('user_long_name'), - title = this.get('user_title'); - - if (!Em.isEmpty(longName)) { - result += longName; - } - if (!Em.isEmpty(title)) { - if (result.length > 0) { - result += ", "; - } - result += title; - } - return result; - }.property() - -}); - diff --git a/app/assets/javascripts/discourse/controllers/invite.js.es6 b/app/assets/javascripts/discourse/controllers/invite.js.es6 index ac1cc0628c..1f7179111e 100644 --- a/app/assets/javascripts/discourse/controllers/invite.js.es6 +++ b/app/assets/javascripts/discourse/controllers/invite.js.es6 @@ -16,12 +16,13 @@ export default ObjectController.extend(Presence, ModalFunctionality, { disabled: function() { if (this.get('saving')) return true; if (this.blank('emailOrUsername')) return true; + const emailOrUsername = this.get('emailOrUsername').trim(); // when inviting to forum, email must be valid - if (!this.get('invitingToTopic') && !Discourse.Utilities.emailValid(this.get('emailOrUsername'))) return true; + if (!this.get('invitingToTopic') && !Discourse.Utilities.emailValid(emailOrUsername)) return true; // normal users (not admin) can't invite users to private topic via email - if (!this.get('isAdmin') && this.get('isPrivateTopic') && Discourse.Utilities.emailValid(this.get('emailOrUsername'))) return true; + 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(this.get('emailOrUsername'))) return true; + if (this.get('isPrivateTopic') && this.blank('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'), @@ -135,7 +136,7 @@ export default ObjectController.extend(Presence, ModalFunctionality, { this.setProperties({ saving: true, error: false }); - return this.get('model').createInvite(this.get('emailOrUsername'), groupNames).then(result => { + return this.get('model').createInvite(this.get('emailOrUsername').trim(), groupNames).then(result => { this.setProperties({ saving: false, finished: true }); if (!this.get('invitingToTopic')) { Discourse.Invite.findInvitedBy(Discourse.User.current()).then(invite_model => { diff --git a/app/assets/javascripts/discourse/controllers/preferences.js.es6 b/app/assets/javascripts/discourse/controllers/preferences.js.es6 index 7b5810ff30..0cc11e05e9 100644 --- a/app/assets/javascripts/discourse/controllers/preferences.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences.js.es6 @@ -1,5 +1,6 @@ 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, { @@ -10,8 +11,10 @@ export default ObjectController.extend(CanCheckEmails, { editHistoryVisible: Discourse.computed.setting('edit_history_visible_to_public'), selectedCategories: function(){ - return [].concat(this.get("watchedCategories"), this.get("trackedCategories"), this.get("mutedCategories")); - }.property("watchedCategories", "trackedCategories", "mutedCategories"), + return [].concat(this.get("model.watchedCategories"), + this.get("model.trackedCategories"), + this.get("model.mutedCategories")); + }.property("model.watchedCategories", "model.trackedCategories", "model.mutedCategories"), // By default we haven't saved anything saved: false, @@ -21,7 +24,7 @@ export default ObjectController.extend(CanCheckEmails, { userFields: function() { let siteUserFields = this.site.get('user_fields'); if (!Ember.isEmpty(siteUserFields)) { - const userFields = this.get('user_fields'); + const userFields = this.get('model.user_fields'); // Staff can edit fields that are not `editable` if (!this.get('currentUser.staff')) { @@ -32,7 +35,7 @@ export default ObjectController.extend(CanCheckEmails, { return Ember.Object.create({ value, field }); }); } - }.property('user_fields.@each.value'), + }.property('model.user_fields.@each.value'), cannotDeleteAccount: Em.computed.not('can_delete_account'), deleteDisabled: Em.computed.or('saving', 'deleting', 'cannotDeleteAccount'), @@ -84,19 +87,19 @@ export default ObjectController.extend(CanCheckEmails, { { name: I18n.t('user.new_topic_duration.last_here'), value: -2 }], saveButtonText: function() { - return this.get('saving') ? I18n.t('saving') : I18n.t('save'); - }.property('saving'), + return this.get('model.isSaving') ? I18n.t('saving') : I18n.t('save'); + }.property('model.isSaving'), - imageUploadUrl: Discourse.computed.url('username', '/users/%@/preferences/user_image'), + passwordProgress: null, actions: { save() { const self = this; - this.setProperties({ saving: true, saved: false }); + this.set('saved', false); - const model = this.get('model'), - userFields = this.get('userFields'); + const model = this.get('model'); + const userFields = this.get('userFields'); // Update the user fields if (!Ember.isEmpty(userFields)) { @@ -111,22 +114,12 @@ export default ObjectController.extend(CanCheckEmails, { // Cook the bio for preview model.set('name', this.get('newNameInput')); return model.save().then(function() { - // model was saved - self.set('saving', false); if (Discourse.User.currentProp('id') === model.get('id')) { Discourse.User.currentProp('name', model.get('name')); } - self.set('bio_cooked', Discourse.Markdown.cook(Discourse.Markdown.sanitize(self.get('bio_raw')))); + model.set('bio_cooked', Discourse.Markdown.cook(Discourse.Markdown.sanitize(model.get('bio_raw')))); self.set('saved', true); - }, function(error) { - // model failed to save - self.set('saving', false); - if (error && error.responseText) { - alert($.parseJSON(error.responseText).errors[0]); - } else { - alert(I18n.t('generic_error')); - } - }); + }).catch(popupAjaxError); }, changePassword() { diff --git a/app/assets/javascripts/discourse/controllers/topic-progress.js.es6 b/app/assets/javascripts/discourse/controllers/topic-progress.js.es6 index ee844f4c6d..7763157fe6 100644 --- a/app/assets/javascripts/discourse/controllers/topic-progress.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic-progress.js.es6 @@ -2,6 +2,7 @@ export default Ember.ObjectController.extend({ needs: ['topic'], progressPosition: null, expanded: false, + toPostIndex: null, actions: { toggleExpansion: function(opts) { @@ -50,11 +51,11 @@ export default Ember.ObjectController.extend({ }, jumpTop: function() { - this.jumpTo(this.get('firstPostUrl')); + this.jumpTo(this.get('model.firstPostUrl')); }, jumpBottom: function() { - this.jumpTo(this.get('lastPostUrl')); + this.jumpTo(this.get('model.lastPostUrl')); } }, @@ -83,8 +84,8 @@ export default Ember.ObjectController.extend({ jumpBottomDisabled: function() { return this.get('progressPosition') >= this.get('model.postStream.filteredPostsCount') || - this.get('progressPosition') >= this.get('highest_post_number'); - }.property('model.postStream.filteredPostsCount', 'highest_post_number', 'progressPosition'), + this.get('progressPosition') >= this.get('model.highest_post_number'); + }.property('model.postStream.filteredPostsCount', 'model.highest_post_number', 'progressPosition'), hideProgress: function() { if (!this.get('model.postStream.loaded')) return true; @@ -95,14 +96,14 @@ export default Ember.ObjectController.extend({ hugeNumberOfPosts: function() { return (this.get('model.postStream.filteredPostsCount') >= Discourse.SiteSettings.short_progress_text_threshold); - }.property('highest_post_number'), + }.property('model.highest_post_number'), jumpToBottomTitle: function() { if (this.get('hugeNumberOfPosts')) { - return I18n.t('topic.progress.jump_bottom_with_number', {post_number: this.get('highest_post_number')}); + return I18n.t('topic.progress.jump_bottom_with_number', {post_number: this.get('model.highest_post_number')}); } else { return I18n.t('topic.progress.jump_bottom'); } - }.property('hugeNumberOfPosts', 'highest_post_number') + }.property('hugeNumberOfPosts', 'model.highest_post_number') }); diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 6256857f60..a7484cba8e 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -16,6 +16,7 @@ export default ObjectController.extend(SelectedPostsCount, BufferedContent, { loadedAllPosts: false, enteredAt: null, firstPostExpanded: false, + retrying: false, maxTitleLength: Discourse.computed.setting('max_topic_title_length'), diff --git a/app/assets/javascripts/discourse/controllers/user-card.js.es6 b/app/assets/javascripts/discourse/controllers/user-card.js.es6 index 2786d25f77..b620ba3ad0 100644 --- a/app/assets/javascripts/discourse/controllers/user-card.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-card.js.es6 @@ -35,57 +35,60 @@ export default Ember.Controller.extend({ show(username, postId, target) { // XSS protection (should be encapsulated) username = username.toString().replace(/[^A-Za-z0-9_]/g, ""); - const url = "/users/" + username; // Don't show on mobile if (Discourse.Mobile.mobileView) { + const url = "/users/" + username; Discourse.URL.routeTo(url); return; } const currentUsername = this.get('username'), - wasVisible = this.get('visible'), - post = this.get('viewingTopic') && postId ? this.get('postStream').findLoadedPost(postId) : null; - - this.setProperties({ avatar: null, post: post, username: username }); - - // If we click the avatar again, close it (unless its diff element on the screen). - if (target === this.get('cardTarget') && wasVisible) { - this.setProperties({ visible: false, username: null, cardTarget: null }); - return; - } + wasVisible = this.get('visible'), + previousTarget = this.get('cardTarget'), + post = this.get('viewingTopic') && postId ? this.get('postStream').findLoadedPost(postId) : null; if (username === currentUsername && this.get('userLoading') === username) { // debounce return; } - this.set('topicPostCount', null); + if (wasVisible) { + this.close(); + if (target === previousTarget) { + return; // Same target, close it without loading the new user card + } + } - this.setProperties({ user: null, userLoading: username, cardTarget: target }); + this.setProperties({ username, userLoading: username, cardTarget: target, post }); const args = { stats: false }; - args.include_post_count_for = this.get('controllers.topic.id'); - - const self = this; - return Discourse.User.findByUsername(username, args).then(function(user) { + args.include_post_count_for = this.get('controllers.topic.model.id'); + return Discourse.User.findByUsername(username, args).then((user) => { if (user.topic_post_count) { - self.set('topicPostCount', user.topic_post_count[args.include_post_count_for]); + this.set('topicPostCount', user.topic_post_count[args.include_post_count_for]); } - user = Discourse.User.create(user); - self.setProperties({ user, avatar: user, visible: true}); - self.appEvents.trigger('usercard:shown'); - }).catch(function(error) { - self.close(); + this.setProperties({ user, avatar: user, visible: true }); + }).catch((error) => { + this.close(); throw error; - }).finally(function() { - self.set('userLoading', null); + }).finally(() => { + this.set('userLoading', null); }); }, close() { - this.setProperties({ visible: false, cardTarget: null }); + this.setProperties({ + visible: false, + user: null, + username: null, + avatar: null, + userLoading: null, + cardTarget: null, + post: null, + topicPostCount: null + }); }, actions: { diff --git a/app/assets/javascripts/discourse/helpers/link-domain.js.es6 b/app/assets/javascripts/discourse/helpers/link-domain.js.es6 index ba59b7f50d..cad0d95f77 100644 --- a/app/assets/javascripts/discourse/helpers/link-domain.js.es6 +++ b/app/assets/javascripts/discourse/helpers/link-domain.js.es6 @@ -2,12 +2,12 @@ import registerUnbound from 'discourse/helpers/register-unbound'; registerUnbound('link-domain', function(link) { if (link) { - var internal = Em.get(link, 'internal'), - hasTitle = (!Em.isEmpty(Em.get(link, 'title'))); - if (hasTitle && !internal) { - var domain = Em.get(link, 'domain'); - if (!Em.isEmpty(domain)) { - var s = domain.split('.'); + const hasTitle = (!Ember.isEmpty(Em.get(link, 'title'))); + + if (hasTitle) { + let domain = Ember.get(link, 'domain'); + if (!Ember.isEmpty(domain)) { + const s = domain.split('.'); domain = s[s.length-2] + "." + s[s.length-1]; return new Handlebars.SafeString("" + domain + ""); } diff --git a/app/assets/javascripts/discourse/initializers/inject-objects.js.es6 b/app/assets/javascripts/discourse/initializers/inject-objects.js.es6 index 2a3eded1dd..16e4a4b2c4 100644 --- a/app/assets/javascripts/discourse/initializers/inject-objects.js.es6 +++ b/app/assets/javascripts/discourse/initializers/inject-objects.js.es6 @@ -45,6 +45,6 @@ export default { inject(app, 'currentUser', 'component', 'route', 'controller'); app.register('message-bus:main', window.MessageBus, { instantiate: false }); - inject(app, 'messageBus', 'route', 'controller', 'view'); + inject(app, 'messageBus', 'route', 'controller', 'view', 'component'); } }; diff --git a/app/assets/javascripts/discourse/lib/ajax-error.js.es6 b/app/assets/javascripts/discourse/lib/ajax-error.js.es6 index 567fc2e45c..b7a8f24c6f 100644 --- a/app/assets/javascripts/discourse/lib/ajax-error.js.es6 +++ b/app/assets/javascripts/discourse/lib/ajax-error.js.es6 @@ -29,6 +29,8 @@ function extractError(error) { if (parsedJSON) { if (parsedJSON.errors && parsedJSON.errors.length > 0) { parsedError = parsedJSON.errors[0]; + } else if (parsedJSON.error) { + parsedError = parsedJSON.error; } else if (parsedJSON.failed) { parsedError = parsedJSON.message; } diff --git a/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 b/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 index 27cdb90913..5d7b18db0f 100644 --- a/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 +++ b/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 @@ -7,7 +7,6 @@ let lastAction = -1; const focusTrackerKey = "focus-tracker"; const idleThresholdTime = 1000 * 10; // 10 seconds -let notificationTagName; // "discourse-notification-popup-" + Discourse.SiteSettings.title; // Called from an initializer function init(messageBus) { @@ -25,8 +24,6 @@ function init(messageBus) { return; } - - if (!("Notification" in window)) { Em.Logger.info('Discourse desktop notifications are disabled - not supported by browser'); return; @@ -55,8 +52,6 @@ function init(messageBus) { // This function is only called if permission was granted function setupNotifications() { - notificationTagName = "discourse-notification-popup-" + Discourse.SiteSettings.title; - window.addEventListener("storage", function(e) { // note: This event only fires when other tabs setItem() const key = e.key; @@ -108,13 +103,14 @@ function onNotification(data) { const notificationBody = data.excerpt; const notificationIcon = Discourse.SiteSettings.logo_small_url || Discourse.SiteSettings.logo_url; + const notificationTag = "discourse-notification-" + Discourse.SiteSettings.title + "-" + data.topic_id; requestPermission().then(function() { // This shows the notification! const notification = new Notification(notificationTitle, { body: notificationBody, icon: notificationIcon, - tag: notificationTagName + tag: notificationTag }); function clickEventHandler() { diff --git a/app/assets/javascripts/discourse/lib/discourse-location.js.es6 b/app/assets/javascripts/discourse/lib/discourse-location.js.es6 index 41a415fcdd..b7299f2c0c 100644 --- a/app/assets/javascripts/discourse/lib/discourse-location.js.es6 +++ b/app/assets/javascripts/discourse/lib/discourse-location.js.es6 @@ -20,7 +20,7 @@ const popstateCallbacks = []; */ const DiscourseLocation = Ember.Object.extend({ - init: function() { + init() { set(this, 'location', get(this, 'location') || window.location); this.initState(); }, @@ -32,11 +32,11 @@ const DiscourseLocation = Ember.Object.extend({ @method initState */ - initState: function() { + initState() { set(this, 'history', get(this, 'history') || window.history); - var url = this.formatURL(this.getURL()), - loc = get(this, 'location'); + let url = this.formatURL(this.getURL()); + const loc = get(this, 'location'); if (loc && loc.hash) { url += loc.hash; @@ -60,15 +60,15 @@ const DiscourseLocation = Ember.Object.extend({ @method getURL */ - getURL: function() { - var rootURL = (Discourse.BaseUri === undefined ? "/" : Discourse.BaseUri), - location = get(this, 'location'), - url = location.pathname; + getURL() { + const location = get(this, 'location'); + let rootURL = (Discourse.BaseUri === undefined ? "/" : Discourse.BaseUri); + let url = location.pathname; rootURL = rootURL.replace(/\/$/, ''); url = url.replace(rootURL, ''); - var search = location.search || ''; + const search = location.search || ''; url += search; return url; @@ -82,8 +82,8 @@ const DiscourseLocation = Ember.Object.extend({ @method setURL @param path {String} */ - setURL: function(path) { - var state = this.getState(); + setURL(path) { + const state = this.getState(); path = this.formatURL(path); if (state && state.path !== path) { @@ -100,8 +100,8 @@ const DiscourseLocation = Ember.Object.extend({ @method replaceURL @param path {String} */ - replaceURL: function(path) { - var state = this.getState(); + replaceURL(path) { + const state = this.getState(); path = this.formatURL(path); if (state && state.path !== path) { @@ -114,11 +114,11 @@ const DiscourseLocation = Ember.Object.extend({ Get the current `history.state` Polyfill checks for native browser support and falls back to retrieving - from a private _historyState variable + from a private _historyState constiable @method getState */ - getState: function() { + getState() { return supportsHistoryState ? get(this, 'history').state : this._historyState; }, @@ -130,8 +130,8 @@ const DiscourseLocation = Ember.Object.extend({ @method pushState @param path {String} */ - pushState: function(path) { - var state = { path: path }; + pushState(path) { + const state = { path: path }; // store state if browser doesn't support `history.state` if (!supportsHistoryState) { @@ -152,8 +152,8 @@ const DiscourseLocation = Ember.Object.extend({ @method replaceState @param path {String} */ - replaceState: function(path) { - var state = { path: path }; + replaceState(path) { + const state = { path: path }; // store state if browser doesn't support `history.state` if (!supportsHistoryState) { @@ -175,8 +175,8 @@ const DiscourseLocation = Ember.Object.extend({ @method onUpdateURL @param callback {Function} */ - onUpdateURL: function(callback) { - var guid = Ember.guidFor(this), + onUpdateURL(callback) { + const guid = Ember.guidFor(this), self = this; Ember.$(window).on('popstate.ember-location-'+guid, function() { @@ -185,7 +185,7 @@ const DiscourseLocation = Ember.Object.extend({ popstateFired = true; if (self.getURL() === self._previousURL) { return; } } - var url = self.getURL(); + const url = self.getURL(); popstateCallbacks.forEach(function(cb) { cb(url); }); @@ -201,8 +201,8 @@ const DiscourseLocation = Ember.Object.extend({ @method formatURL @param url {String} */ - formatURL: function(url) { - var rootURL = get(this, 'rootURL'); + formatURL(url) { + let rootURL = get(this, 'rootURL'); if (url !== '') { rootURL = rootURL.replace(/\/$/, ''); @@ -215,8 +215,8 @@ const DiscourseLocation = Ember.Object.extend({ return rootURL + url; }, - willDestroy: function() { - var guid = Ember.guidFor(this); + willDestroy() { + const guid = Ember.guidFor(this); Ember.$(window).off('popstate.ember-location-'+guid); } @@ -230,7 +230,7 @@ const DiscourseLocation = Ember.Object.extend({ **/ CloakedCollectionView.reopen({ _watchForPopState: function() { - var self = this, + const self = this, cb = function() { // Sam: This is a hack, but a very important one // Due to the way we use replace state the back button works strangely diff --git a/app/assets/javascripts/discourse/lib/keyboard_shortcuts.js b/app/assets/javascripts/discourse/lib/keyboard_shortcuts.js index 16ce57a554..abb6b951c7 100644 --- a/app/assets/javascripts/discourse/lib/keyboard_shortcuts.js +++ b/app/assets/javascripts/discourse/lib/keyboard_shortcuts.js @@ -4,7 +4,8 @@ var PATH_BINDINGS = { 'g n': '/new', 'g u': '/unread', 'g c': '/categories', - 'g t': '/top' + 'g t': '/top', + 'g b': '/bookmarks' }, SELECTED_POST_BINDINGS = { diff --git a/app/assets/javascripts/discourse/lib/load-script.js.es6 b/app/assets/javascripts/discourse/lib/load-script.js.es6 index 1c56f9f029..f0398cecdc 100644 --- a/app/assets/javascripts/discourse/lib/load-script.js.es6 +++ b/app/assets/javascripts/discourse/lib/load-script.js.es6 @@ -1,18 +1,23 @@ /* global assetPath */ const _loaded = {}; +const _loading = {}; function loadWithTag(path, cb) { const head = document.getElementsByTagName('head')[0]; let s = document.createElement('script'); s.src = path; + if (Ember.Test) { Ember.Test.pendingAjaxRequests++; } head.appendChild(s); s.onload = s.onreadystatechange = function(_, abort) { + if (Ember.Test) { Ember.Test.pendingAjaxRequests--; } if (abort || !s.readyState || s.readyState === "loaded" || s.readyState === "complete") { s = s.onload = s.onreadystatechange = null; - if (!abort) { cb(); } + if (!abort) { + Ember.run(null, cb); + } } }; } @@ -25,9 +30,20 @@ export default function loadScript(url, opts) { // If we already loaded this url if (_loaded[url]) { return resolve(); } + if (_loading[url]) { return _loading[url].then(resolve);} + + var done; + _loading[url] = new Ember.RSVP.Promise(function(_done){ + done = _done; + }); + + _loading[url].then(function(){ + delete _loading[url]; + }); const cb = function() { _loaded[url] = true; + done(); resolve(); }; diff --git a/app/assets/javascripts/discourse/lib/url.js b/app/assets/javascripts/discourse/lib/url.js index 25f6bb32fc..e5a8b6840e 100644 --- a/app/assets/javascripts/discourse/lib/url.js +++ b/app/assets/javascripts/discourse/lib/url.js @@ -89,7 +89,7 @@ Discourse.URL = Ember.Object.createWithMixins({ Em.run.schedule('afterRender', function() { var $elem = $(id); if ($elem.length === 0) { - $elem = $("[name=" + id.replace('#', '')); + $elem = $("[name='" + id.replace('#', '') + "']"); } if ($elem.length > 0) { $('html,body').scrollTop($elem.offset().top - $('header').height() - 15); @@ -230,7 +230,7 @@ Discourse.URL = Ember.Object.createWithMixins({ var self = this; postStream.refresh(opts).then(function() { topicController.setProperties({ - currentPost: closest, + 'model.currentPost': closest, enteredAt: new Date().getTime().toString() }); var closestPost = postStream.closestPostForPostNumber(closest), diff --git a/app/assets/javascripts/discourse/lib/utilities.js b/app/assets/javascripts/discourse/lib/utilities.js index f51fd4d46f..556c5c89f3 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js +++ b/app/assets/javascripts/discourse/lib/utilities.js @@ -1,10 +1,3 @@ -/** - General utility functions - - @class Utilities - @namespace Discourse - @module Discourse -**/ Discourse.Utilities = { translateSize: function(size) { @@ -195,22 +188,10 @@ Discourse.Utilities = { return true; }, - - /** - Determine whether all file extensions are authorized. - - @method authorizesAllExtensions - **/ authorizesAllExtensions: function() { return Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0; }, - /** - Check the extension of the file against the list of authorized extensions - - @method isAuthorizedUpload - @param {File} file The file we want to upload - **/ isAuthorizedUpload: function(file) { if (file && file.name) { var extensions = _.chain(Discourse.SiteSettings.authorized_extensions.split("|")) @@ -222,11 +203,6 @@ Discourse.Utilities = { return false; }, - /** - List the authorized extension for display - - @method authorizedExtensions - **/ authorizedExtensions: function() { return _.chain(Discourse.SiteSettings.authorized_extensions.split("|")) .reject(function(extension) { return extension.indexOf("*") >= 0; }) @@ -235,12 +211,6 @@ Discourse.Utilities = { .join(", "); }, - /** - Get the markdown template for an upload (either an image or an attachment) - - @method getUploadMarkdown - @param {Upload} upload The upload we want the markdown from - **/ getUploadMarkdown: function(upload) { if (Discourse.Utilities.isAnImage(upload.original_filename)) { return ''; @@ -249,12 +219,6 @@ Discourse.Utilities = { } }, - /** - Check whether the path is refering to an image - - @method isAnImage - @param {String} path The path - **/ isAnImage: function(path) { return (/\.(png|jpe?g|gif|bmp|tiff?|svg|webp)$/i).test(path); }, @@ -264,11 +228,6 @@ Discourse.Utilities = { (/(png|jpe?g|gif|bmp|tiff?|svg|webp)/i).test(Discourse.Utilities.authorizedExtensions()); }, - /** - Determines whether we allow attachments or not - - @method allowsAttachments - **/ allowsAttachments: function() { return Discourse.Utilities.authorizesAllExtensions() || !(/((png|jpe?g|gif|bmp|tiff?|svg|webp)(,\s)?)+$/i).test(Discourse.Utilities.authorizedExtensions()); @@ -296,60 +255,14 @@ Discourse.Utilities = { } return; } + } else if (data.errors && data.errors.length > 0) { + bootbox.alert(data.errors.join("\n")); + return; } // otherwise, display a generic error message bootbox.alert(I18n.t('post.errors.upload')); }, - /** - Crop an image to be used as avatar. - Simulate the "centered square thumbnail" generation done server-side. - Uses only the first frame of animated gifs when they are disabled. - - @method cropAvatar - @param {String} url The url of the avatar - @param {String} fileType The file type of the uploaded file - @returns {Promise} a promise that will eventually be the cropped avatar. - **/ - cropAvatar: function(url, fileType) { - if (Discourse.SiteSettings.allow_animated_avatars && fileType === "image/gif") { - // can't crop animated gifs... let the browser stretch the gif - return Ember.RSVP.resolve(url); - } else { - return new Ember.RSVP.Promise(function(resolve) { - var image = document.createElement("img"); - image.crossOrigin = 'Anonymous'; - // this event will be fired as soon as the image is loaded - image.onload = function(e) { - var img = e.target; - // computes the dimension & position (x, y) of the largest square we can fit in the image - var width = img.width, height = img.height, dimension, center, x, y; - if (width <= height) { - dimension = width; - center = height / 2; - x = 0; - y = center - (dimension / 2); - } else { - dimension = height; - center = width / 2; - x = center - (dimension / 2); - y = 0; - } - // set the size of the canvas to the maximum available size for avatars (browser will take care of downsizing the image) - var canvas = document.createElement("canvas"); - var size = Discourse.Utilities.getRawSize(Discourse.Utilities.translateSize("huge")); - canvas.height = canvas.width = size; - // draw the image into the canvas - canvas.getContext("2d").drawImage(img, x, y, dimension, dimension, 0, 0, size, size); - // retrieve the image from the canvas - resolve(canvas.toDataURL(fileType)); - }; - // launch the onload event - image.src = url; - }); - } - }, - defaultHomepage: function() { // the homepage is the first item of the 'top_menu' site setting return Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0]; diff --git a/app/assets/javascripts/discourse/mixins/scroll-top.js.es6 b/app/assets/javascripts/discourse/mixins/scroll-top.js.es6 new file mode 100644 index 0000000000..e3a5a81f64 --- /dev/null +++ b/app/assets/javascripts/discourse/mixins/scroll-top.js.es6 @@ -0,0 +1,12 @@ +function scrollTop() { + if (Discourse.URL.isJumpScheduled()) { return; } + Ember.run.schedule('afterRender', function() { + $(document).scrollTop(0); + }); +} + +export default Ember.Mixin.create({ + _scrollTop: scrollTop.on('didInsertElement') +}); + +export { scrollTop }; diff --git a/app/assets/javascripts/discourse/mixins/scroll_top.js b/app/assets/javascripts/discourse/mixins/scroll_top.js deleted file mode 100644 index b183fbbb2d..0000000000 --- a/app/assets/javascripts/discourse/mixins/scroll_top.js +++ /dev/null @@ -1,8 +0,0 @@ -Discourse.ScrollTop = Em.Mixin.create({ - _scrollTop: function() { - if (Discourse.URL.isJumpScheduled()) { return; } - Em.run.schedule('afterRender', function() { - $(document).scrollTop(0); - }); - }.on('didInsertElement') -}); diff --git a/app/assets/javascripts/discourse/mixins/upload.js.es6 b/app/assets/javascripts/discourse/mixins/upload.js.es6 index ac91a056de..a056622804 100644 --- a/app/assets/javascripts/discourse/mixins/upload.js.es6 +++ b/app/assets/javascripts/discourse/mixins/upload.js.es6 @@ -2,27 +2,33 @@ export default Em.Mixin.create({ uploading: false, uploadProgress: 0, - uploadDone: function() { + uploadDone() { Em.warn("You should implement `uploadDone`"); }, - deleteDone: function() { - Em.warn("You should implement `deleteDone`"); - }, + _initialize: function() { + const $upload = this.$(), + csrf = Discourse.Session.currentProp("csrfToken"), + uploadUrl = this.getWithDefault("uploadUrl", "/uploads"), + reset = () => this.setProperties({ uploading: false, uploadProgress: 0}); - _initializeUploader: function() { - var $upload = this.$(), - self = this, - csrf = Discourse.Session.currentProp("csrfToken"); + this.messageBus.subscribe("/uploads/" + this.get("type"), upload => { + if (upload && upload.url) { + this.uploadDone(upload); + } else { + Discourse.Utilities.displayErrorForUpload(upload); + } + reset(); + }); $upload.fileupload({ - url: this.get('uploadUrl') + ".json?authenticity_token=" + encodeURIComponent(csrf), + url: uploadUrl + ".json?authenticity_token=" + encodeURIComponent(csrf), dataType: "json", dropZone: $upload, pasteZone: $upload }); - $upload.on("fileuploaddrop", function (e, data) { + $upload.on("fileuploaddrop", (e, data) => { if (data.files.length > 10) { bootbox.alert(I18n.t("post.errors.too_many_dragged_and_dropped_files")); return false; @@ -31,51 +37,31 @@ export default Em.Mixin.create({ } }); - $upload.on('fileuploadsubmit', function (e, data) { - var isValid = Discourse.Utilities.validateUploadedFiles(data.files, true); - var form = { image_type: self.get('type') }; - if (self.get("data")) { form = $.extend(form, self.get("data")); } + $upload.on("fileuploadsubmit", (e, data) => { + const isValid = Discourse.Utilities.validateUploadedFiles(data.files, true); + let form = { type: this.get("type") }; + if (this.get("data")) { form = $.extend(form, this.get("data")); } data.formData = form; - self.setProperties({ uploadProgress: 0, uploading: isValid }); + this.setProperties({ uploadProgress: 0, uploading: isValid }); return isValid; }); - $upload.on("fileuploadprogressall", function(e, data) { - var progress = parseInt(data.loaded / data.total * 100, 10); - self.set("uploadProgress", progress); + $upload.on("fileuploadprogressall", (e, data) => { + const progress = parseInt(data.loaded / data.total * 100, 10); + this.set("uploadProgress", progress); }); - $upload.on("fileuploaddone", function(e, data) { - if (data.result) { - if (data.result.url) { - self.uploadDone(data); - } else { - if (data.result.message) { - bootbox.alert(data.result.message); - } else if (data.result.length > 0) { - bootbox.alert(data.result.join("\n")); - } else { - bootbox.alert(I18n.t('post.errors.upload')); - } - } - } else { - bootbox.alert(I18n.t('post.errors.upload')); - } - }); - - $upload.on("fileuploadfail", function(e, data) { + $upload.on("fileuploadfail", (e, data) => { Discourse.Utilities.displayErrorForUpload(data); + reset(); }); + }.on("didInsertElement"), - $upload.on("fileuploadalways", function() { - self.setProperties({ uploading: false, uploadProgress: 0}); - }); - }.on('didInsertElement'), - - _destroyUploader: function() { - var $upload = this.$(); - try { $upload.fileupload('destroy'); } + _destroy: function() { + this.messageBus.unsubscribe("/uploads/" + this.get("type")); + const $upload = this.$(); + try { $upload.fileupload("destroy"); } catch (e) { /* wasn't initialized yet */ } $upload.off(); - }.on('willDestroyElement') + }.on("willDestroyElement") }); diff --git a/app/assets/javascripts/discourse/models/action_summary.js b/app/assets/javascripts/discourse/models/action-summary.js.es6 similarity index 94% rename from app/assets/javascripts/discourse/models/action_summary.js rename to app/assets/javascripts/discourse/models/action-summary.js.es6 index 729c5c629e..4d011da75f 100644 --- a/app/assets/javascripts/discourse/models/action_summary.js +++ b/app/assets/javascripts/discourse/models/action-summary.js.es6 @@ -1,4 +1,7 @@ -Discourse.ActionSummary = Discourse.Model.extend({ +import RestModel from 'discourse/models/rest'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; + +export default RestModel.extend({ // Description for the action description: function() { @@ -84,10 +87,9 @@ Discourse.ActionSummary = Discourse.Model.extend({ if (!self.get('flagTopic')) { return post.updateActionsSummary(result); } - }).catch(function (error) { + }).catch(function(error) { + popupAjaxError(error); self.removeAction(); - var message = $.parseJSON(error.responseText).errors; - bootbox.alert(message); }); }, diff --git a/app/assets/javascripts/discourse/models/category.js b/app/assets/javascripts/discourse/models/category.js index d01e93110d..c354f0d48b 100644 --- a/app/assets/javascripts/discourse/models/category.js +++ b/app/assets/javascripts/discourse/models/category.js @@ -90,7 +90,7 @@ Discourse.Category = Discourse.Model.extend({ }.property("permissions"), destroy: function() { - return Discourse.ajax("/categories/" + (this.get('slug') || this.get('id')), { type: 'DELETE' }); + return Discourse.ajax("/categories/" + (this.get('id') || this.get('slug')), { type: 'DELETE' }); }, addPermission: function(permission){ diff --git a/app/assets/javascripts/discourse/models/login_method.js b/app/assets/javascripts/discourse/models/login_method.js index e969fefb0e..074cdedf27 100644 --- a/app/assets/javascripts/discourse/models/login_method.js +++ b/app/assets/javascripts/discourse/models/login_method.js @@ -26,12 +26,7 @@ Discourse.LoginMethod.reopenClass({ var methods = this.methods = Em.A(); - /* - * enable_google_logins etc. - * */ - - [ "google", - "google_oauth2", + [ "google_oauth2", "facebook", "cas", "twitter", @@ -42,7 +37,7 @@ Discourse.LoginMethod.reopenClass({ var params = {name: name}; - if (name === "google" || name === "google_oauth2") { + if (name === "google_oauth2") { params.frameWidth = 850; params.frameHeight = 500; } else if (name === "facebook") { diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index d03ca5b000..aecb9cbc4c 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -69,7 +69,7 @@ const User = RestModel.extend({ profileBackground: function() { var url = this.get('profile_background'); if (Em.isEmpty(url) || !Discourse.SiteSettings.allow_profile_backgrounds) { return; } - return 'background-image: url(' + Discourse.getURLWithCDN(url) + ')'; + return ('background-image: url(' + Discourse.getURLWithCDN(url) + ')').htmlSafe(); }.property('profile_background'), /** @@ -171,27 +171,31 @@ const User = RestModel.extend({ @returns {Promise} the result of the operation **/ save: function() { - var self = this, - data = this.getProperties('auto_track_topics_after_msecs', - 'bio_raw', - 'website', - 'location', - 'name', - 'locale', - 'email_digests', - 'email_direct', - 'email_always', - 'email_private_messages', - 'dynamic_favicon', - 'digest_after_days', - 'new_topic_duration_minutes', - 'external_links_in_new_tab', - 'mailing_list_mode', - 'enable_quoting', - 'disable_jump_reply', - 'custom_fields', - 'user_fields', - 'muted_usernames'); + const self = this, + data = this.getProperties( + 'auto_track_topics_after_msecs', + 'bio_raw', + 'website', + 'location', + 'name', + 'locale', + 'email_digests', + 'email_direct', + 'email_always', + 'email_private_messages', + 'dynamic_favicon', + 'digest_after_days', + 'new_topic_duration_minutes', + 'external_links_in_new_tab', + 'mailing_list_mode', + 'enable_quoting', + 'disable_jump_reply', + 'custom_fields', + 'user_fields', + 'muted_usernames', + 'profile_background', + 'card_background' + ); ['muted','watched','tracked'].forEach(function(s){ var cats = self.get(s + 'Categories').map(function(c){ return c.get('id')}); @@ -204,6 +208,8 @@ const User = RestModel.extend({ data['edit_history_public'] = this.get('edit_history_public'); } + // TODO: We can remove this when migrated fully to rest model. + this.set('isSaving', true); return Discourse.ajax("/users/" + this.get('username_lower'), { data: data, type: 'PUT' @@ -212,6 +218,8 @@ const User = RestModel.extend({ var userProps = self.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon'); Discourse.User.current().setProperties(userProps); + }).finally(() => { + this.set('isSaving', false); }); }, diff --git a/app/assets/javascripts/discourse/routes/discovery.js.es6 b/app/assets/javascripts/discourse/routes/discovery.js.es6 index 572b9536bd..e5be7da1e3 100644 --- a/app/assets/javascripts/discourse/routes/discovery.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery.js.es6 @@ -2,11 +2,11 @@ The parent route for all discovery routes. Handles the logic for showing the loading spinners. **/ - import ShowFooter from "discourse/mixins/show-footer"; import OpenComposer from "discourse/mixins/open-composer"; +import { scrollTop } from 'discourse/mixins/scroll-top'; -const DiscoveryRoute = Discourse.Route.extend(Discourse.ScrollTop, OpenComposer, ShowFooter, { +const DiscoveryRoute = Discourse.Route.extend(OpenComposer, ShowFooter, { redirect: function() { return this.redirectIfLoginRequired(); }, beforeModel: function(transition) { @@ -27,7 +27,7 @@ const DiscoveryRoute = Discourse.Route.extend(Discourse.ScrollTop, OpenComposer, loadingComplete: function() { this.controllerFor('discovery').set('loading', false); if (!this.session.get('topicListScrollPosition')) { - this._scrollTop(); + scrollTop(); } }, diff --git a/app/assets/javascripts/discourse/routes/restricted-user.js.es6 b/app/assets/javascripts/discourse/routes/restricted-user.js.es6 index 46cc1fe6fe..fa95efdb0b 100644 --- a/app/assets/javascripts/discourse/routes/restricted-user.js.es6 +++ b/app/assets/javascripts/discourse/routes/restricted-user.js.es6 @@ -1,6 +1,7 @@ -// A base route that allows us to redirect when access is restricted +import DiscourseRoute from 'discourse/routes/discourse'; -export default Discourse.Route.extend({ +// A base route that allows us to redirect when access is restricted +export default DiscourseRoute.extend({ afterModel() { if (!this.modelFor('user').get('can_edit')) { diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 61f660991a..c86022e63e 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -155,10 +155,7 @@ const TopicRoute = Discourse.Route.extend(ShowFooter, { let topic = this.modelFor('topic'); if (topic && (topic.get('id') === parseInt(params.id, 10))) { this.setupParams(topic, queryParams); - // If we have the existing model, refresh it - return topic.get('postStream').refresh().then(function() { - return topic; - }); + return topic; } else { topic = this.store.createRecord('topic', _.omit(params, 'username_filters', 'filter')); return this.setupParams(topic, queryParams); diff --git a/app/assets/javascripts/discourse/routes/user-index.js.es6 b/app/assets/javascripts/discourse/routes/user-index.js.es6 index a2e100000a..24a536237e 100644 --- a/app/assets/javascripts/discourse/routes/user-index.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-index.js.es6 @@ -1,7 +1,14 @@ export default Discourse.Route.extend({ beforeModel: function() { - this.replaceWith('userActivity'); + // HACK: Something with the way the user card intercepts clicks seems to break how the + // transition into a user's activity works. This makes the back button work on mobile + // where there is no user card as well as desktop where there is. + if (Discourse.Mobile.mobileView) { + this.replaceWith('userActivity'); + } else { + this.transitionTo('userActivity'); + } } }); diff --git a/app/assets/javascripts/discourse/templates/components/image-uploader.hbs b/app/assets/javascripts/discourse/templates/components/image-uploader.hbs index 32c0dce129..9b35716e8e 100644 --- a/app/assets/javascripts/discourse/templates/components/image-uploader.hbs +++ b/app/assets/javascripts/discourse/templates/components/image-uploader.hbs @@ -1,6 +1,6 @@ -
+
-
{{i18n "admin.plugins.name"}}{{i18n "admin.plugins.version"}}
{{i18n "admin.plugins.name"}}{{i18n "admin.plugins.version"}}
diff --git a/app/assets/javascripts/admin/templates/reports/per_day_counts_report.hbs b/app/assets/javascripts/admin/templates/reports/per_day_counts_report.hbs deleted file mode 100644 index bc025cdf8f..0000000000 --- a/app/assets/javascripts/admin/templates/reports/per_day_counts_report.hbs +++ /dev/null @@ -1,8 +0,0 @@ -
{{title}}{{todayCount}}{{yesterdayCount}}{{sevenDaysAgoCount}}{{thirtyDaysAgoCount}}
{{title}}{{#link-to 'adminUsersList.show' 'newuser'}}{{value-at-tl data level="0"}}{{/link-to}}{{#link-to 'adminUsersList.show' 'basic'}}{{value-at-tl data level="1"}}{{/link-to}}{{#link-to 'adminUsersList.show' 'regular'}}{{value-at-tl data level="2"}}{{/link-to}}{{#link-to 'adminUsersList.show' 'leader'}}{{value-at-tl data level="3"}}{{/link-to}}{{#link-to 'adminUsersList.show' 'elder'}}{{value-at-tl data level="4"}}{{/link-to}}
- {{#each link in infoLinks}} + {{#each infoLinks as |link|}}